diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4a7ea30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4bd771c..0bf7e22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,8 +89,8 @@ jobs: version: ${{ env.PNPM_VERSION }} - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Run tests with coverage - run: pnpm test:coverage + - name: Run tests + run: pnpm test env: DATABASE_URL: "postgresql://shieldai:shieldai_dev@localhost:5432/shieldai" REDIS_URL: "redis://localhost:6379" @@ -106,22 +106,6 @@ jobs: name: Docker Build runs-on: ubuntu-latest needs: [lint, typecheck, test] - strategy: - fail-fast: false - matrix: - include: - - name: api - context: . - dockerfile: packages/api/Dockerfile - - name: darkwatch - context: . - dockerfile: services/darkwatch/Dockerfile - - name: spamshield - context: . - dockerfile: services/spamshield/Dockerfile - - name: voiceprint - context: . - dockerfile: services/voiceprint/Dockerfile steps: - uses: actions/checkout@v4 - name: Docker Buildx @@ -129,10 +113,9 @@ jobs: - name: Build Docker image uses: docker/build-push-action@v5 with: - context: ${{ matrix.context }} - file: ${{ matrix.dockerfile }} + context: . push: false - tags: shieldai-${{ matrix.name }}:${{ github.sha }} + tags: shieldai:${{ github.sha }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 238b97e..4408e8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ node_modules dist +.output .env *.log .DS_Store -load-tests/voiceprint/results/ .turbo +.nitro diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ec506e9..0000000 --- a/Dockerfile +++ /dev/null @@ -1,38 +0,0 @@ -# Build stage -FROM node:18-alpine AS builder - -WORKDIR /app - -# Copy package files -COPY package*.json ./ -COPY apps/ ./apps/ -COPY packages/ ./packages/ - -# Install dependencies -RUN npm ci - -# Build all packages -RUN npm run build - -# Production stage -FROM node:18-alpine AS production - -WORKDIR /app - -# Copy package files -COPY package*.json ./ -COPY apps/ ./apps/ -COPY packages/ ./packages/ - -# Copy built artifacts from builder -COPY --from=builder /app/apps/web/dist ./apps/web/dist -COPY --from=builder /app/apps/api/dist ./apps/api/dist - -# Install production dependencies only -RUN npm ci --production - -# Expose port -EXPOSE 3000 - -# Start the API server -CMD ["node", "apps/api/dist/index.js"] diff --git a/check-identity.js b/check-identity.js deleted file mode 100644 index e9512bc..0000000 --- a/check-identity.js +++ /dev/null @@ -1,50 +0,0 @@ -const http = require('http'); - -const agentId = process.env.PAPERCLIP_AGENT_ID; -const apiKey = process.env.PAPERCLIP_API_KEY; -const apiUrl = process.env.PAPERCLIP_API_URL; -const runId = process.env.PAPERCLIP_RUN_ID; - -console.log('Agent ID:', agentId); -console.log('API URL:', apiUrl); -console.log('Run ID:', runId); - -if (!apiKey || !apiUrl) { - console.error('Missing environment variables'); - process.exit(1); -} - -async function fetchJson(url, options = {}) { - const request = http.request({ - hostname: new URL(url).hostname, - port: new URL(url).port, - path: new URL(url).pathname, - method: options.method || 'GET', - headers: { - 'Authorization': `Bearer ${apiKey}`, - 'X-Paperclip-Run-Id': runId, - ...options.headers - } - }, (response) => { - let data = ''; - response.on('data', chunk => data += chunk); - response.on('end', () => { - try { - console.log(JSON.stringify(JSON.parse(data), null, 2)); - } catch (e) { - console.log(data); - } - }); - }); - request.on('error', console.error); - request.end(); -} - -console.log('\n=== FETCHING AGENT IDENTITY ===\n'); -fetchJson(`${apiUrl}/api/agents/me`).catch(console.error); - -console.log('\n=== FETCHING INBOX-LITE ===\n'); -fetchJson(`${apiUrl}/api/agents/me/inbox-lite`).catch(console.error); - -console.log('\n=== FETCHING ALL ASSIGNED ISSUES ===\n'); -fetchJson(`${apiUrl}/api/companies/${apiKey.split('-')[0] || 'unknown'}/issues?assigneeAgentId=${agentId}&status=todo,in_progress,blocked`).catch(console.error); diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index b8e3d59..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -1,142 +0,0 @@ -version: '3.9' - -x-monitoring: &monitoring - DD_ENV: ${DD_ENV:-production} - DD_SERVICE: ${DD_SERVICE:-shieldai} - DD_VERSION: ${DOCKER_TAG:-latest} - DD_TRACE_ENABLED: ${DD_TRACE_ENABLED:-true} - DD_AGENT_HOST: datadog-agent - DD_AGENT_PORT: "8126" - DD_LOGS_INJECTION: "true" - SENTRY_DSN: ${SENTRY_DSN:-} - SENTRY_ENVIRONMENT: ${DD_ENV:-production} - SENTRY_RELEASE: ${DOCKER_TAG:-latest} - -services: - api: - image: ghcr.io/${GITHUB_REPOSITORY_OWNER}/shieldai-api:${DOCKER_TAG:-latest} - restart: unless-stopped - ports: - - "${PORT:-3000}:3000" - environment: - DATABASE_URL: "postgresql://shieldai:${POSTGRES_PASSWORD}@postgres:5432/shieldai" - REDIS_URL: "redis://redis:6379" - PORT: "3000" - LOG_LEVEL: info - HIBP_API_KEY: ${HIBP_API_KEY} - RESEND_API_KEY: ${RESEND_API_KEY} - <<: *monitoring - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - shieldai - - darkwatch: - image: ghcr.io/${GITHUB_REPOSITORY_OWNER}/shieldai-darkwatch:${DOCKER_TAG:-latest} - restart: unless-stopped - environment: - DATABASE_URL: "postgresql://shieldai:${POSTGRES_PASSWORD}@postgres:5432/shieldai" - REDIS_URL: "redis://redis:6379" - HIBP_API_KEY: ${HIBP_API_KEY} - DD_SERVICE: "shieldai-darkwatch" - <<: *monitoring - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - shieldai - - spamshield: - image: ghcr.io/${GITHUB_REPOSITORY_OWNER}/shieldai-spamshield:${DOCKER_TAG:-latest} - restart: unless-stopped - environment: - DATABASE_URL: "postgresql://shieldai:${POSTGRES_PASSWORD}@postgres:5432/shieldai" - REDIS_URL: "redis://redis:6379" - DD_SERVICE: "shieldai-spamshield" - <<: *monitoring - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - shieldai - - voiceprint: - image: ghcr.io/${GITHUB_REPOSITORY_OWNER}/shieldai-voiceprint:${DOCKER_TAG:-latest} - restart: unless-stopped - environment: - DATABASE_URL: "postgresql://shieldai:${POSTGRES_PASSWORD}@postgres:5432/shieldai" - REDIS_URL: "redis://redis:6379" - DD_SERVICE: "shieldai-voiceprint" - <<: *monitoring - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - networks: - - shieldai - - datadog-agent: - image: datadog/agent:7 - restart: unless-stopped - environment: - DD_API_KEY: ${DD_API_KEY} - DD_SITE: ${DD_SITE:-datadoghq.com} - DD_ENV: ${DD_ENV:-production} - DD_DOGSTATSD_NON_LOCAL_TRAFFIC: "true" - DD_APM_ENABLED: "true" - DD_APM_NON_LOCAL_TRAFFIC: "true" - DD_LOGS_ENABLED: "true" - DD_LOGS_CONFIG_CONTAINER_COLLECT_ALL: "true" - DD_HEALTH_PORT_ENABLE: "true" - ports: - - "8125:8125/udp" - - "8126:8126" - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - - /proc/:/host/proc/:ro - - /sys/fs/cgroup:/host/sys/fs/cgroup:ro - networks: - - shieldai - - postgres: - image: postgres:16-alpine - restart: unless-stopped - environment: - POSTGRES_DB: shieldai - POSTGRES_USER: shieldai - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} - volumes: - - pgdata:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U shieldai"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - shieldai - - redis: - image: redis:7-alpine - restart: unless-stopped - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 5s - retries: 5 - networks: - - shieldai - -volumes: - pgdata: - -networks: - shieldai: - driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 94c6c8b..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,53 +0,0 @@ -version: '3.8' - -services: - postgres: - image: postgres:15-alpine - container_name: shieldsai_postgres - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: shieldsai_dev - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] - interval: 5s - timeout: 5s - retries: 5 - - redis: - image: redis:7-alpine - container_name: shieldsai_redis - ports: - - "6379:6379" - volumes: - - redis_data:/data - healthcheck: - test: ["CMD", "redis-cli", "ping"] - interval: 5s - timeout: 5s - retries: 5 - - mailhog: - image: mailhog/mailhog:latest - container_name: shieldsai_mailhog - ports: - - "1025:1025" # SMTP - - "8025:8025" # Web UI - depends_on: - - postgres - - adminer: - image: adminer:4 - container_name: shieldsai_adminer - ports: - - "8080:8080" - depends_on: - - postgres - -volumes: - postgres_data: - redis_data: diff --git a/docs/MIXPANEL_ANALYTICS.md b/docs/MIXPANEL_ANALYTICS.md deleted file mode 100644 index e24c741..0000000 --- a/docs/MIXPANEL_ANALYTICS.md +++ /dev/null @@ -1,149 +0,0 @@ -# ShieldAI Mixpanel Analytics Configuration - -## Implementation Complete ✅ - -The Mixpanel analytics infrastructure is fully implemented and ready for use. This document covers setup and usage. - ---- - -## What's Implemented - -### Backend (`packages/shared-analytics`) -- **MixpanelService**: Full implementation using Segment analytics-node SDK -- **Event Taxonomy**: 30+ events defined in `EventType` enum covering: - - **User Events**: `user_signed_up`, `user_logged_in`, `user_upgraded`, `user_downgraded` - - **Subscription Events**: `subscription_created`, `subscription_updated`, `subscription_cancelled`, `subscription_renewed` - - **DarkWatch Events**: `dark_web_scan_started`, `dark_web_scan_completed`, `exposure_detected`, `exposure_resolved`, `watchlist_item_added`, `watchlist_item_removed` - - **VoicePrint Events**: `voice_enrolled`, `voice_analyzed`, `voice_match_found`, `synthetic_voice_detected` - - **SpamShield Events**: `call_analyzed`, `sms_analyzed`, `spam_blocked`, `spam_flagged`, `spam_feedback_submitted` - - **KPI Events**: `mrr_updated`, `conversion_occurred`, `churn_occurred`, `referral_sent`, `referral_converted` -- **User Identification**: `identify()` and `group()` methods for user segmentation -- **KPI Definitions**: MAU, MRR, conversion rate, churn, CAC, LTV, NPS, viral coefficient -- **Data Validation**: Zod schema for event properties - -### Frontend (`packages/web/src/hooks/useAnalytics.ts`) -- **Initialization**: Auto-loads Mixpanel script via `initMixpanel()` -- **Event Tracking**: `trackEvent(name, params)` for generic events -- **Page View Tracking**: `trackPageView(path, title)` with automatic page title -- **Waitlist Tracking**: `trackWaitlistSignup(email, source, tier)` with Meta Pixel integration -- **Multi-Platform Support**: GA4, Meta Pixel, LinkedIn Insight included - ---- - -## Setup Instructions - -### Step 1: Create Mixpanel Account (Manual) -1. Go to https://mixpanel.com -2. Sign up for an account (free tier: 100K monthly tracked users) -3. Create a new project named "ShieldAI" -4. Copy the **Project Token** from Project Settings → Project Token - -### Step 2: Configure Backend Environment Variables -Add to `ShieldAI/.env`: -```bash -# Mixpanel (Backend) -MIXPANEL_TOKEN= -MIXPANEL_API_SECRET= -ANALYTICS_ENV=development # or production, staging -``` - -### Step 3: Configure Frontend Environment Variables -Create or update `ShieldAI/packages/web/.env`: -```bash -# Mixpanel (Frontend - Vite) -VITE_MIXPANEL_TOKEN= -``` - -### Step 4: Verify Integration -Backend usage example: -```typescript -import { mixpanelService, EventType } from '@shieldsai/shared-analytics'; - -// Track an event -await mixpanelService.track(EventType.USER_SIGNED_UP, userId, { - plan: 'premium', - referrer: 'google', -}); - -// Identify a user -await mixpanelService.identify(userId, { - email: 'user@example.com', - tier: 'premium', -}); -``` - -Frontend usage example: -```typescript -import { trackEvent, trackWaitlistSignup, trackPageView } from './hooks/useAnalytics'; - -// Track a page view -trackPageView('/waitlist', 'Waitlist Signup'); - -// Track a signup -trackWaitlistSignup('user@example.com', 'landing_page', 'free'); - -// Track a custom event -trackEvent('feature_used', { feature: 'darkwatch_scan', duration: 45 }); -``` - ---- - -## Event Properties Schema - -All events support these standard properties (validated via Zod): -- `userId` (string, optional): User identifier -- `sessionId` (string, optional): Session identifier -- `timestamp` (Date, optional): Event timestamp -- `platform` (enum: 'web' | 'mobile' | 'desktop' | 'api', optional): Event source -- `version` (string, optional): App version -- `environment` (string, optional): deployment environment - -Additional custom properties are accepted and passed through to Mixpanel. - ---- - -## KPI Definitions - -| KPI | Description | Calculation | -|-----|-------------|-------------| -| MAU | Monthly Active Users | COUNT(DISTINCT userId) WHERE timestamp > NOW() - 30 DAYS | -| Paying Users | Active subscriptions | COUNT(DISTINCT userId) WHERE subscription.status = "active" | -| MRR | Monthly Recurring Revenue | SUM(subscription.amount) WHERE subscription.status = "active" | -| Conversion Rate | Free → Paid | COUNT(upgrade events) / COUNT(signup events) | -| Churn Rate | Cancellations | COUNT(cancel events) / COUNT(active subscriptions) | -| CAC | Customer Acquisition Cost | Total marketing spend / COUNT(new paying users) | -| LTV | Lifetime Value | Average subscription amount / Churn rate | -| NPS | Net Promoter Score | % Promoters - % Detractors | -| Viral Coefficient | Referral multiplier | COUNT(referral events) / COUNT(users) | - ---- - -## Alert Thresholds - -Configured thresholds for monitoring: -- **Churn**: Warning >5%, Critical >10% -- **Conversion Rate**: Warning <2%, Critical <1% -- **MRR**: Warning <90% target, Critical <80% target -- **NPS**: Warning <50, Critical <40 -- **Viral Coefficient**: Warning <0.4, Critical <0.3 - ---- - -## Files Modified - -- `packages/shared-analytics/src/config/analytics.config.ts` - Event taxonomy & KPIs -- `packages/shared-analytics/src/services/mixpanel.service.ts` - Mixpanel service -- `packages/shared-analytics/src/index.ts` - Exports -- `packages/web/src/hooks/useAnalytics.ts` - Frontend analytics hook -- `.env.example` - Environment variable templates -- `docs/MIXPANEL_ANALYTICS.md` - This documentation - ---- - -## Next Steps - -1. **Create Mixpanel account** (requires human action - no API for account creation) -2. **Add tokens to `.env`** files -3. **Test event tracking** in development -4. **Set up Mixpanel dashboards** for KPI monitoring -5. **Configure Mixpanel alerts** for threshold breaches diff --git a/docs/MOBILE_PUSH_INTEGRATION.md b/docs/MOBILE_PUSH_INTEGRATION.md deleted file mode 100644 index 352116d..0000000 --- a/docs/MOBILE_PUSH_INTEGRATION.md +++ /dev/null @@ -1,389 +0,0 @@ -# Push Notifications - Mobile App Quick Start - -This guide helps you integrate push notifications into the ShieldAI React Native mobile app. - -## Prerequisites - -Before starting, ensure: -- ✅ Backend push notification infrastructure is deployed -- ✅ Firebase project is set up (for Android) -- ✅ Apple Developer account is active (for iOS) -- ✅ Environment variables are configured on the backend - -## Step 1: Install Dependencies - -```bash -npm install @react-native-firebase/app @react-native-firebase/messaging -``` - -## Step 2: Android Setup - -### 2.1 Add Firebase to Android Project - -1. Go to [Firebase Console](https://console.firebase.google.com) -2. Select your ShieldAI project -3. Add Android app with package name: `com.shieldai.mobile` -4. Download `google-services.json` -5. Place it in `android/app/google-services.json` - -### 2.2 Update Build Configuration - -In `android/build.gradle`: -```gradle -buildscript { - dependencies { - classpath 'com.google.gms:google-services:4.4.0' - } -} -``` - -In `android/app/build.gradle`: -```gradle -apply plugin: 'com.google.gms.google-services' -``` - -### 2.3 Request Permissions - -In `AndroidManifest.xml`: -```xml - - - -``` - -## Step 3: iOS Setup - -### 3.1 Enable Push Notifications - -1. Open Xcode project -2. Select your app target -3. Go to "Signing & Capabilities" -4. Click "+ Capability" -5. Add "Push Notifications" -6. Add "Background Modes" and check "Remote notifications" - -### 3.2 Configure Firebase - -1. Download `GoogleService-Info.plist` from Firebase Console -2. Add it to your Xcode project (drag to project folder) -3. Ensure "Copy items if needed" is checked - -### 3.3 Add Import in AppDelegate.swift - -```swift -import FirebaseMessaging -import UserNotifications - -@main -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate { - - func application(_ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - FirebaseApp.configure() - Messaging.messaging().delegate = self - return true - } - - // Request permission - func requestAuthorization() { - UNUserNotificationCenter.current().delegate = self - let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] - UNUserNotificationCenter.current().requestAuthorization( - options: authOptions, - completionHandler: { granted, error in - if granted { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - } - ) - } - - // Handle token refresh - func messaging(_ messaging: Messaging, didRefreshRegistrationToken fcmToken: String) { - print("FCM token refreshed: \(fcmToken)") - // Send new token to backend - registerDeviceToken(fcmToken) - } - - // Handle foreground notifications - func userNotificationCenter(_ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: - @escaping (UNNotificationPresentationOptions) -> Void) { - let userInfo = notification.request.content.userInfo - print("Foreground notification: \(userInfo)") - completionHandler([.banner, .sound, .badge]) - } - - // Handle notification tap - func userNotificationCenter(_ center: UNUserNotificationCenter, - didReceive response: UNNotificationResponse, - withCompletionHandler completionHandler: @escaping () -> Void) { - let userInfo = response.notification.request.content.userInfo - print("Notification tapped: \(userInfo)") - completionHandler() - } -} - -extension AppDelegate: MessagingDelegate { - func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { - print("FCM token received: \(fcmToken ?? "none")") - if let token = fcmToken { - registerDeviceToken(token) - } - } -} - -// Helper function to register with backend -func registerDeviceToken(_ token: String) { - // Call your API to register the device - // POST /api/v1/devices/register - // Body: { userId, platform: "ios", token, deviceType: "mobile" } -} -``` - -## Step 4: React Native Integration - -### 4.1 Create Notification Service - -Create `src/services/NotificationService.ts`: - -```typescript -import messaging from '@react-native-firebase/messaging'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import axios from 'axios'; - -const API_BASE_URL = 'https://api.shieldai.com/api/v1'; - -export class NotificationService { - private static instance: NotificationService; - private userId: string | null = null; - - private constructor() {} - - static getInstance(): NotificationService { - if (!NotificationService.instance) { - NotificationService.instance = new NotificationService(); - } - return NotificationService.instance; - } - - setUserId(userId: string) { - this.userId = userId; - } - - async requestPermission(): Promise { - try { - const authStatus = await messaging().requestPermission(); - const enabled = - authStatus === messaging.AuthorizationStatus.AUTHORIZED || - authStatus === messaging.AuthorizationStatus.PROVISIONAL; - - if (enabled) { - console.log('Push notification permission granted'); - await this.registerDevice(); - return true; - } - return false; - } catch (error) { - console.error('Failed to request permission:', error); - return false; - } - } - - async registerDevice(): Promise { - if (!this.userId) { - console.warn('User ID not set, cannot register device'); - return; - } - - try { - const token = await messaging().getToken(); - const platform = Platform.OS === 'ios' ? 'ios' : 'android'; - - const response = await axios.post(`${API_BASE_URL}/devices/register`, { - userId: this.userId, - platform, - token, - deviceType: 'mobile', - appName: 'ShieldAI Mobile', - appVersion: '1.0.0', - }); - - console.log('Device registered:', response.data); - } catch (error) { - console.error('Failed to register device:', error); - } - } - - setupListeners() { - // Foreground messages - messaging().onMessage(async (remoteMessage) => { - console.log('Foreground message received:', remoteMessage); - // Show local notification - this.showLocalNotification(remoteMessage); - }); - - // Background messages - messaging().setBackgroundMessageHandler(async (remoteMessage) => { - console.log('Background message received:', remoteMessage); - }); - - // Notification opened - messaging().onNotificationOpenedApp((remoteMessage) => { - console.log('Notification opened app:', remoteMessage); - // Navigate to relevant screen - this.handleNotificationTap(remoteMessage); - }); - - // Check if app was opened from notification - messaging() - .getInitialNotification() - .then((remoteMessage) => { - if (remoteMessage) { - console.log('App opened from notification:', remoteMessage); - this.handleNotificationTap(remoteMessage); - } - }); - } - - private showLocalNotification(message: any) { - // Use react-native-push-notification or similar - console.log('Show notification:', message.notification?.title); - } - - private handleNotificationTap(message: any) { - // Navigate to relevant screen based on notification data - const { alertId, type } = message.data || {}; - if (alertId) { - // Navigate to alert detail - console.log('Navigate to alert:', alertId); - } - } - - async deregisterDevice(): Promise { - if (!this.userId) return; - - try { - const token = await messaging().getToken(); - await axios.post(`${API_BASE_URL}/devices/deregister`, { - token, - userId: this.userId, - }); - } catch (error) { - console.error('Failed to deregister device:', error); - } - } -} -``` - -### 4.2 Initialize in App Component - -```typescript -// App.tsx -import React, { useEffect } from 'react'; -import { NotificationService } from './src/services/NotificationService'; - -const notificationService = NotificationService.getInstance(); - -function App() { - useEffect(() => { - // Initialize push notifications - notificationService.setupListeners(); - }, []); - - // When user logs in - const handleLogin = async (userId: string) => { - notificationService.setUserId(userId); - await notificationService.requestPermission(); - }; - - // When user logs out - const handleLogout = async () => { - await notificationService.deregisterDevice(); - notificationService.setUserId(''); - }; - - return ( - // Your app components - ); -} -``` - -## Step 5: Testing - -### Android Emulator - -1. Start Android emulator with Google Play Services -2. Install and run app -3. Trigger a test notification from backend -4. Check notification appears - -### iOS Device (Required) - -iOS Simulator does not support push notifications. Use a real device. - -### Backend Test - -```bash -# Test notification API -curl -X POST https://api.shieldai.com/api/v1/notifications/send \ - -H "Authorization: Bearer YOUR_JWT_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "userId": "user-uuid", - "channel": "push", - "subject": "Test Alert", - "body": "This is a test notification from ShieldAI", - "priority": "high", - "metadata": { - "alertId": "alert-uuid", - "type": "darkweb_exposure" - } - }' -``` - -## Troubleshooting - -### Android Issues - -**Problem**: Token not received -- Check `google-services.json` is in correct location -- Verify Firebase project ID matches -- Check internet connection on emulator - -**Problem**: Notifications not showing -- Ensure notification permissions granted -- Check battery optimization settings -- Verify notification channel is created - -### iOS Issues - -**Problem**: Token not received -- Verify Push Notifications capability is enabled -- Check APNs key configuration in backend -- Ensure device is not in development mode if using production certs - -**Problem**: Notifications not showing -- Check notification permissions granted -- Verify APNs configuration on backend -- Ensure proper certificate/key is used - -## Next Steps - -1. ✅ Set up Firebase project -2. ✅ Configure APNs on Apple Developer Portal -3. ✅ Implement notification handling in app -4. ✅ Test on Android device/emulator -5. ✅ Test on iOS device -6. ✅ Integrate with DarkWatch and SpamShield alerts -7. ✅ Add notification preferences UI -8. ✅ Implement deep linking from notifications - -## Resources - -- [Firebase Cloud Messaging Docs](https://firebase.google.com/docs/cloud-messaging) -- [React Native Firebase Messaging](https://rnfirebase.io/messaging/usage) -- [Apple Push Notification Service](https://developer.apple.com/documentation/usernotifications) diff --git a/docs/PUSH_NOTIFICATIONS_SETUP.md b/docs/PUSH_NOTIFICATIONS_SETUP.md deleted file mode 100644 index 40c7dc3..0000000 --- a/docs/PUSH_NOTIFICATIONS_SETUP.md +++ /dev/null @@ -1,462 +0,0 @@ -# Push Notifications Setup Guide (FCM & APNs) - -## Overview - -This guide covers setting up Firebase Cloud Messaging (FCM) for Android and Apple Push Notification service (APNs) for iOS push notifications in the ShieldAI mobile app. - -## Architecture - -``` -┌─────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ Mobile │────▶│ API Gateway │────▶│ Notification │ -│ App │ │ /devices/* │ │ Service │ -└─────────────┘ └──────────────────┘ └────────┬────────┘ - │ - ┌──────────────────────────────┼──────────────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌────────────────┐ ┌─────────────────┐ ┌──────────────────┐ - │ Firebase │ │ Apple Push │ │ Redis (Rate │ - │ Cloud │ │ Notification │ │ Limiting) │ - │ Messaging │ │ Service │ │ │ - │ (Android) │ │ (iOS) │ │ │ - └────────────────┘ └─────────────────┘ └──────────────────┘ -``` - -## Environment Variables - -Add the following to your `.env` file: - -```bash -# Firebase Cloud Messaging (FCM) -FCM_PROJECT_ID=your-firebase-project-id -FCM_CLIENT_EMAIL=service-account@your-project.iam.gserviceaccount.com -FCM_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" - -# Apple Push Notification (APNs) -APNS_KEY_ID=your-key-id -APNS_TEAM_ID=your-team-id -APNS_BUNDLE_ID=com.yourcompany.shieldai -APNS_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" - -# Redis (for rate limiting) -REDIS_URL=redis://localhost:6379 - -# Rate Limits -PUSH_RATE_LIMIT=100 -RATE_LIMIT_WINDOW_SECONDS=60 -``` - -## Firebase Cloud Messaging (FCM) Setup - -### 1. Create Firebase Project - -1. Go to [Firebase Console](https://console.firebase.google.com/) -2. Click **Add project** -3. Enter project name: `shieldai-production` (or your preferred name) -4. Disable Google Analytics (optional) -5. Click **Create project** - -### 2. Enable Cloud Messaging - -1. In Firebase Console, go to **Project settings** -2. Scroll down to **Your apps** section -3. Click the **Web** icon `` to add a web app -4. Register app nickname: `ShieldAI API` -5. **Do not** check "Also set up for Firebase Hosting" -6. Click **Register app** -7. Copy the `firebaseConfig` for later use in mobile app - -### 3. Generate Service Account Key - -1. Go to **Project settings** → **Service accounts** -2. Click **Generate new private key** -3. Save the downloaded JSON file securely -4. Extract the following values: - - `project_id` - - `client_email` - - `private_key` - -### 4. Configure FCM in ShieldAI - -Create a file `packages/shared-notifications/.fcm-config.json` (gitignored): - -```json -{ - "projectId": "your-firebase-project-id", - "clientEmail": "service-account@your-project.iam.gserviceaccount.com", - "privateKey": "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" -} -``` - -Set environment variables: - -```bash -export FCM_PROJECT_ID="your-firebase-project-id" -export FCM_CLIENT_EMAIL="service-account@your-project.iam.gserviceaccount.com" -export FCM_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" -``` - -### 5. Install Firebase Admin SDK - -```bash -cd packages/shared-notifications -npm install firebase-admin -``` - -## Apple Push Notification (APNs) Setup - -### 1. Create App ID - -1. Go to [Apple Developer Portal](https://developer.apple.com/account/resources/identifiers/list) -2. Click **+** to create new identifier -3. Select **App IDs** → **App** -4. Enter description: `ShieldAI Mobile` -5. Enter Bundle ID: `com.yourcompany.shieldai` -6. Enable **Push Notifications** capability -7. Click **Continue** → **Register** - -### 2. Create APNs Key - -1. Go to **Certificates, IDs & Profiles** → **Keys** -2. Click **+** to create new key -3. Enter key name: `ShieldAI APNs Key` -4. Enable **Apple Push Notification service (APNs)** -5. Click **Continue** → **Register** -6. **Download the key** (`.p8` file) - you can only download once! -7. Note the **Key ID** displayed - -### 3. Configure APNs in ShieldAI - -Convert the `.p8` file to PEM format: - -```bash -# Convert .p8 to PEM -openssl pkcs8 -topk8 -nocrypt -in AuthKey_XXXXXX.p8 -out apns_key.pem - -# Copy the PEM content -cat apns_key.pem -``` - -Set environment variables: - -```bash -export APNS_KEY_ID="XXXXXX" -export APNS_TEAM_ID="YYYYYY" -export APNS_BUNDLE_ID="com.yourcompany.shieldai" -export APNS_KEY="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n" -``` - -### 4. Configure in Xcode - -In your iOS app's `Info.plist`: - -```xml -UIBackgroundModes - - fetch - remote-notification - -``` - -Enable capabilities in Xcode: -- **Push Notifications** -- **Background Modes** → **Remote notifications** - -## API Endpoints - -### Device Registration - -#### Register Device -```http -POST /api/v1/devices/register -Authorization: Bearer -Content-Type: application/json - -{ - "platform": "android", - "fcmToken": "eXwR9...", - "appVersion": "1.0.0", - "osVersion": "Android 13" -} -``` - -Response: -```json -{ - "success": true, - "device": { - "deviceId": "dev_1234567890_abc123", - "platform": "android", - "registeredAt": "2026-05-14T13:00:00.000Z" - }, - "message": "Device registered successfully" -} -``` - -#### Update Device Tokens -```http -PUT /api/v1/devices/:deviceId/tokens -Authorization: Bearer -Content-Type: application/json - -{ - "fcmToken": "new-token-here", - "apnsToken": "new-token-here" -} -``` - -#### Get Registered Devices -```http -GET /api/v1/devices -Authorization: Bearer -``` - -#### Deregister Device -```http -DELETE /api/v1/devices/:deviceId -Authorization: Bearer -``` - -## Mobile App Integration - -### Android (React Native) - -```javascript -import messaging from '@react-native-firebase/messaging'; -import { API } from '@shieldai/api-client'; - -// Request permission -async function requestPushPermission() { - const authStatus = await messaging().requestPermission(); - const enabled = - authStatus === messaging.AuthorizationStatus.AUTHORIZED || - authStatus === messaging.AuthorizationStatus.PROVISIONAL; - - if (enabled) { - console.log('Push permission granted:', authStatus); - } -} - -// Get FCM token -async function getFCMToken() { - const token = await messaging().getToken(); - return token; -} - -// Register device with API -async function registerDevice() { - try { - const fcmToken = await getFCMToken(); - - const response = await API.post('/devices/register', { - platform: 'android', - fcmToken, - appVersion: '1.0.0', - osVersion: Platform.OS + ' ' + Platform.Version, - }); - - console.log('Device registered:', response.data); - } catch (error) { - console.error('Failed to register device:', error); - } -} - -// Listen for token refresh -messaging().onTokenRefresh(async (newToken) => { - await API.put('/devices/tokens', { - fcmToken: newToken, - }); -}); - -// Handle foreground messages -messaging().onMessage(async (remoteMessage) => { - console.log('Foreground message received:', remoteMessage); - // Show local notification -}); - -// Handle background messages -messaging().setBackgroundMessageHandler(async remoteMessage => { - console.log('Background message received:', remoteMessage); -}); -``` - -### iOS (React Native) - -```javascript -import messaging from '@react-native-firebase/messaging'; -import { API } from '@shieldai/api-client'; - -// Request permission -async function requestPushPermission() { - const authStatus = await messaging().requestPermission(); - const enabled = - authStatus === messaging.AuthorizationStatus.AUTHORIZED || - authStatus === messaging.AuthorizationStatus.PROVISIONAL; - - if (enabled) { - console.log('Push permission granted:', authStatus); - } -} - -// Get APNs token -async function getAPNSToken() { - const token = await messaging().getToken(); - return token; -} - -// Register device with API -async function registerDevice() { - try { - const apnsToken = await getAPNSToken(); - - const response = await API.post('/devices/register', { - platform: 'ios', - apnsToken, - appVersion: '1.0.0', - osVersion: Platform.OS + ' ' + Platform.Version, - }); - - console.log('Device registered:', response.data); - } catch (error) { - console.error('Failed to register device:', error); - } -} - -// Handle foreground messages -messaging().onMessage(async (remoteMessage => { - console.log('Foreground message received:', remoteMessage); - // Show local notification -})); -``` - -## Testing Push Notifications - -### Test with Firebase Console - -1. Go to [Firebase Console](https://console.firebase.google.com/) → **Cloud Messaging** -2. Click **Send your first message** -3. Enter notification title and body -4. Choose test device or all users -5. Click **Send test message** - -### Test with API - -```bash -# Send test push notification -curl -X POST https://your-api.com/api/v1/notifications/send \ - -H "Authorization: Bearer YOUR_JWT_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "userId": "user-123", - "channel": "push", - "title": "Test Notification", - "body": "This is a test push notification", - "data": { - "type": "test", - "action": "open_app" - } - }' -``` - -### Test with cURL (Direct FCM) - -```bash -curl -X POST https://fcm.googleapis.com/v1/projects/your-project-id/messages:send \ - -H "Authorization: Bearer $(gcloud auth application-default print-access-token)" \ - -H "Content-Type: application/json" \ - -d '{ - "message": { - "token": "FCM_TOKEN_HERE", - "notification": { - "title": "Test Title", - "body": "Test Body" - }, - "data": { - "type": "test" - } - } - }' -``` - -## Production Checklist - -### FCM Configuration -- [ ] Firebase project created -- [ ] Service account key generated and stored securely -- [ ] FCM environment variables configured -- [ ] Firebase Admin SDK initialized -- [ ] Test notifications sent successfully - -### APNs Configuration -- [ ] App ID created with push capability -- [ ] APNs key generated and downloaded -- [ ] Key converted to PEM format -- [ ] APNs environment variables configured -- [ ] Xcode capabilities enabled -- [ ] Test notifications sent from simulator - -### API Configuration -- [ ] Device registration endpoints working -- [ ] Token update endpoints working -- [ ] Rate limiting configured -- [ ] Error handling implemented -- [ ] Logging for push notifications - -### Mobile App Configuration -- [ ] Push permissions requested -- [ ] Token retrieval implemented -- [ ] Device registration on app start -- [ ] Token refresh handling -- [ ] Foreground message handling -- [ ] Background message handling -- [ ] Notification display implemented - -## Troubleshooting - -### FCM Token Not Received -- Check Firebase configuration in mobile app -- Verify Google Services (Android) / GoogleService-Info.plist (iOS) -- Ensure push permissions granted - -### APNs Token Not Received -- Verify App ID configuration in Apple Developer -- Check APNs key configuration -- Ensure background modes enabled in Xcode -- Test on actual device (not simulator) - -### Notifications Not Delivering -- Check device registration status -- Verify tokens are valid -- Check rate limiting status -- Review server logs for errors - -### Token Refresh Issues -- Listen for `onTokenRefresh` events -- Update tokens via API immediately -- Handle network errors gracefully - -## Security Considerations - -1. **Never expose service account keys** in client-side code -2. **Always validate** device ownership on server -3. **Use HTTPS** for all API calls -4. **Implement rate limiting** to prevent abuse -5. **Store tokens securely** using secure storage libraries -6. **Deregister devices** on user logout - -## Monitoring - -Monitor the following metrics: -- Push notification delivery rate -- Token refresh frequency -- Device registration failures -- Rate limit hits -- Notification open rate - -## Support - -- Firebase Support: https://firebase.google.com/support -- Apple Developer Support: https://developer.apple.com/contact/ -- FCM Documentation: https://firebase.google.com/docs/cloud-messaging -- APNs Documentation: https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server diff --git a/docs/STRIPE_INTEGRATION.md b/docs/STRIPE_INTEGRATION.md deleted file mode 100644 index 9a89009..0000000 --- a/docs/STRIPE_INTEGRATION.md +++ /dev/null @@ -1,353 +0,0 @@ -# Stripe Integration Guide - -## Overview - -This guide covers the Stripe live API integration for ShieldAI subscription management. The implementation includes webhook handlers, subscription management endpoints, and tier-based feature gating. - -## Environment Variables - -Add the following to your `.env` file: - -```bash -# Stripe Configuration -STRIPE_API_KEY=sk_live_... -STRIPE_WEBHOOK_SECRET=whsec_... -STRIPE_FREE_TIER_PRICE_ID=price_... -STRIPE_BASIC_TIER_PRICE_ID=price_... -STRIPE_PLUS_TIER_PRICE_ID=price_... -STRIPE_PREMIUM_TIER_PRICE_ID=price_... -``` - -## Getting Stripe Live API Keys - -### 1. Get Live API Key - -1. Go to [Stripe Dashboard](https://dashboard.stripe.com/) -2. Navigate to **Developers** → **API keys** -3. Toggle **Live mode** (top right corner) -4. Copy the **Secret key** (starts with `sk_live_`) - -### 2. Get Webhook Signing Secret - -1. In Stripe Dashboard, go to **Developers** → **Webhooks** -2. Click **Add endpoint** -3. Add your webhook URL: `https://your-api-domain.com/api/v1/billing/webhooks/stripe` -4. Select these events to listen for: - - `customer.subscription.created` - - `customer.subscription.updated` - - `customer.subscription.deleted` - - `invoice.payment_succeeded` - - `invoice.payment_failed` -5. Click **Add endpoint** -6. Copy the **Signing secret** (starts with `whsec_`) - -### 3. Create Price IDs for Subscription Tiers - -1. Go to **Products** in Stripe Dashboard -2. Create products for each tier: - - Free Tier ( $0/month) - - Basic Tier ($9.99/month) - - Plus Tier ($19.99/month) - - Premium Tier ($49.99/month) -3. For each product, create a recurring price -4. Copy the Price IDs (start with `price_`) - -## API Endpoints - -### Subscription Management - -#### Get Current Subscription -```http -GET /api/v1/billing/subscription -Authorization: Bearer -``` - -Response: -```json -{ - "subscription": { - "id": "sub_123", - "status": "active", - "currentPeriodStart": "2026-05-01T00:00:00.000Z", - "currentPeriodEnd": "2026-06-01T00:00:00.000Z", - "cancelAtPeriodEnd": false - }, - "customer": { - "id": "cus_123" - } -} -``` - -#### Create Subscription -```http -POST /api/v1/billing/subscription/create -Authorization: Bearer -Content-Type: application/json - -{ - "tier": "basic", - "customerId": "cus_123" -} -``` - -#### Update Subscription Tier -```http -PUT /api/v1/billing/subscription/:subscriptionId/tier -Authorization: Bearer -Content-Type: application/json - -{ - "tier": "plus" -} -``` - -#### Cancel Subscription -```http -DELETE /api/v1/billing/subscription/:subscriptionId -Authorization: Bearer -Content-Type: application/json - -{ - "cancelAtPeriodEnd": true -} -``` - -#### Get User Tier -```http -GET /api/v1/billing/user/tier -Authorization: Bearer -``` - -Response: -```json -{ - "tier": "basic", - "limits": { - "callMinutesLimit": 500, - "smsCountLimit": 2000, - "darkWebScans": 12 - } -} -``` - -#### Create Customer Portal Session -```http -POST /api/v1/billing/customer/portal -Authorization: Bearer -Content-Type: application/json - -{ - "customerId": "cus_123", - "returnUrl": "https://yourapp.com/billing" -} -``` - -Response: -```json -{ - "url": "https://billing.stripe.com/p/login/...", - "expiresAt": "2026-05-14T14:00:00.000Z" -} -``` - -#### Get Invoice History -```http -GET /api/v1/billing/invoices?customerId=cus_123 -Authorization: Bearer -``` - -### Webhook Handler - -#### Stripe Webhook -```http -POST /api/v1/billing/webhooks/stripe -Stripe-Signature: -Content-Type: application/json - - -``` - -## Webhook Events Handled - -### customer.subscription.created -Triggered when a new subscription is created. - -### customer.subscription.updated -Triggered when subscription details change (tier upgrade/downgrade, payment method update). - -### customer.subscription.deleted -Triggered when a subscription is cancelled. - -### invoice.payment_succeeded -Triggered when a payment is successfully processed. - -### invoice.payment_failed -Triggered when a payment fails. - -## Testing - -### Test with Stripe CLI - -1. Install [Stripe CLI](https://stripe.com/docs/stripe-cli) -2. Login: `stripe login` -3. Forward webhooks: `stripe listen --forward-to localhost:3000/api/v1/billing/webhooks/stripe` - -### Test Events - -```bash -# Trigger a subscription created event -stripe trigger customer.subscription.created - -# Trigger a payment succeeded event -stripe trigger invoice.payment_succeeded -``` - -## Mobile App Integration - -### React Native Example - -```javascript -import { API } from '@shieldai/api-client'; - -// Get current subscription -const getSubscription = async () => { - try { - const response = await API.get('/billing/subscription'); - return response.data; - } catch (error) { - console.error('Failed to fetch subscription:', error); - } -}; - -// Create subscription -const createSubscription = async (tier, customerId) => { - try { - const response = await API.post('/billing/subscription/create', { - tier, - customerId, - }); - return response.data; - } catch (error) { - console.error('Failed to create subscription:', error); - } -}; - -// Upgrade subscription -const upgradeSubscription = async (subscriptionId, newTier) => { - try { - const response = await API.put( - `/billing/subscription/${subscriptionId}/tier`, - { tier: newTier } - ); - return response.data; - } catch (error) { - console.error('Failed to upgrade subscription:', error); - } -}; - -// Cancel subscription -const cancelSubscription = async (subscriptionId) => { - try { - const response = await API.delete( - `/billing/subscription/${subscriptionId}`, - { data: { cancelAtPeriodEnd: true } } - ); - return response.data; - } catch (error) { - console.error('Failed to cancel subscription:', error); - } -}; -``` - -## Feature Gating - -Use the middleware to protect routes based on subscription tier: - -```typescript -import { requireTier } from '@shieldai/shared-billing'; -import { SubscriptionTier } from '@shieldai/shared-billing'; - -// Require minimum tier -fastify.get( - '/premium-feature', - { - preHandler: requireTier([SubscriptionTier.BASIC, SubscriptionTier.PLUS, SubscriptionTier.PREMIUM]) - }, - async (request, reply) => { - // Only accessible to BASIC tier and above - } -); - -// Require specific tier -fastify.get( - '/exclusive-feature', - { - preHandler: requireTier([SubscriptionTier.PREMIUM]) - }, - async (request, reply) => { - // Only accessible to PREMIUM tier - } -); -``` - -## Deployment Checklist - -- [ ] Set `STRIPE_API_KEY` to live key (not test key) -- [ ] Set `STRIPE_WEBHOOK_SECRET` to live webhook secret -- [ ] Configure webhook endpoint in Stripe Dashboard -- [ ] Verify webhook events are being received -- [ ] Test subscription creation flow -- [ ] Test tier upgrade/downgrade flow -- [ ] Test cancellation flow -- [ ] Verify feature gating works correctly -- [ ] Monitor Stripe dashboard for errors -- [ ] Set up alerts for failed payments - -## Production Considerations - -### Security - -1. **Never expose secret keys** in client-side code -2. **Always verify webhook signatures** on the server -3. **Use HTTPS** for all API endpoints in production -4. **Implement rate limiting** on webhook endpoints - -### Error Handling - -1. **Idempotency**: Webhook events may be delivered multiple times -2. **Retry logic**: Stripe will retry failed webhook deliveries -3. **Logging**: Log all webhook events for debugging -4. **Alerts**: Set up alerts for payment failures - -### Compliance - -1. **PCI DSS**: Use Stripe Elements for payment collection -2. **GDPR**: Handle customer data according to regulations -3. **Tax**: Consider tax calculation for different regions - -## Troubleshooting - -### Webhook Signature Verification Fails - -- Ensure `STRIPE_WEBHOOK_SECRET` is correctly set -- Verify the webhook URL matches what's configured in Stripe -- Check that raw body is being captured (not parsed JSON) - -### Subscription Creation Fails - -- Verify `STRIPE_API_KEY` is valid -- Check that price IDs exist and are active -- Ensure customer ID is valid - -### Tier Not Updating - -- Verify the new tier's price ID exists -- Check for active subscriptions on the customer -- Review Stripe dashboard for error messages - -## Support - -For issues or questions: -- Stripe Dashboard: https://dashboard.stripe.com/ -- Stripe Docs: https://stripe.com/docs -- Stripe Support: https://support.stripe.com/ diff --git a/drizzle.config.ts b/drizzle.config.ts deleted file mode 100644 index bdc51d4..0000000 --- a/drizzle.config.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -export default defineConfig({ - schema: "./src/db/schema/index.ts", - out: "./src/db/migrations", - dialect: "turso", - dbCredentials: { - url: process.env.TURSO_DATABASE_URL!, - authToken: process.env.TURSO_AUTH_TOKEN!, - }, -}); diff --git a/index.html b/index.html deleted file mode 100644 index 4b95548..0000000 --- a/index.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - Scripter — Write Faster - - -
- - - diff --git a/infra/.gitignore b/infra/.gitignore deleted file mode 100644 index f601f47..0000000 --- a/infra/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -.terraform/ -*.tfstate -*.tfstate.backup -*.tfvars -.terraform.lock.hcl -override.tf -override.tf.json -*_override.tf -*_override.tf.json diff --git a/infra/README.md b/infra/README.md deleted file mode 100644 index 0d57b17..0000000 --- a/infra/README.md +++ /dev/null @@ -1,113 +0,0 @@ -/infra/ -├── main.tf # Root module: VPC, ECS, RDS, ElastiCache, S3, Secrets, CloudWatch -├── variables.tf # Input variables with validation -├── outputs.tf # Output values (endpoints, ARNs, URLs) -├── modules/ -│ ├── vpc/main.tf # VPC, subnets, IGW, NAT GW, security groups -│ ├── ecs/main.tf # ECS cluster, task definitions, services, ALB, auto-scaling -│ ├── rds/main.tf # RDS PostgreSQL with automated backups -│ ├── elasticache/main.tf # ElastiCache Redis with replication -│ ├── s3/main.tf # S3 buckets: state, artifacts, logs -│ ├── secrets/main.tf # AWS Secrets Manager -│ └── cloudwatch/main.tf # Dashboards, alarms, notifications -├── environments/ -│ ├── staging/main.tf # Staging environment config -│ └── production/main.tf # Production environment config -└── scripts/ - ├── rollback.sh # ECS service rollback (AWS) - ├── rollback-compose.sh # Docker Compose rollback (local/staging) - └── rollback-migration.sh # Database migration rollback - -## Quick Start - -### Prerequisites -- Terraform >= 1.5.0 -- AWS CLI configured with appropriate credentials -- AWS account with ECS, RDS, ElastiCache permissions - -### Initialize -```bash -cd infra/environments/staging -terraform init -terraform plan -var-file=terraform.tfvars.example -terraform apply -var-file=terraform.tfvars.example -``` - -### Deploy via CI/CD -- Push to `main` → deploys to staging -- Create a release → deploys to production -- Health check failure → automatic rollback - -## Architecture - -### Networking -- VPC with public/private subnets across multiple AZs -- NAT Gateway for outbound traffic from private subnets -- Security groups: ECS → RDS (5432), ECS → ElastiCache (6379) - -### Compute -- ECS Fargate for serverless container orchestration -- Application Load Balancer with health checks -- Auto-scaling: CPU-based scaling (70% target) -- Production: 3 replicas per service, min 2, max 10 - -### Data -- RDS PostgreSQL 16.2 with Multi-AZ (production) -- Automated daily backups, 7-14 day retention -- ElastiCache Redis 7.0 with replication -- S3 with versioning and lifecycle policies - -### Secrets -- AWS Secrets Manager for all credentials -- ECS task execution role with SecretsManagerReadOnly -- DB credentials auto-rotated via RDS integration - -### Monitoring -- CloudWatch dashboards: CPU, memory, ALB metrics -- Alarms: CPU >80%, memory >85%, 5xx >10/min, RDS storage <500MB -- Container Insights enabled for ECS -- Logs: 30-day retention (production), 7-day (staging) - -### Backup Strategy -- RDS: automated snapshots every 24h, 7-14 day retention -- RDS: Multi-AZ for automatic failover (production) -- ElastiCache: daily snapshots, 1-7 day retention -- S3: versioning enabled, non-current versions expire after 30 days -- Terraform state: S3 with versioning + DynamoDB locking - -## Rollback - -See **[ROLLBACK.md](./ROLLBACK.md)** for the complete rollback runbook, including: - -- ECS service rollback (automated + manual) -- Docker Compose rollback (local / staging) -- Database migration rollback (Drizzle) -- Blue-green deployment rollback -- RDS point-in-time recovery -- Automated rollback triggers and health checks -- Emergency rollback runbook -- Testing checklist - -### Quick Reference - -```bash -# ECS service rollback (AWS) -./infra/scripts/rollback.sh [--verify] - -# Docker Compose rollback (local/staging) -./infra/scripts/rollback-compose.sh - -# Database migration rollback -./infra/scripts/rollback-migration.sh [--migration ] -``` - -## GitHub Secrets Required -| Secret | Description | -|--------|-------------| -| AWS_ACCESS_KEY_ID | IAM user with ECS, RDS, ElastiCache permissions | -| AWS_SECRET_ACCESS_KEY | IAM secret key | -| HIBP_API_KEY | Have I Been Pwned API key | -| RESEND_API_KEY | Resend email API key | -| SENTRY_DSN | Sentry error tracking DSN | -| DATADOG_API_KEY | Datadog monitoring API key | -| GITHUB_TOKEN | Auto-provided, needs write:packages scope | diff --git a/infra/ROLLBACK.md b/infra/ROLLBACK.md deleted file mode 100644 index 7a81f75..0000000 --- a/infra/ROLLBACK.md +++ /dev/null @@ -1,611 +0,0 @@ -# ShieldAI Rollback Runbook - -> **Last updated:** 2026-05-12 -> **Owner:** Senior Engineer -> **Parent:** [FRE-4574](/FRE/issues/FRE-4574) ShieldAI Production Infrastructure & CI/CD Pipeline -> **Reviewed by:** Code Reviewer (FRE-4808) on 2026-05-12 - ---- - -## Table of Contents - -1. [Overview](#1-overview) -2. [Rollback Strategies](#2-rollback-strategies) -3. [ECS Service Rollback (AWS)](#3-ecs-service-rollback-aws) -4. [Docker Compose Rollback (Local / Staging)](#4-docker-compose-rollback-local--staging) -5. [Database Migration Rollback](#5-database-migration-rollback) -6. [Automated Rollback Triggers](#6-automated-rollback-triggers) -7. [Blue-Green Deployment Rollback](#7-blue-green-deployment-rollback) -8. [Rollback Decision Tree](#8-rollback-decision-tree) -9. [Post-Rollback Verification](#9-post-rollback-verification) -10. [Testing Checklist](#10-testing-checklist) -11. [Runbook: Emergency Rollback](#11-runbook-emergency-rollback) - ---- - -## 1. Overview - -ShieldAI runs four services (api, darkwatch, spamshield, voiceprint) on AWS ECS Fargate behind an Application Load Balancer. Each service has independent deployment, health checks, and rollback capability. - -**Rollback types:** - -| Type | Trigger | Scope | Automation | -|------|---------|-------|------------| -| **ECS Service Rollback** | Health check failure, manual | Single or all services | ✅ CI/CD + manual script | -| **Docker Compose Rollback** | Manual (local/staging) | All services | ✅ Scripted | -| **Database Migration Rollback** | Manual | Schema changes | ⚠️ Semi-manual | -| **Blue-Green Rollback** | Manual or automated | Full environment | ✅ CI/CD | -| **RDS Point-in-Time Restore** | Manual (disaster) | Full database | ⚠️ Semi-manual | - ---- - -## 2. Rollback Strategies - -### 2.1 ECS Service-Level Rollback - -Each ECS service maintains a history of task definitions. Rolling back reverts to the **previous successfully deployed task definition**. - -**Prerequisites:** -- AWS CLI configured with credentials for the target environment -- IAM permissions: `ecs:UpdateService`, `ecs:DescribeServices`, `ecs:WaitServicesStable` - -### 2.2 Blue-Green Rollback - -The CI/CD pipeline deploys new images to existing ECS services. If health checks fail after deployment, the `rollback` job in the deploy workflow automatically reverts all four services to their previous task definition revision. - -**Pipeline flow:** -``` -build-and-push → deploy-ecs → health-check → [PASS: done | FAIL: rollback] -``` - -### 2.3 Database Migration Rollback - -ShieldAI uses Drizzle ORM for database migrations. Each migration is versioned and stored in `src/db/migrations/`. Rollback requires running the previous migration set. - ---- - -## 3. ECS Service Rollback (AWS) - -### 3.1 Automated (CI/CD Pipeline) - -The deploy workflow (`.github/workflows/deploy.yml`) includes a `rollback` job that triggers on health check failure: - -```yaml -rollback: - if: failure() && needs.health-check.result == 'failure' - # Rolls back all 4 services to previous task definition -``` - -**When it runs:** -- Post-deploy health check fails (HTTP 200 not received from `/health`) -- Runs after `deploy-ecs` and `health-check` jobs -- Rolls back all four services: api, darkwatch, spamshield, voiceprint - -**How to verify:** -1. Navigate to the GitHub Actions run for the failed deployment -2. Check the `Rollback on Failure` job logs -3. Confirm each service shows "Rolled back" status - -### 3.2 Manual Rollback Script - -```bash -# Single service -./infra/scripts/rollback.sh production api - -# All services -./infra/scripts/rollback.sh production all - -# Staging environment -./infra/scripts/rollback.sh staging all -``` - -**Script behavior:** -1. Iterates over target services (or all if `all` specified) -2. Calls `aws ecs update-service --rollback` for each service -3. Waits for service to stabilize via `aws ecs wait services-stable` -4. Reports success/failure per service -5. Exits with non-zero code if any service fails to stabilize - -**Expected output:** -``` -Rolling back services in cluster: shieldai-production -Rolling back api... -Waiting for api to stabilize... -api rolled back successfully -Rolling back darkwatch... -Waiting for darkwatch to stabilize... -darkwatch rolled back successfully -... -Rollback complete for api darkwatch spamshield voiceprint -``` - -### 3.3 Manual CLI Rollback (Fallback) - -If the script is unavailable, rollback individual services: - -```bash -CLUSTER="shieldai-production" -SERVICE="api" - -# Rollback to previous task definition -aws ecs update-service \ - --cluster "$CLUSTER" \ - --service "${CLUSTER}-${SERVICE}" \ - --rollback \ - --no-cli-auto-prompt - -# Wait for stabilization -aws ecs wait services-stable \ - --cluster "$CLUSTER" \ - --services "${CLUSTER}-${SERVICE}" - -# Verify health -curl -s -o /dev/null -w "%{http_code}" \ - "https://shieldai-production-alb.us-east-1.elb.amazonaws.com/health" -``` - ---- - -## 4. Docker Compose Rollback (Local / Staging) - -### 4.1 Production Compose Rollback - -The `docker-compose.prod.yml` deploys all services with tagged images. To rollback: - -```bash -# 1. Identify the previous working tag -# Check GitHub releases or git tags for the last known good version -PREVIOUS_TAG="v1.2.3" - -# 2. Stop current services -docker compose -f docker-compose.prod.yml down - -# 3. Pull previous images -docker pull ghcr.io/${GITHUB_REPOSITORY_OWNER}/shieldai-api:${PREVIOUS_TAG} -docker pull ghcr.io/${GITHUB_REPOSITORY_OWNER}/shieldai-darkwatch:${PREVIOUS_TAG} -docker pull ghcr.io/${GITHUB_REPOSITORY_OWNER}/shieldai-spamshield:${PREVIOUS_TAG} -docker pull ghcr.io/${GITHUB_REPOSITORY_OWNER}/shieldai-voiceprint:${PREVIOUS_TAG} - -# 4. Override tag in compose -DOCKER_TAG=${PREVIOUS_TAG} docker compose -f docker-compose.prod.yml up -d - -# 5. Verify health -for svc in api darkwatch spamshield voiceprint; do - PORT=$(case $svc in - api) echo 3000;; darkwatch) echo 3001;; - spamshield) echo 3002;; voiceprint) echo 3003;; - esac) - curl -sf "http://localhost:${PORT}/health" && echo "$svc: OK" || echo "$svc: FAIL" -done -``` - -### 4.2 Local Dev Rollback - -```bash -# Stop and remove containers -docker compose down - -# Rebuild from previous commit -git checkout -docker compose up -d --build -``` - ---- - -## 5. Database Migration Rollback - -### 5.1 Drizzle Migration Rollback - -ShieldAI uses Drizzle ORM with Turso dialect. Migrations are stored in `src/db/migrations/`. - -```bash -# 1. Get database credentials from AWS Secrets Manager -DB_SECRET=$(aws secretsmanager get-secret-value \ - --secret-id "shieldai-${ENVIRONMENT}-db-password" \ - --query 'SecretString' --output json) - -DB_HOST=$(echo "$DB_SECRET" | jq -r '.host') -DB_PORT=$(echo "$DB_SECRET" | jq -r '.port') -DB_USER=$(echo "$DB_SECRET" | jq -r '.username') -DB_PASS=$(echo "$DB_SECRET" | jq -r '.password') - -DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/shieldai" - -# 2. List migrations to identify the one to revert -npx drizzle-kit introspect --config=drizzle.config.ts - -# 3. Resolve the problematic migration (marks it as not applied) -npx drizzle-kit migrate:resolve --migration "" --status applied - -# 4. Re-run previous migration state -npx drizzle-kit migrate --config=drizzle.config.ts -``` - -### 5.2 RDS Point-in-Time Recovery (Disaster) - -When the database itself needs recovery (e.g., data corruption, bad migration): - -```bash -# 1. Find available recovery window (automated backups: every 24h, 7-14 day retention) -aws rds describe-db-instances \ - --db-instance-identifier "shieldai-production-db" \ - --query 'DBInstances[0].LatestRestorableTime' - -# 2. Create restored instance (does not affect primary) -aws rds restore-db-instance-to-point-in-time \ - --source-db-instance-identifier "shieldai-production-db" \ - --db-instance-identifier "shieldai-production-db-restored" \ - --restore-time "2026-05-09T08:00:00Z" - -# 3. Verify restored instance -aws rds wait db-instance-available \ - --db-instance-identifier "shieldai-production-db-restored" - -# 4. Update ECS services to point to restored instance -# Update DATABASE_URL secret in Secrets Manager -aws secretsmanager put-secret-value \ - --secret-id "shieldai-production-db-password" \ - --secret-string "$(echo "$DB_SECRET" | jq --arg host "$(aws rds describe-db-instances --db-instance-identifier shieldai-production-db-restored --query 'DBInstances[0].Endpoint.Address' --output text)" '.host = $host')" - -# 5. Trigger ECS service redeployment to pick up new DB endpoint -./infra/scripts/rollback.sh production all -``` - -### 5.3 RDS Snapshot Restore - -```bash -# 1. List available snapshots -aws rds describe-db-snapshots \ - --db-instance-identifier "shieldai-production-db" - -# 2. Restore from specific snapshot -aws rds restore-db-instance-from-db-snapshot \ - --db-instance-identifier "shieldai-production-db-restored" \ - --db-snapshot-identifier "rds:shieldai-production-db-2026-05-08-03-00" \ - --db-instance-class "db.t3.medium" \ - --vpc-security-group-ids "$(terraform -chdir=infra/output -raw vpc_security_group_id)" - -# 3. Follow steps 3-5 from Point-in-Time Recovery above -``` - ---- - -## 6. Automated Rollback Triggers - -### 6.1 CI/CD Health Check Failure - -**Trigger:** Post-deploy health check returns non-200 from `/health` - -**Pipeline job:** `rollback` in `.github/workflows/deploy.yml` - -**Condition:** `if: failure() && needs.health-check.result == 'failure'` - -**Action:** Rolls back all four ECS services to previous task definition - -**Timeout:** Health check retries for 5 minutes before triggering rollback - -### 6.2 ECS Container Health Check - -Each container has an in-container health check defined in the ECS task definition: - -```json -"healthCheck": { - "command": ["CMD-SHELL", "wget -q --spider http://localhost:{port}/health || exit 1"], - "interval": 30, - "timeout": 5, - "retries": 3, - "startPeriod": 60 -} -``` - -**Failure consequence:** Container is marked unhealthy after 3 consecutive failures (90 seconds). ALB marks target as unhealthy after 3 failed health checks (90 seconds). Service enters draining state. - -### 6.3 ALB Target Group Health Check - -The ALB performs HTTP health checks against `/health` on each target: - -| Parameter | Value | -|-----------|-------| -| Interval | 30s | -| Timeout | 5s | -| Healthy threshold | 3 | -| Unhealthy threshold | 3 | -| Expected code | 200 | - -### 6.4 CloudWatch Alarms - -The following alarms are configured in `infra/modules/cloudwatch/main.tf`: - -| Alarm | Threshold | Action | -|-------|-----------|--------| -| ECS CPU >80% | 80% for 2 periods (10min) | SNS notification | -| ECS Memory >85% | 85% for 2 periods (10min) | SNS notification | -| ALB 5xx >10/min | 10 for 3 periods (3min) | SNS notification | -| RDS CPU >75% | 75% for 2 periods (10min) | SNS notification | -| RDS Free Storage <500MB | 500MB for 2 periods (10min) | SNS notification | - -**Alarm escalation path:** -1. CloudWatch alarm fires -2. SNS notification sent to on-call engineer -3. Engineer evaluates: if service is degraded, trigger manual rollback -4. If root cause is deployment-related, run `./infra/scripts/rollback.sh production all` - ---- - -## 7. Blue-Green Deployment Rollback - -### 7.1 Architecture - -ShieldAI uses ECS services with rolling deployments. Each deployment creates a new task definition revision. The ALB routes traffic to healthy targets only. - -**Rollback mechanism:** ECS `--rollback` flag reverts the service to the previous task definition revision. This is equivalent to a blue-green swap since: - -1. Old task definition (blue) remains registered -2. New task definition (green) is deployed -3. On rollback, ECS reverts to blue task definition -4. ALB automatically routes to healthy (blue) targets - -### 7.2 Blue-Green Rollback Procedure - -```bash -# 1. Check current deployment state -aws ecs list-services --cluster shieldai-production -aws ecs describe-services --cluster shieldai-production \ - --services shieldai-production-api \ - --query 'services[0].deployments' - -# 2. Identify previous deployment -# The deployment with status "PRIMARY" is current. -# Look for "ACTIVE" deployment with older task definition. - -# 3. Execute rollback (script handles all services) -./infra/scripts/rollback.sh production all - -# 4. Verify rollback -aws ecs describe-services --cluster shieldai-production \ - --services shieldai-production-api \ - --query 'services[0].deployments[?status==`PRIMARY`].taskDefinition' -``` - -### 7.3 Docker Compose Blue-Green (Local) - -For local/staging environments using Docker Compose, implement blue-green via service version pinning: - -```bash -# Current deployment uses DOCKER_TAG env var -# Rollback by setting DOCKER_TAG to previous version - -# Save current tag -CURRENT_TAG=$(grep DOCKER_TAG .env.prod 2>/dev/null | cut -d= -f2 || echo "latest") - -# Rollback to previous -export DOCKER_TAG="v1.2.3" -docker compose -f docker-compose.prod.yml up -d - -# Verify all services -docker compose -f docker-compose.prod.yml ps -``` - ---- - -## 8. Rollback Decision Tree - -``` -Is the service responding? -├── YES → Is the response correct? -│ ├── YES → Monitor, no action needed -│ └── NO → Is it a data issue? -│ ├── YES → Database Migration Rollback (§5) -│ └── NO → ECS Service Rollback (§3) -└── NO → Is it a single service or all? - ├── Single → ECS Service Rollback (§3, specific service) - └── All → Full Environment Rollback - ├── Is DB corrupted? - │ ├── YES → RDS Point-in-Time Recovery (§5.2) - │ └── NO → ECS Full Rollback + DB Migration Rollback -``` - -**SLA targets:** -- Single service rollback: **< 5 minutes** -- Full environment rollback: **< 15 minutes** -- Database recovery: **< 30 minutes** (Point-in-Time) - ---- - -## 9. Post-Rollback Verification - -After any rollback, verify the following: - -### 9.1 Service Health - -```bash -# Check all services are healthy -for svc in api darkwatch spamshield voiceprint; do - PORT=$(case $svc in - api) echo 3000;; darkwatch) echo 3001;; - spamshield) echo 3002;; voiceprint) echo 3003;; - esac) - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \ - "https://shieldai-${ENVIRONMENT}-alb.us-east-1.elb.amazonaws.com/health") - echo "$svc: HTTP $HTTP_CODE" -done -``` - -### 9.2 ECS Service Status - -```bash -# Verify all services are stable -for svc in api darkwatch spamshield voiceprint; do - RUNNING=$(aws ecs describe-services \ - --cluster "shieldai-${ENVIRONMENT}" \ - --services "shieldai-${ENVIRONMENT}-${svc}" \ - --query 'services[0].runningCount' --output text) - DESIRED=$(aws ecs describe-services \ - --cluster "shieldai-${ENVIRONMENT}" \ - --services "shieldai-${ENVIRONMENT}-${svc}" \ - --query 'services[0].desiredCount' --output text) - echo "$svc: $RUNNING/$DESIRED running" -done -``` - -### 9.3 Database Connectivity - -```bash -# Verify database connection -aws ecs execute-command \ - --cluster "shieldai-${ENVIRONMENT}" \ - --service "shieldai-${ENVIRONMENT}-api" \ - --command "npx drizzle-kit status" \ - --interactive --cluster "shieldai-${ENVIRONMENT}" -``` - -### 9.4 CloudWatch Verification - -1. Navigate to CloudWatch dashboard: `shieldai-${ENVIRONMENT}-dashboard` -2. Verify CPU/Memory utilization is within normal range -3. Verify ALB 5xx errors have returned to baseline -4. Verify no new alarms are in ALARM state - ---- - -## 10. Testing Checklist - -### 10.1 ECS Rollback Test - -- [ ] Deploy a known-bad image (e.g., image with `/health` returning 500) -- [ ] Verify CI/CD health check fails within 5 minutes -- [ ] Verify `rollback` job triggers automatically -- [ ] Verify all four services revert to previous task definition -- [ ] Verify health check passes post-rollback -- [ ] Verify CloudWatch metrics show recovery - -### 10.2 Manual Script Test - -- [ ] Run `./infra/scripts/rollback.sh staging api` on staging -- [ ] Verify single service rolls back correctly -- [ ] Run `./infra/scripts/rollback.sh staging all` on staging -- [ ] Verify all services roll back correctly -- [ ] Verify script exits with code 0 on success -- [ ] Verify script exits with code 1 on failure - -### 10.3 Docker Compose Rollback Test - -- [ ] Deploy v2.0.0 of all services via docker-compose.prod.yml -- [ ] Rollback to v1.0.0 using DOCKER_TAG override -- [ ] Verify all services restart with previous images -- [ ] Verify health endpoints respond correctly - -### 10.4 Database Migration Rollback Test - -- [ ] Apply a test migration on staging -- [ ] Run migration rollback procedure -- [ ] Verify schema matches pre-migration state -- [ ] Verify application connects and functions correctly - -### 10.5 RDS Point-in-Time Recovery Test - -- [ ] Create a test RDS instance -- [ ] Insert test data -- [ ] Restore to point before data insertion -- [ ] Verify restored instance has correct data state -- [ ] Clean up test instance - -### 10.6 End-to-End Rollback Drills - -| Drill | Frequency | Participants | -|-------|-----------|--------------| -| ECS service rollback | Monthly | Senior Engineer | -| Full environment rollback | Quarterly | Full engineering team | -| Database recovery | Quarterly | Senior Engineer + Founding Engineer | -| Blue-green rollback | Quarterly | Full engineering team | - ---- - -## 11. Runbook: Emergency Rollback - -### 11.1 Symptoms - -- ALB 5xx error rate > 10/minute for 3+ minutes -- CloudWatch alarm: `shieldai-production-alb-5xx` in ALARM state -- Customer-reported service degradation - -### 11.2 Immediate Actions (0-5 minutes) - -```bash -# 1. Confirm environment and scope -ENVIRONMENT="production" - -# 2. Check service status -aws ecs describe-services \ - --cluster "shieldai-${ENVIRONMENT}" \ - --services shieldai-${ENVIRONMENT}-api,shieldai-${ENVIRONMENT}-darkwatch,shieldai-${ENVIRONMENT}-spamshield,shieldai-${ENVIRONMENT}-voiceprint \ - --query 'services[*].{Name:serviceName,Running:runningCount,Desired:desiredCount,Status:status}' - -# 3. Check ALB health -curl -s -o /dev/null -w "%{http_code}" \ - "https://shieldai-${ENVIRONMENT}-alb.us-east-1.elb.amazonaws.com/health" - -# 4. Execute rollback -./infra/scripts/rollback.sh ${ENVIRONMENT} all -``` - -### 11.3 Verification (5-10 minutes) - -```bash -# 1. Wait for services to stabilize -aws ecs wait services-stable \ - --cluster "shieldai-${ENVIRONMENT}" \ - --services shieldai-${ENVIRONMENT}-api,shieldai-${ENVIRONMENT}-darkwatch,shieldai-${ENVIRONMENT}-spamshield,shieldai-${ENVIRONMENT}-voiceprint - -# 2. Verify health endpoint -curl -sf "https://shieldai-${ENVIRONMENT}-alb.us-east-1.elb.amazonaws.com/health" \ - && echo "Health: OK" || echo "Health: FAIL" - -# 3. Check CloudWatch for recovery -# Navigate to CloudWatch dashboard and verify metrics -``` - -### 11.4 Communication Template - -``` -## Rollback Notification - -**Environment:** production -**Time:** $(date -u '+%Y-%m-%d %H:%M UTC') -**Trigger:** [ALB 5xx alarm / manual / CI/CD health check] -**Action:** Rolled back all services to previous deployment -**Status:** [In Progress / Verified / Resolved] -**Next steps:** [Post-mortem / monitoring / investigation] -``` - -### 11.5 Post-Incident - -1. Create incident ticket with timeline -2. Document root cause -3. Update runbook if procedure changed -4. Schedule post-mortem within 48 hours -5. Create follow-up issues for preventive measures - ---- - -## Appendix A: Quick Reference - -| Resource | Command | -|----------|---------| -| Rollback script | `./infra/scripts/rollback.sh ` | -| ECS service status | `aws ecs describe-services --cluster shieldai- --services shieldai--` | -| ALB health check | `curl -s -o /dev/null -w "%{http_code}" https://shieldai--alb.us-east-1.elb.amazonaws.com/health` | -| RDS snapshots | `aws rds describe-db-snapshots --db-instance-identifier shieldai--db` | -| CloudWatch dashboard | `https://us-east-1.console.aws.amazon.com/cloudwatch/home#dashboards/dashboard/shieldai--dashboard` | -| ECS task logs | `aws logs filter-log-events --log-group-name /ecs/shieldai--` | - -## Appendix B: Environment Variables - -| Variable | Description | Required | -|----------|-------------|----------| -| `AWS_ACCESS_KEY_ID` | IAM user with ECS, RDS permissions | Yes | -| `AWS_SECRET_ACCESS_KEY` | IAM secret key | Yes | -| `AWS_DEFAULT_REGION` | AWS region (default: us-east-1) | Yes | -| `GITHUB_REPOSITORY_OWNER` | GitHub org/user for container registry | Docker Compose only | -| `DOCKER_TAG` | Container image tag to deploy | Docker Compose only | -| `POSTGRES_PASSWORD` | Database password | Docker Compose only | diff --git a/infra/environments/production/main.tf b/infra/environments/production/main.tf deleted file mode 100644 index 6cdce9d..0000000 --- a/infra/environments/production/main.tf +++ /dev/null @@ -1,57 +0,0 @@ -terraform { - backend "s3" { - bucket = "shieldai-production-terraform-state" - key = "production/terraform.tfstate" - region = "us-east-1" - encrypt = true - dynamodb_table = "shieldai-terraform-locks" - } -} - -module "shieldai" { - source = "../.." - - environment = "production" - aws_region = "us-east-1" - project_name = "shieldai" - vpc_cidr = "10.1.0.0/16" - az_count = 3 - - db_instance_class = "db.r6g.large" - db_multi_az = true - db_backup_retention = 14 - - elasticache_node_type = "cache.r6g.large" - elasticache_num_nodes = 3 - - secrets = { - HIBP_API_KEY = var.hibp_api_key - RESEND_API_KEY = var.resend_api_key - SENTRY_DSN = var.sentry_dsn - DATADOG_API_KEY = var.datadog_api_key - } -} - -variable "hibp_api_key" { - description = "Have I Been Pwned API key" - type = string - sensitive = true -} - -variable "resend_api_key" { - description = "Resend API key" - type = string - sensitive = true -} - -variable "sentry_dsn" { - description = "Sentry DSN" - type = string - sensitive = true -} - -variable "datadog_api_key" { - description = "Datadog API key" - type = string - sensitive = true -} diff --git a/infra/environments/production/terraform.tfvars.example b/infra/environments/production/terraform.tfvars.example deleted file mode 100644 index fa09d2c..0000000 --- a/infra/environments/production/terraform.tfvars.example +++ /dev/null @@ -1,4 +0,0 @@ -hibp_api_key = "YOUR_HIBP_API_KEY" -resend_api_key = "YOUR_RESEND_API_KEY" -sentry_dsn = "YOUR_SENTRY_DSN" -datadog_api_key = "YOUR_DATADOG_API_KEY" diff --git a/infra/environments/staging/main.tf b/infra/environments/staging/main.tf deleted file mode 100644 index f297fa2..0000000 --- a/infra/environments/staging/main.tf +++ /dev/null @@ -1,57 +0,0 @@ -terraform { - backend "s3" { - bucket = "shieldai-staging-terraform-state" - key = "staging/terraform.tfstate" - region = "us-east-1" - encrypt = true - dynamodb_table = "shieldai-terraform-locks" - } -} - -module "shieldai" { - source = "../.." - - environment = "staging" - aws_region = "us-east-1" - project_name = "shieldai" - vpc_cidr = "10.0.0.0/16" - az_count = 2 - - db_instance_class = "db.t3.medium" - db_multi_az = false - db_backup_retention = 3 - - elasticache_node_type = "cache.t3.small" - elasticache_num_nodes = 1 - - secrets = { - HIBP_API_KEY = var.hibp_api_key - RESEND_API_KEY = var.resend_api_key - SENTRY_DSN = var.sentry_dsn - DATADOG_API_KEY = var.datadog_api_key - } -} - -variable "hibp_api_key" { - description = "Have I Been Pwned API key" - type = string - sensitive = true -} - -variable "resend_api_key" { - description = "Resend API key" - type = string - sensitive = true -} - -variable "sentry_dsn" { - description = "Sentry DSN" - type = string - sensitive = true -} - -variable "datadog_api_key" { - description = "Datadog API key" - type = string - sensitive = true -} diff --git a/infra/environments/staging/terraform.tfvars.example b/infra/environments/staging/terraform.tfvars.example deleted file mode 100644 index fa09d2c..0000000 --- a/infra/environments/staging/terraform.tfvars.example +++ /dev/null @@ -1,4 +0,0 @@ -hibp_api_key = "YOUR_HIBP_API_KEY" -resend_api_key = "YOUR_RESEND_API_KEY" -sentry_dsn = "YOUR_SENTRY_DSN" -datadog_api_key = "YOUR_DATADOG_API_KEY" diff --git a/infra/load-tests/README.md b/infra/load-tests/README.md deleted file mode 100644 index 657b85f..0000000 --- a/infra/load-tests/README.md +++ /dev/null @@ -1,61 +0,0 @@ -# ShieldAI Load Tests - -k6 load testing suite for ShieldAI services. - -## Prerequisites - -- k6 v0.45+ installed -- Target services running on staging environment -- Authentication tokens for API access - -## Running Tests - -### Local Execution - -```bash -# Run against local development environment -k6 run --env BASE_URL=http://localhost:3000 --env AUTH_TOKEN=dev-token src/darkwatch.js - -# Run with results output -k6 run --out json=results.json src/darkwatch.js -``` - -### CI/CD Execution - -```bash -# Run on staging environment -k6 run --env BASE_URL=https://staging-api.freno.me --env AUTH_TOKEN=$STAGING_AUTH_TOKEN src/darkwatch.js -``` - -## Test Configuration - -Each test script includes: - -- **Stages**: Ramp-up, sustained load, ramp-down -- **Thresholds**: P99 latency and error rate limits -- **Metrics**: Custom metrics for error tracking - -### Current Thresholds - -| Service | P99 Latency | Error Rate | -|---------|-------------|------------| -| Darkwatch | < 200ms | < 1% | - -## Metrics Collection - -Run with output options: - -```bash -# JSON output for analysis -k6 run --out json=darkwatch-results.json src/darkwatch.js - -# InfluxDB for visualization -k6 run --out influxdb=http://influxdb:8086/k6 src/darkwatch.js -``` - -## Next Steps - -1. Create load test scripts for Spamshield and Voiceprint -2. Integrate with GitHub Actions CI pipeline -3. Set up metrics visualization dashboard -4. Configure alerting on threshold breaches diff --git a/infra/load-tests/src/darkwatch.js b/infra/load-tests/src/darkwatch.js deleted file mode 100644 index 6ba8cdd..0000000 --- a/infra/load-tests/src/darkwatch.js +++ /dev/null @@ -1,99 +0,0 @@ -import http from 'k6/http'; -import { check, group } from 'k6'; -import { Rate } from 'k6/metrics'; - -// Test configuration -export const options = { - stages: [ - { duration: '30s', target: 100 }, // Ramp up to 100 users - { duration: '2m', target: 500 }, // Ramp to 500 req/s - { duration: '3m', target: 500 }, // Stay at 500 req/s for 3 minutes - { duration: '30s', target: 0 }, // Ramp down to 0 - ], - thresholds: { - http_req_duration: ['p(99)<200'], // P99 latency < 200ms - errors: ['rate<0.01'], // Error rate < 1% - }, -}; - -const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'; - -export default function () { - group('Watchlist Operations', function () { - // GET /watchlist - const watchlistRes = http.get(`${BASE_URL}/watchlist`, { - headers: { 'Authorization': `Bearer ${getAuthToken()}` }, - }); - - check(watchlistRes, { - 'watchlist GET status is 200': (r) => r.status === 200, - 'watchlist GET P99 < 100ms': (r) => r.timings.duration < 100, - }); - - // POST /watchlist - const newItemRes = http.post( - `${BASE_URL}/watchlist`, - JSON.stringify({ type: 'email', value: `test${Date()}@example.com` }), - { - headers: { - 'Authorization': `Bearer ${getAuthToken()}`, - 'Content-Type': 'application/json', - }, - } - ); - - check(newItemRes, { - 'watchlist POST status is 201': (r) => r.status === 201, - 'watchlist POST P99 < 200ms': (r) => r.timings.duration < 200, - }); - - // POST /scan - const scanRes = http.post( - `${BASE_URL}/scan`, - {}, - { - headers: { 'Authorization': `Bearer ${getAuthToken()}` }, - } - ); - - check(scanRes, { - 'scan POST status is 200': (r) => r.status === 200, - 'scan POST P99 < 150ms': (r) => r.timings.duration < 150, - }); - - // GET /scan/schedule - const scheduleRes = http.get(`${BASE_URL}/scan/schedule`, { - headers: { 'Authorization': `Bearer ${getAuthToken()}` }, - }); - - check(scheduleRes, { - 'schedule GET status is 200': (r) => r.status === 200, - 'schedule GET P99 < 100ms': (r) => r.timings.duration < 100, - }); - - // GET /exposures - const exposuresRes = http.get(`${BASE_URL}/exposures`, { - headers: { 'Authorization': `Bearer ${getAuthToken()}` }, - }); - - check(exposuresRes, { - 'exposures GET status is 200': (r) => r.status === 200, - 'exposures GET P99 < 150ms': (r) => r.timings.duration < 150, - }); - - // GET /alerts - const alertsRes = http.get(`${BASE_URL}/alerts`, { - headers: { 'Authorization': `Bearer ${getAuthToken()}` }, - }); - - check(alertsRes, { - 'alerts GET status is 200': (r) => r.status === 200, - 'alerts GET P99 < 150ms': (r) => r.timings.duration < 150, - }); - }); -} - -// Helper function to get auth token (replace with actual token retrieval) -function getAuthToken() { - return __ENV.AUTH_TOKEN || 'test-token'; -} diff --git a/infra/main.tf b/infra/main.tf deleted file mode 100644 index c0b70e8..0000000 --- a/infra/main.tf +++ /dev/null @@ -1,113 +0,0 @@ -terraform { - required_version = ">= 1.5.0" - - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 5.30" - } - - } - - backend "s3" { - bucket = "shieldai-terraform-state" - key = "global/terraform.tfstate" - region = "us-east-1" - encrypt = true - dynamodb_table = "shieldai-terraform-locks" - } -} - -provider "aws" { - region = var.aws_region - - default_tags { - tags = { - Project = "ShieldAI" - ManagedBy = "terraform" - Environment = var.environment - } - } -} - -module "vpc" { - source = "./modules/vpc" - - environment = var.environment - vpc_cidr = var.vpc_cidr - az_count = var.az_count - project_name = var.project_name - kms_key_arn = module.ecs.kms_key_arn -} - -module "ecs" { - source = "./modules/ecs" - - environment = var.environment - cluster_name = "${var.project_name}-${var.environment}" - vpc_id = module.vpc.vpc_id - subnet_ids = module.vpc.private_subnet_ids - public_subnet_ids = module.vpc.public_subnet_ids - security_group_ids = [module.vpc.ecs_security_group_id] - alb_security_group_id = module.vpc.alb_security_group_id - services = var.services - container_images = var.container_images - secrets_arn = module.secrets.secrets_manager_arn - cache_cluster_arn = module.elasticache.replication_group_arn - domain_name = var.domain_name -} - -module "rds" { - source = "./modules/rds" - - environment = var.environment - vpc_id = module.vpc.vpc_id - subnet_ids = module.vpc.private_subnet_ids - security_group_id = module.vpc.rds_security_group_id - db_name = var.db_name - db_instance_class = var.db_instance_class - multi_az = var.db_multi_az - backup_retention = var.db_backup_retention - project_name = var.project_name -} - -module "elasticache" { - source = "./modules/elasticache" - - environment = var.environment - vpc_id = module.vpc.vpc_id - subnet_ids = module.vpc.private_subnet_ids - security_group_id = module.vpc.elasticache_security_group_id - node_type = var.elasticache_node_type - num_nodes = var.elasticache_num_nodes - project_name = var.project_name -} - -module "s3" { - source = "./modules/s3" - - environment = var.environment - project_name = var.project_name -} - -module "secrets" { - source = "./modules/secrets" - - environment = var.environment - project_name = var.project_name - rds_endpoint = module.rds.db_endpoint - db_password = module.rds.db_password - elasticache_endpoint = module.elasticache.cache_endpoint - redis_auth_token = module.elasticache.auth_token - secrets = var.secrets -} - -module "cloudwatch" { - source = "./modules/cloudwatch" - - environment = var.environment - cluster_name = "${var.project_name}-${var.environment}" - project_name = var.project_name - rds_identifier = module.rds.db_instance_identifier - cache_endpoint = module.elasticache.cache_endpoint -} diff --git a/infra/modules/cloudwatch/main.tf b/infra/modules/cloudwatch/main.tf deleted file mode 100644 index 0e208e7..0000000 --- a/infra/modules/cloudwatch/main.tf +++ /dev/null @@ -1,464 +0,0 @@ -variable "environment" { - description = "Deployment environment" - type = string -} - -variable "cluster_name" { - description = "ECS cluster name" - type = string -} - -variable "project_name" { - description = "Project name" - type = string -} - -variable "rds_identifier" { - description = "RDS instance identifier" - type = string -} - -variable "cache_endpoint" { - description = "ElastiCache endpoint" - type = string -} - -variable "alert_email" { - description = "Email address for alert notifications" - type = string - default = "ops@shieldai.com" -} - -resource "aws_sns_topic" "alerts" { - name = "${var.project_name}-${var.environment}-alerts" - - tags = { - Environment = var.environment - Project = var.project_name - } -} - -resource "aws_sns_topic_subscription" "alerts_email" { - topic_arn = aws_sns_topic.alerts.arn - protocol = "email" - endpoint = var.alert_email -} - -resource "aws_cloudwatch_dashboard" "main" { - dashboard_name = "${var.project_name}-${var.environment}-dashboard" - - dashboard_body = jsonencode({ - widgets = [ - { - type = "metric" - properties = { - title = "ECS CPU Utilization" - metrics = [ - ["AWS/ECS", "CPUUtilization", "ClusterName", var.cluster_name] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 300 - } - }, - { - type = "metric" - properties = { - title = "ECS Memory Utilization" - metrics = [ - ["AWS/ECS", "MemoryUtilization", "ClusterName", var.cluster_name] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 300 - } - }, - { - type = "metric" - properties = { - title = "RDS CPU Utilization" - metrics = [ - ["AWS/RDS", "CPUUtilization", "DBInstanceIdentifier", var.rds_identifier] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 300 - } - }, - { - type = "metric" - properties = { - title = "ALB Request Count" - metrics = [ - ["AWS/ApplicationELB", "RequestCount", "LoadBalancer", "${var.cluster_name}-alb"] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 60 - } - }, - { - type = "metric" - properties = { - title = "ALB 5xx Errors" - metrics = [ - ["AWS/ApplicationELB", "HTTPCode_Elb_5XX_Count", "LoadBalancer", "${var.cluster_name}-alb"] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 60 - } - }, - { - type = "metric" - properties = { - title = "P99 Latency (Target Group)" - metrics = [ - ["AWS/ApplicationELB", "TargetResponseTime", "LoadBalancer", "${var.cluster_name}-alb", "Statistic", "p99"], - ["AWS/ApplicationELB", "TargetResponseTime", "LoadBalancer", "${var.cluster_name}-alb", "Statistic", "p95"] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 60 - } - }, - { - type = "metric" - properties = { - title = "Error Rate (5xx / Total)" - metrics = [ - ["AWS/ApplicationELB", "HTTPCode_Elb_5XX_Count", "LoadBalancer", "${var.cluster_name}-alb"], - ["AWS/ApplicationELB", "HTTPCode_Elb_4XX_Count", "LoadBalancer", "${var.cluster_name}-alb"] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 60 - } - }, - { - type = "metric" - properties = { - title = "Throughput (Request Count)" - metrics = [ - ["AWS/ApplicationELB", "RequestCount", "LoadBalancer", "${var.cluster_name}-alb"] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 60 - yAxis = { - left = { - label = "Requests/sec" - } - } - } - }, - { - type = "metric" - properties = { - title = "API Latency Percentiles" - metrics = [ - ["ShieldAI", "api_latency", "service", "api", "percentile", "p99", "statistic", "Average"], - ["ShieldAI", "api_latency", "service", "api", "percentile", "p95", "statistic", "Average"], - ["ShieldAI", "api_latency", "service", "api", "percentile", "p50", "statistic", "Average"] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 60 - } - }, - { - type = "metric" - properties = { - title = "API Error Rate" - metrics = [ - ["ShieldAI", "api_errors", "service", "api", "statistic", "Sum"] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 60 - } - }, - { - type = "metric" - properties = { - title = "API Throughput" - metrics = [ - ["ShieldAI", "api_requests", "service", "api", "statistic", "Sum"] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 60 - } - }, - { - type = "metric" - properties = { - title = "ECS Running Tasks" - metrics = [ - ["AWS/ECS", "RunningTaskCount", "ClusterName", var.cluster_name] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 60 - } - }, - { - type = "metric" - properties = { - title = "RDS Read/Write IOPS" - metrics = [ - ["AWS/RDS", "ReadIOPS", "DBInstanceIdentifier", var.rds_identifier], - ["AWS/RDS", "WriteIOPS", "DBInstanceIdentifier", var.rds_identifier] - ] - view = "timeSeries" - stacked = false - region = "us-east-1" - period = 60 - } - } - ] - }) -} - -resource "aws_cloudwatch_metric_alarm" "ecs_cpu_high" { - alarm_name = "${var.project_name}-${var.environment}-ecs-cpu-high" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 2 - metric_name = "CPUUtilization" - namespace = "AWS/ECS" - period = 300 - statistic = "Average" - threshold = 80 - alarm_description = "ECS CPU utilization above 80%" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - ClusterName = var.cluster_name - } -} - -resource "aws_cloudwatch_metric_alarm" "ecs_memory_high" { - alarm_name = "${var.project_name}-${var.environment}-ecs-memory-high" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 2 - metric_name = "MemoryUtilization" - namespace = "AWS/ECS" - period = 300 - statistic = "Average" - threshold = 85 - alarm_description = "ECS memory utilization above 85%" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - ClusterName = var.cluster_name - } -} - -resource "aws_cloudwatch_metric_alarm" "alb_5xx" { - alarm_name = "${var.project_name}-${var.environment}-alb-5xx" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 3 - metric_name = "HTTPCode_Elb_5XX_Count" - namespace = "AWS/ApplicationELB" - period = 60 - statistic = "Sum" - threshold = 10 - alarm_description = "ALB 5xx errors above 10 per minute" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - LoadBalancer = "${var.cluster_name}-alb" - } -} - -resource "aws_cloudwatch_metric_alarm" "rds_cpu_high" { - alarm_name = "${var.project_name}-${var.environment}-rds-cpu-high" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 2 - metric_name = "CPUUtilization" - namespace = "AWS/RDS" - period = 300 - statistic = "Average" - threshold = 75 - alarm_description = "RDS CPU utilization above 75%" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - DBInstanceIdentifier = var.rds_identifier - } -} - -resource "aws_cloudwatch_metric_alarm" "rds_free_storage" { - alarm_name = "${var.project_name}-${var.environment}-rds-free-storage" - comparison_operator = "LessThanThreshold" - evaluation_periods = 2 - metric_name = "FreeStorageSpace" - namespace = "AWS/RDS" - period = 300 - statistic = "Average" - threshold = 524288000 - alarm_description = "RDS free storage below 500MB" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - DBInstanceIdentifier = var.rds_identifier - } -} - -resource "aws_cloudwatch_metric_alarm" "p99_latency_high" { - alarm_name = "${var.project_name}-${var.environment}-p99-latency-high" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 3 - metric_name = "TargetResponseTime" - namespace = "AWS/ApplicationELB" - period = 60 - statistic = "p99" - threshold = 2 - alarm_description = "P99 latency above 2 seconds" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - LoadBalancer = "${var.cluster_name}-alb" - } -} - -resource "aws_cloudwatch_metric_alarm" "error_rate_high" { - alarm_name = "${var.project_name}-${var.environment}-error-rate-high" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 3 - metric_name = "HTTPCode_Elb_5XX_Count" - namespace = "AWS/ApplicationELB" - period = 60 - statistic = "Sum" - threshold = 5 - alarm_description = "Error rate above 5 errors per minute" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - LoadBalancer = "${var.cluster_name}-alb" - } -} - -resource "aws_cloudwatch_metric_alarm" "throughput_low" { - alarm_name = "${var.project_name}-${var.environment}-throughput-low" - comparison_operator = "LessThanThreshold" - evaluation_periods = 5 - metric_name = "RequestCount" - namespace = "AWS/ApplicationELB" - period = 60 - statistic = "Sum" - threshold = 10 - alarm_description = "Throughput below 10 requests per minute" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - LoadBalancer = "${var.cluster_name}-alb" - } -} - -resource "aws_cloudwatch_log_group" "api" { - name = "/${var.project_name}/${var.environment}/api" - retention_in_days = 30 - - tags = { - Environment = var.environment - Project = var.project_name - Service = "api" - } -} - -resource "aws_cloudwatch_log_group" "datadog" { - name = "/${var.project_name}/${var.environment}/datadog" - retention_in_days = 30 - - tags = { - Environment = var.environment - Project = var.project_name - Service = "datadog" - } -} - -resource "aws_cloudwatch_log_group" "sentry" { - name = "/${var.project_name}/${var.environment}/sentry" - retention_in_days = 30 - - tags = { - Environment = var.environment - Project = var.project_name - Service = "sentry" - } -} - -resource "aws_cloudwatch_metric_alarm" "app_p99_latency_high" { - alarm_name = "${var.project_name}-${var.environment}-app-p99-latency-high" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 3 - metric_name = "api_latency" - namespace = "ShieldAI" - period = 60 - statistic = "Average" - threshold = 2000 - alarm_description = "Application P99 latency above 2000ms" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - service = "api" - percentile = "p99" - } -} - -resource "aws_cloudwatch_metric_alarm" "app_error_rate_high" { - alarm_name = "${var.project_name}-${var.environment}-app-error-rate-high" - comparison_operator = "GreaterThanThreshold" - evaluation_periods = 3 - metric_name = "api_errors" - namespace = "ShieldAI" - period = 60 - statistic = "Sum" - threshold = 10 - alarm_description = "Application error count above 10 per minute" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - service = "api" - } -} - -resource "aws_cloudwatch_metric_alarm" "app_throughput_low" { - alarm_name = "${var.project_name}-${var.environment}-app-throughput-low" - comparison_operator = "LessThanThreshold" - evaluation_periods = 5 - metric_name = "api_requests" - namespace = "ShieldAI" - period = 60 - statistic = "Sum" - threshold = 10 - alarm_description = "Application throughput below 10 requests per minute" - alarm_actions = [aws_sns_topic.alerts.arn] - - dimensions = { - service = "api" - } -} - -output "dashboard_url" { - description = "CloudWatch dashboard URL" - value = "https://us-east-1.console.aws.amazon.com/cloudwatch/home#dashboards/dashboard/${var.project_name}-${var.environment}-dashboard" -} - -output "sns_topic_arn" { - description = "SNS topic ARN for alerts" - value = aws_sns_topic.alerts.arn -} diff --git a/infra/modules/ecs/main.tf b/infra/modules/ecs/main.tf deleted file mode 100644 index 825a0f1..0000000 --- a/infra/modules/ecs/main.tf +++ /dev/null @@ -1,519 +0,0 @@ -variable "environment" { - description = "Deployment environment" - type = string -} - -variable "cluster_name" { - description = "ECS cluster name" - type = string -} - -variable "vpc_id" { - description = "VPC ID" - type = string -} - -variable "subnet_ids" { - description = "Private subnet IDs for ECS tasks" - type = list(string) -} - -variable "public_subnet_ids" { - description = "Public subnet IDs for ALB" - type = list(string) -} - -variable "security_group_ids" { - description = "Security group IDs" - type = list(string) -} - -variable "alb_security_group_id" { - description = "ALB security group ID" - type = string -} - -variable "services" { - description = "ECS services to deploy" - type = map(object({ - cpu = number - memory = number - port = number - })) -} - -variable "container_images" { - description = "Container image tags" - type = map(string) -} - -variable "secrets_arn" { - description = "Secrets Manager ARN" - type = string -} - -variable "cache_cluster_arn" { - description = "ElastiCache replication group ARN" - type = string -} - -variable "domain_name" { - description = "Route53 hosted zone domain for ACM cert validation" - type = string - default = "shieldai.app" -} - -resource "aws_ecs_cluster" "main" { - name = var.cluster_name - - settings { - name = "containerInsights" - value = "enabled" - } - - tags = { - Name = var.cluster_name - } -} - -resource "aws_ecs_cluster_capacity_providers" "main" { - cluster_name = aws_ecs_cluster.main.name - - capacity_providers = ["FARGATE"] - - default_capacity_provider_strategy { - base = 1 - weight = 100 - capacity_provider = "FARGATE" - } -} - -resource "aws_ecs_task_definition" "services" { - for_each = var.services - - family = "${var.cluster_name}-${each.key}" - - container_definitions = jsonencode([ - { - name = each.key - image = "ghcr.io/shieldai/shieldai-${each.key}:${var.container_images[each.key]}" - cpu = each.cpu - memory = each.memory - essential = true - - portMappings = [ - { - containerPort = each.port - hostPort = each.port - protocol = "tcp" - } - ] - - environment = [ - { - name = "NODE_ENV" - value = var.environment - }, - { - name = "PORT" - value = tostring(each.port) - }, - { - name = "DD_ENV" - value = var.environment - }, - { - name = "DD_SERVICE" - value = "${var.cluster_name}-${each.key}" - }, - { - name = "DD_VERSION" - value = var.container_images[each.key] - }, - { - name = "DD_TRACE_ENABLED" - value = "true" - }, - { - name = "DD_LOGS_INJECTION" - value = "true" - }, - { - name = "DD_AGENT_HOST" - value = "localhost" - }, - { - name = "DD_AGENT_PORT" - value = "8126" - }, - { - name = "SENTRY_ENVIRONMENT" - value = var.environment - }, - { - name = "SENTRY_RELEASE" - value = var.container_images[each.key] - }, - { - name = "AWS_REGION" - value = "us-east-1" - }, - { - name = "DD_SITE" - value = "datadoghq.com" - } - ] - - secrets = [ - { - name = "DATABASE_URL" - valueFrom = "${var.secrets_arn}:DATABASE_URL::" - }, - { - name = "REDIS_URL" - valueFrom = "${var.secrets_arn}:REDIS_URL::" - }, - { - name = "HIBP_API_KEY" - valueFrom = "${var.secrets_arn}:HIBP_API_KEY::" - }, - { - name = "RESEND_API_KEY" - valueFrom = "${var.secrets_arn}:RESEND_API_KEY::" - }, - { - name = "SENTRY_DSN" - valueFrom = "${var.secrets_arn}:SENTRY_DSN::" - }, - { - name = "DD_API_KEY" - valueFrom = "${var.secrets_arn}:DD_API_KEY::" - } - ] - - logConfiguration = { - logDriver = "awslogs" - options = { - "awslogs-group" = "/ecs/${var.cluster_name}-${each.key}" - "awslogs-region" = "us-east-1" - "awslogs-stream-prefix" = each.key - } - } - - healthCheck = { - command = ["CMD-SHELL", "curl -f http://localhost:${each.port}/health || exit 1"] - interval = 30 - timeout = 5 - retries = 3 - startPeriod = 60 - } - } - ]) - - network_mode = "awsvpc" - memory = each.memory - cpu = each.cpu - requires_compatibilities = ["FARGATE"] - - execution_role_arn = aws_iam_role.execution[each.key].arn - task_role_arn = aws_iam_role.task[each.key].arn - - tags = { - Name = "${var.cluster_name}-${each.key}" - } -} - -resource "aws_iam_role" "execution" { - for_each = var.services - - name = "${var.cluster_name}-${each.key}-execution" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "ecs-tasks.amazonaws.com" - } - } - ] - }) - - managed_policy_arns = [ - "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" - ] -} - -resource "aws_iam_role" "task" { - for_each = var.services - - name = "${var.cluster_name}-${each.key}-task" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "ecs-tasks.amazonaws.com" - } - } - ] - }) - - inline_policy { - name = "secrets-manager-access" - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "secretsmanager:GetSecretValue", - "secretsmanager:DescribeSecret" - ] - Resource = var.secrets_arn - } - ] - }) - } - - inline_policy { - name = "elasticache-access" - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Effect = "Allow" - Action = [ - "elasticache:DescribeCacheClusters", - "elasticache:DescribeCacheSubnetGroups" - ] - Resource = var.cache_cluster_arn - } - ] - }) - } -} - -resource "aws_ecs_service" "services" { - for_each = var.services - - name = "${var.cluster_name}-${each.key}" - cluster = aws_ecs_cluster.main.id - task_definition = aws_ecs_task_definition.services[each.key].arn - desired_count = var.environment == "production" ? 3 : 1 - - launch_type = "FARGATE" - - network_configuration { - subnets = var.subnet_ids - security_groups = var.security_group_ids - assign_public_ip = false - } - - load_balancer { - target_group_arn = aws_lb_target_group.services[each.key].arn - container_name = each.key - container_port = each.port - } - - auto_scaling { - max_capacity = var.environment == "production" ? 10 : 3 - min_capacity = var.environment == "production" ? 2 : 1 - } - - tags = { - Name = "${var.cluster_name}-${each.key}" - Service = each.key - } - - depends_on = [ - aws_lb_listener.https - ] -} - -resource "aws_lb" "main" { - name = "${var.cluster_name}-alb" - internal = false - load_balancer_type = "application" - security_groups = [var.alb_security_group_id] - subnets = var.public_subnet_ids - - tags = { - Name = "${var.cluster_name}-alb" - } -} - -resource "aws_acm_certificate" "main" { - domain_name = "${var.cluster_name}.${var.environment}.shieldai.app" - validation_method = "DNS" - - tags = { - Name = "${var.cluster_name}-cert" - } -} - -data "aws_route53_zone" "main" { - name = var.domain_name -} - -resource "aws_route53_record" "acm_validation" { - for_each = { - for rv in aws_acm_certificate.main.domain_validation_options : rv.domain_name => rv - if rv.resource_record_name != null - } - - zone_id = data.aws_route53_zone.main.zone_id - name = each.value.resource_record_name - type = each.value.resource_record_type - ttl = 60 - records = [each.value.resource_record_value] -} - -resource "aws_acm_certificate_validation" "main" { - certificate_arn = aws_acm_certificate.main.arn - validation_record_fqdns = [aws_route53_record.acm_validation[*].fqdn] -} - -resource "aws_lb_target_group" "services" { - for_each = var.services - - name = "${var.cluster_name}-${each.key}-tg" - port = each.port - protocol = "HTTP" - vpc_id = var.vpc_id - - health_check { - enabled = true - healthy_threshold = 3 - interval = 30 - matcher = "200" - path = "/health" - port = "traffic-port" - protocol = "HTTP" - timeout = 5 - unhealthy_threshold = 3 - } - - stickiness { - type = "lb_cookie" - cookie_duration = 86400 - } -} - -resource "aws_lb_listener" "https" { - load_balancer_arn = aws_lb.main.arn - port = 443 - protocol = "HTTPS" - ssl_certificate_arn = aws_acm_certificate_validation.main.certificate_arn - - default_action { - type = "forward" - target_group_arn = aws_lb_target_group.services["api"].arn - } -} - -resource "aws_lb_listener_rule" "services" { - for_each = { for k, v in var.services : k => v if k != "api" } - - listener_arn = aws_lb_listener.https.arn - action { - type = "forward" - target_group_arn = aws_lb_target_group.services[each.key].arn - } - - condition { - path_pattern { - values = ["/${each.key}/*", "/${each.key}"] - } - } -} - -resource "aws_lb_listener" "http_redirect" { - load_balancer_arn = aws_lb.main.arn - port = 80 - protocol = "HTTP" - - default_action { - type = "redirect" - - redirect { - port = "443" - protocol = "HTTPS" - status_code = "HTTP_301" - } - } -} - -resource "aws_appautoscaling_target" "services" { - for_each = var.services - - service_namespace = "ecs" - resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.services[each.key].name}" - scalable_dimension = "ecs:service:DesiredCount" - min_capacity = var.environment == "production" ? 2 : 1 - max_capacity = var.environment == "production" ? 10 : 3 -} - -resource "aws_appautoscaling_policy" "cpu" { - for_each = var.services - - name = "${var.cluster_name}-${each.key}-cpu-scaling" - service_namespace = "ecs" - resource_id = "service/${aws_ecs_cluster.main.name}/${aws_ecs_service.services[each.key].name}" - scalable_dimension = "ecs:service:DesiredCount" - - target_tracking_scaling_policy_configuration { - target_value = 70.0 - scale_in_cooldown = 60 - scale_out_cooldown = 30 - - customized_metric_specification { - metric_name = "CPUUtilization" - namespace = "AWS/ECS" - statistic = "Average" - dimensions = [{ name = "ClusterName", value = aws_ecs_cluster.main.name }] - } - } -} - -resource "aws_kms_key" "logs" { - description = "${var.cluster_name} logs encryption key" - deletion_window_in_days = 7 - enable_key_rotation = true - - tags = { - Name = "${var.cluster_name}-logs-kms" - } -} - -resource "aws_cloudwatch_log_group" "services" { - for_each = var.services - - name = "/ecs/${var.cluster_name}-${each.key}" - retention_in_days = var.environment == "production" ? 30 : 7 - kms_key_id = aws_kms_key.logs.arn - - tags = { - Name = "${var.cluster_name}-${each.key}-logs" - } -} - -output "cluster_arn" { - description = "ECS cluster ARN" - value = aws_ecs_cluster.main.arn -} - -output "alb_dns_name" { - description = "ALB DNS name" - value = aws_lb.main.dns_name -} - -output "kms_key_arn" { - description = "KMS key ARN for log encryption" - value = aws_kms_key.logs.arn -} diff --git a/infra/modules/elasticache/main.tf b/infra/modules/elasticache/main.tf deleted file mode 100644 index 3f354da..0000000 --- a/infra/modules/elasticache/main.tf +++ /dev/null @@ -1,102 +0,0 @@ -variable "environment" { - description = "Deployment environment" - type = string -} - -variable "vpc_id" { - description = "VPC ID" - type = string -} - -variable "subnet_ids" { - description = "Private subnet IDs" - type = list(string) -} - -variable "security_group_id" { - description = "ElastiCache security group ID" - type = string -} - -variable "node_type" { - description = "Cache node type" - type = string -} - -variable "num_nodes" { - description = "Number of cache nodes" - type = number -} - -variable "project_name" { - description = "Project name" - type = string -} - -resource "aws_elasticache_subnet_group" "main" { - name = "${var.project_name}-${var.environment}-redis-subnet" - subnet_ids = var.subnet_ids - - tags = { - Name = "${var.project_name}-${var.environment}-redis-subnet" - } -} - -resource "random_password" "redis_auth" { - length = 32 - special = false - - keepers = { - environment = var.environment - } -} - -resource "aws_elasticache_replication_group" "main" { - replication_group_id = "${var.project_name}-${var.environment}-redis" - description = "${var.project_name} Redis cluster (${var.environment})" - - node_type = var.node_type - num_cache_clusters = var.num_nodes - engine = "redis" - engine_version = "7.0" - - auth_token = random_password.redis_auth.result - - transit_encryption_enabled = true - at_rest_encryption_enabled = true - - port = 6379 - - subnet_group_name = aws_elasticache_subnet_group.main.name - security_group_ids = [var.security_group_id] - - automatic_failover_enabled = var.environment == "production" - - snapshot_retention_limit = var.environment == "production" ? 7 : 1 - snapshot_window = "03:00-04:00" - - tags = { - Name = "${var.project_name}-${var.environment}-redis" - } -} - -output "cache_endpoint" { - description = "ElastiCache primary endpoint" - value = aws_elasticache_replication_group.main.primary_endpoint_address -} - -output "reader_endpoint" { - description = "ElastiCache reader endpoint" - value = aws_elasticache_replication_group.main.reader_endpoint_address -} - -output "auth_token" { - description = "Redis auth token" - value = random_password.redis_auth.result - sensitive = true -} - -output "replication_group_arn" { - description = "ElastiCache replication group ARN" - value = aws_elasticache_replication_group.main.arn -} diff --git a/infra/modules/rds/main.tf b/infra/modules/rds/main.tf deleted file mode 100644 index 0dd0950..0000000 --- a/infra/modules/rds/main.tf +++ /dev/null @@ -1,138 +0,0 @@ -variable "environment" { - description = "Deployment environment" - type = string -} - -variable "vpc_id" { - description = "VPC ID" - type = string -} - -variable "subnet_ids" { - description = "Private subnet IDs" - type = list(string) -} - -variable "security_group_id" { - description = "RDS security group ID" - type = string -} - -variable "db_name" { - description = "Database name" - type = string -} - -variable "db_instance_class" { - description = "RDS instance class" - type = string -} - -variable "multi_az" { - description = "Multi-AZ deployment" - type = bool -} - -variable "backup_retention" { - description = "Backup retention days" - type = number -} - -variable "project_name" { - description = "Project name" - type = string -} - -resource "aws_db_subnet_group" "main" { - name = "${var.project_name}-${var.environment}-db-subnet" - subnet_ids = var.subnet_ids - - tags = { - Name = "${var.project_name}-${var.environment}-db-subnet" - } -} - -resource "aws_db_instance" "main" { - identifier = "${var.project_name}-${var.environment}-db" - - engine = "postgres" - engine_version = "16.2" - instance_class = var.db_instance_class - allocated_storage = var.environment == "production" ? 100 : 20 - - db_name = var.db_name - username = "shieldai" - password = random_password.db_password.result - - multi_az = var.multi_az - db_subnet_group_name = aws_db_subnet_group.main.name - vpc_security_group_ids = [var.security_group_id] - - backup_retention_period = var.backup_retention - backup_window = "03:00-04:00" - maintenance_window = "sun:04:00-sun:05:00" - - skip_final_snapshot = var.environment != "production" - final_snapshot_identifier = "${var.project_name}-${var.environment}-final" - - storage_encrypted = true - storage_type = "gp3" - iops = var.environment == "production" ? 3000 : 1000 - - deletion_protection = var.environment == "production" - copy_tags_to_snapshot = true - - tags = { - Name = "${var.project_name}-${var.environment}-db" - } -} - -resource "random_password" "db_password" { - length = 16 - special = true - - keepers = { - environment = var.environment - } -} - -resource "aws_secretsmanager_secret_version" "db_password" { - secret_id = aws_secretsmanager_secret.db_password.id - secret_string = jsonencode({ - username = "shieldai" - password = random_password.db_password.result - engine = "postgres" - host = aws_db_instance.main.address - port = aws_db_instance.main.port - }) -} - -resource "aws_secretsmanager_secret" "db_password" { - name = "${var.project_name}-${var.environment}-db-password" - - tags = { - Name = "${var.project_name}-${var.environment}-db-password" - } -} - -output "db_endpoint" { - description = "RDS endpoint" - value = aws_db_instance.main.endpoint - sensitive = true -} - -output "db_instance_identifier" { - description = "RDS instance identifier" - value = aws_db_instance.main.identifier -} - -output "db_password_secret_arn" { - description = "DB password secret ARN" - value = aws_secretsmanager_secret.db_password.arn -} - -output "db_password" { - description = "Generated DB password" - value = random_password.db_password.result - sensitive = true -} diff --git a/infra/modules/s3/main.tf b/infra/modules/s3/main.tf deleted file mode 100644 index 1c23294..0000000 --- a/infra/modules/s3/main.tf +++ /dev/null @@ -1,145 +0,0 @@ -variable "environment" { - description = "Deployment environment" - type = string -} - -variable "project_name" { - description = "Project name" - type = string -} - -resource "aws_s3_bucket" "terraform_state" { - bucket = "${var.project_name}-${var.environment}-terraform-state" - - tags = { - Name = "${var.project_name}-${var.environment}-terraform-state" - } -} - -resource "aws_s3_bucket_public_access_block" "terraform_state" { - bucket = aws_s3_bucket.terraform_state.id - - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -resource "aws_s3_bucket_versioning" "terraform_state" { - bucket = aws_s3_bucket.terraform_state.id - versioning_configuration { - status = "Enabled" - } -} - -resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" { - bucket = aws_s3_bucket.terraform_state.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "aws:kms" - } - } -} - -resource "aws_s3_bucket_lifecycle_configuration" "terraform_state" { - bucket = aws_s3_bucket.terraform_state.id - - rule { - id = "expire-noncurrent" - status = "Enabled" - - noncurrent_version_expiration { - noncurrent_days = 30 - } - } -} - -resource "aws_s3_bucket" "artifacts" { - bucket = "${var.project_name}-${var.environment}-artifacts" - - tags = { - Name = "${var.project_name}-${var.environment}-artifacts" - } -} - -resource "aws_s3_bucket_public_access_block" "artifacts" { - bucket = aws_s3_bucket.artifacts.id - - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -resource "aws_s3_bucket_versioning" "artifacts" { - bucket = aws_s3_bucket.artifacts.id - versioning_configuration { - status = "Enabled" - } -} - -resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" { - bucket = aws_s3_bucket.artifacts.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "aws:kms" - } - } -} - -resource "aws_s3_bucket" "logs" { - bucket = "${var.project_name}-${var.environment}-logs" - - tags = { - Name = "${var.project_name}-${var.environment}-logs" - } -} - -resource "aws_s3_bucket_public_access_block" "logs" { - bucket = aws_s3_bucket.logs.id - - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { - bucket = aws_s3_bucket.logs.id - - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "aws:kms" - } - } -} - -resource "aws_s3_bucket_lifecycle_configuration" "logs" { - bucket = aws_s3_bucket.logs.id - - rule { - id = "expire-old-logs" - status = "Enabled" - - expiration { - days = 90 - } - } -} - -output "bucket_name" { - description = "Terraform state S3 bucket name" - value = aws_s3_bucket.terraform_state.id -} - -output "artifacts_bucket_name" { - description = "Artifacts S3 bucket name" - value = aws_s3_bucket.artifacts.id -} - -output "logs_bucket_name" { - description = "Logs S3 bucket name" - value = aws_s3_bucket.logs.id -} diff --git a/infra/modules/secrets/main.tf b/infra/modules/secrets/main.tf deleted file mode 100644 index fd5b5f8..0000000 --- a/infra/modules/secrets/main.tf +++ /dev/null @@ -1,69 +0,0 @@ -variable "environment" { - description = "Deployment environment" - type = string -} - -variable "project_name" { - description = "Project name" - type = string -} - -variable "rds_endpoint" { - description = "RDS instance endpoint" - type = string -} - -variable "db_password" { - description = "Generated RDS password" - type = string - sensitive = true -} - -variable "elasticache_endpoint" { - description = "ElastiCache primary endpoint" - type = string -} - -variable "redis_auth_token" { - description = "ElastiCache auth token" - type = string - sensitive = true -} - -variable "secrets" { - description = "Secrets to store" - type = map(string) - default = {} -} - -resource "aws_secretsmanager_secret" "main" { - name = "${var.project_name}-${var.environment}-app-secrets" - - description = "Application secrets for ${var.project_name} (${var.environment})" - - tags = { - Name = "${var.project_name}-${var.environment}-app-secrets" - Environment = var.environment - } -} - -resource "aws_secretsmanager_secret_version" "main" { - secret_id = aws_secretsmanager_secret.main.id - - secret_string = jsonencode(merge({ - DATABASE_URL = "postgresql://shieldai:${var.db_password}@${var.rds_endpoint}:5432/shieldai" - REDIS_URL = "redis://:${var.redis_auth_token}@${var.elasticache_endpoint}:6379" - NODE_ENV = var.environment - LOG_LEVEL = var.environment == "production" ? "info" : "debug" - }, var.secrets)) -} - -output "secrets_manager_arn" { - description = "Secrets Manager ARN" - value = aws_secretsmanager_secret.main.arn -} - -output "secrets_manager_name" { - description = "Secrets Manager secret name" - value = aws_secretsmanager_secret.main.name -} diff --git a/infra/modules/vpc/main.tf b/infra/modules/vpc/main.tf deleted file mode 100644 index 9f87108..0000000 --- a/infra/modules/vpc/main.tf +++ /dev/null @@ -1,338 +0,0 @@ -variable "environment" { - description = "Deployment environment" - type = string -} - -variable "vpc_cidr" { - description = "CIDR block for VPC" - type = string -} - -variable "az_count" { - description = "Number of availability zones" - type = number -} - -variable "project_name" { - description = "Project name" - type = string -} - -variable "kms_key_arn" { - description = "KMS key ARN for log encryption" - type = string - default = "" -} - -resource "aws_vpc" "main" { - cidr_block = var.vpc_cidr - enable_dns_support = true - enable_dns_hostnames = true - - tags = { - Name = "${var.project_name}-${var.environment}-vpc" - } -} - -data "aws_availability_zones" "available" { - state = "available" -} - -resource "aws_subnet" "public" { - count = var.az_count - - vpc_id = aws_vpc.main.id - cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index) - availability_zone = data.aws_availability_zones.available.names[count.index] - map_public_ip_on_launch = false - - tags = { - Name = "${var.project_name}-${var.environment}-public-${data.aws_availability_zones.available.names[count.index]}" - "kubernetes.io/role/elb" = "1" - } -} - -resource "aws_subnet" "private" { - count = var.az_count - - vpc_id = aws_vpc.main.id - cidr_block = cidrsubnet(var.vpc_cidr, 8, var.az_count + count.index) - availability_zone = data.aws_availability_zones.available.names[count.index] - - tags = { - Name = "${var.project_name}-${var.environment}-private-${data.aws_availability_zones.available.names[count.index]}" - "kubernetes.io/role/internal-elb" = "1" - } -} - -resource "aws_internet_gateway" "main" { - vpc_id = aws_vpc.main.id - - tags = { - Name = "${var.project_name}-${var.environment}-igw" - } -} - -resource "aws_eip" "nat" { - count = var.az_count - - domain = "vpc" - - tags = { - Name = "${var.project_name}-${var.environment}-nat-${count.index}" - } -} - -resource "aws_nat_gateway" "main" { - count = var.az_count - - allocation_id = aws_eip.nat[count.index].id - subnet_id = aws_subnet.public[count.index].id - - tags = { - Name = "${var.project_name}-${var.environment}-nat-${count.index}" - } - - depends_on = [aws_internet_gateway.main] -} - -resource "aws_route_table" "public" { - vpc_id = aws_vpc.main.id - - route { - cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.main.id - } - - tags = { - Name = "${var.project_name}-${var.environment}-public-rt" - } -} - -resource "aws_route_table" "private" { - count = var.az_count - - vpc_id = aws_vpc.main.id - - route { - cidr_block = "0.0.0.0/0" - nat_gateway_id = aws_nat_gateway.main[count.index].id - } - - tags = { - Name = "${var.project_name}-${var.environment}-private-rt-${count.index}" - } -} - -resource "aws_route_table_association" "public" { - count = var.az_count - - subnet_id = aws_subnet.public[count.index].id - route_table_id = aws_route_table.public.id -} - -resource "aws_route_table_association" "private" { - count = var.az_count - - subnet_id = aws_subnet.private[count.index].id - route_table_id = aws_route_table.private[count.index].id -} - -resource "aws_security_group" "alb" { - name_prefix = "${var.project_name}-${var.environment}-alb" - vpc_id = aws_vpc.main.id - - ingress { - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - description = "HTTPS from internet" - } - - ingress { - from_port = 80 - to_port = 80 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - description = "HTTP from internet (redirect)" - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - Name = "${var.project_name}-${var.environment}-alb-sg" - } -} - -resource "aws_security_group" "ecs" { - name_prefix = "${var.project_name}-${var.environment}-ecs" - vpc_id = aws_vpc.main.id - - ingress { - from_port = 3000 - to_port = 3003 - protocol = "tcp" - security_groups = [aws_security_group.alb.id] - description = "Service ports from ALB only" - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - Name = "${var.project_name}-${var.environment}-ecs-sg" - } -} - -resource "aws_security_group" "rds" { - name_prefix = "${var.project_name}-${var.environment}-rds" - vpc_id = aws_vpc.main.id - - ingress { - from_port = 5432 - to_port = 5432 - protocol = "tcp" - security_groups = [aws_security_group.ecs.id] - description = "PostgreSQL from ECS" - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - Name = "${var.project_name}-${var.environment}-rds-sg" - } -} - -resource "aws_security_group" "elasticache" { - name_prefix = "${var.project_name}-${var.environment}-elasticache" - vpc_id = aws_vpc.main.id - - ingress { - from_port = 6379 - to_port = 6379 - protocol = "tcp" - security_groups = [aws_security_group.ecs.id] - description = "Redis from ECS" - } - - egress { - from_port = 0 - to_port = 0 - protocol = "-1" - cidr_blocks = ["0.0.0.0/0"] - } - - tags = { - Name = "${var.project_name}-${var.environment}-elasticache-sg" - } -} - -resource "aws_flow_log" "main" { - iam_role_arn = aws_iam_role.flow_log.arn - log_destination = aws_cloudwatch_log_group.flow_log.arn - vpc_id = aws_vpc.main.id - traffic_type = "ALL" - - tags = { - Name = "${var.project_name}-${var.environment}-flow-log" - } -} - -resource "aws_iam_role" "flow_log" { - name = "${var.project_name}-${var.environment}-flow-log-role" - - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRole" - Effect = "Allow" - Principal = { - Service = "vpc-flow-logs.amazonaws.com" - } - } - ] - }) -} - -resource "aws_iam_role_policy" "flow_log" { - name = "${var.project_name}-${var.environment}-flow-log-policy" - role = aws_iam_role.flow_log.id - - policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - "logs:DescribeLogGroups", - "logs:DescribeLogStreams" - ] - Effect = "Allow" - Resource = [aws_cloudwatch_log_group.flow_log.arn] - } - ] - }) -} - -resource "aws_cloudwatch_log_group" "flow_log" { - name = "/${var.project_name}/${var.environment}/vpc-flow-log" - retention_in_days = var.environment == "production" ? 30 : 7 - kms_key_id = var.kms_key_arn != "" ? var.kms_key_arn : null - - tags = { - Name = "${var.project_name}-${var.environment}-flow-log" - } -} - -output "vpc_id" { - description = "VPC ID" - value = aws_vpc.main.id -} - -output "private_subnet_ids" { - description = "Private subnet IDs" - value = aws_subnet.private[*].id -} - -output "public_subnet_ids" { - description = "Public subnet IDs" - value = aws_subnet.public[*].id -} - -output "alb_security_group_id" { - description = "ALB security group ID" - value = aws_security_group.alb.id -} - -output "ecs_security_group_id" { - description = "ECS security group ID" - value = aws_security_group.ecs.id -} - -output "rds_security_group_id" { - description = "RDS security group ID" - value = aws_security_group.rds.id -} - -output "elasticache_security_group_id" { - description = "ElastiCache security group ID" - value = aws_security_group.elasticache.id -} diff --git a/infra/outputs.tf b/infra/outputs.tf deleted file mode 100644 index 2bb06b5..0000000 --- a/infra/outputs.tf +++ /dev/null @@ -1,35 +0,0 @@ -output "vpc_id" { - description = "VPC ID" - value = module.vpc.vpc_id -} - -output "cluster_name" { - description = "ECS cluster name" - value = "${var.project_name}-${var.environment}" -} - -output "rds_endpoint" { - description = "RDS endpoint" - value = module.rds.db_endpoint - sensitive = true -} - -output "elasticache_endpoint" { - description = "ElastiCache primary endpoint" - value = module.elasticache.cache_endpoint -} - -output "s3_bucket_name" { - description = "S3 bucket name" - value = module.s3.bucket_name -} - -output "secrets_manager_arn" { - description = "Secrets Manager ARN" - value = module.secrets.secrets_manager_arn -} - -output "cloudwatch_dashboard_url" { - description = "CloudWatch dashboard URL" - value = module.cloudwatch.dashboard_url -} diff --git a/infra/scripts/rollback-compose.sh b/infra/scripts/rollback-compose.sh deleted file mode 100755 index 96412c0..0000000 --- a/infra/scripts/rollback-compose.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ShieldAI Docker Compose Rollback Script -# Usage: ./rollback-compose.sh [--env prod|dev] -# -# Rolls back all services to a previous tagged image using docker-compose.prod.yml -# -# Examples: -# ./rollback-compose.sh v1.2.3 # Rollback to v1.2.3 -# ./rollback-compose.sh v1.2.3 --env prod # Explicit production compose - -PREVIOUS_TAG="${1:-}" -ENV_MODE="${2:-prod}" - -# ─── Configuration ─────────────────────────────────────────────── -SERVICES="api darkwatch spamshield voiceprint" -COMPOSE_FILE="docker-compose.prod.yml" -REGISTRY_OWNER="${GITHUB_REPOSITORY_OWNER:-shieldai}" - -# ─── Helpers ───────────────────────────────────────────────────── -log() { - local level="$1" - shift - echo "[$(date -u '+%H:%M:%S')] [$level] $*" -} - -log_info() { log "INFO" "$@"; } -log_warn() { log "WARN" "$@"; } -log_error() { log "ERROR" "$@"; } - -# ─── Validation ────────────────────────────────────────────────── -if [[ -z "$PREVIOUS_TAG" ]]; then - log_error "Usage: $0 [--env prod|dev]" - log_error "Example: $0 v1.2.3" - exit 1 -fi - -if ! command -v docker &>/dev/null; then - log_error "Docker not found in PATH" - exit 1 -fi - -# ─── Rollback Logic ────────────────────────────────────────────── -main() { - log_info "=== Docker Compose Rollback ===" - log_info "Target tag: $PREVIOUS_TAG" - log_info "Compose file: $COMPOSE_FILE" - log_info "Registry: ghcr.io/$REGISTRY_OWNER" - - # 1. Pull previous images - log_info "Pulling previous images..." - local pull_failed=0 - for svc in $SERVICES; do - local image="ghcr.io/${REGISTRY_OWNER}/shieldai-${svc}:${PREVIOUS_TAG}" - log_info "Pulling $image..." - if docker pull "$image" 2>/dev/null; then - log_info "Pulled: $image" - else - log_warn "Pull failed: $image (may not exist)" - pull_failed=1 - fi - done - - if [[ $pull_failed -eq 1 ]]; then - log_warn "Some images may not exist at tag $PREVIOUS_TAG" - log_info "Continuing with available images..." - fi - - # 2. Stop current services gracefully - log_info "Stopping current services..." - DOCKER_TAG="$PREVIOUS_TAG" docker compose -f "$COMPOSE_FILE" down --timeout 30 2>/dev/null || true - - # 3. Start with previous tag - log_info "Starting services with tag $PREVIOUS_TAG..." - DOCKER_TAG="$PREVIOUS_TAG" docker compose -f "$COMPOSE_FILE" up -d - - # 4. Wait for services to be healthy - log_info "Waiting for services to become healthy..." - sleep 10 - - # 5. Verify health - local passed=0 - local failed=0 - - for svc in $SERVICES; do - local port - port=$(case "$svc" in - api) echo 3000 ;; - darkwatch) echo 3001 ;; - spamshield) echo 3002 ;; - voiceprint) echo 3003 ;; - esac) - - local http_code - http_code=$(curl -s -o /dev/null -w "%{http_code}" \ - --connect-timeout 10 --max-time 30 \ - "http://localhost:${port}/health" 2>/dev/null || echo "000") - - if [[ "$http_code" == "200" ]]; then - log_info "Health OK: $svc (port $port, HTTP $http_code)" - ((passed++)) - else - log_warn "Health FAIL: $svc (port $port, HTTP $http_code)" - ((failed++)) - fi - done - - log_info "=== Rollback Complete ===" - log_info "Passed: $passed, Failed: $failed" - - if [[ $failed -gt 0 ]]; then - log_warn "Some services failed health check. Check logs: docker compose -f $COMPOSE_FILE logs" - exit 1 - fi - - log_info "All services healthy after rollback" - exit 0 -} - -main "$@" diff --git a/infra/scripts/rollback-migration.sh b/infra/scripts/rollback-migration.sh deleted file mode 100755 index 2ccf4aa..0000000 --- a/infra/scripts/rollback-migration.sh +++ /dev/null @@ -1,164 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ShieldAI Database Migration Rollback Script -# Usage: ./rollback-migration.sh [--migration ] -# -# Rolls back the most recent migration or a specific named migration -# Uses AWS Secrets Manager for database credentials -# -# Examples: -# ./rollback-migration.sh staging # Rollback latest -# ./rollback-migration.sh production --migration 001_create_users # Rollback specific - -ENVIRONMENT="${1:-staging}" -MIGRATION_NAME="${3:-}" - -# ─── Configuration ─────────────────────────────────────────────── -SECRET_ID="shieldai-${ENVIRONMENT}-db-password" -DB_NAME="shieldai" -DB_USER="shieldai" - -# ─── Helpers ───────────────────────────────────────────────────── -log() { - local level="$1" - shift - echo "[$(date -u '+%H:%M:%S')] [$level] $*" -} - -log_info() { log "INFO" "$@"; } -log_warn() { log "WARN" "$@"; } -log_error() { log "ERROR" "$@"; } - -# ─── Validation ────────────────────────────────────────────────── -if [[ "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "production" ]]; then - log_error "Invalid environment: $ENVIRONMENT (expected: staging, production)" - exit 1 -fi - -for cmd in aws jq; do - if ! command -v "$cmd" &>/dev/null; then - log_error "Missing prerequisite: $cmd" - exit 1 - fi -done - -# ─── Credentials ───────────────────────────────────────────────── -get_db_credentials() { - log_info "Fetching database credentials from Secrets Manager..." - - local secret - secret=$(aws secretsmanager get-secret-value \ - --secret-id "$SECRET_ID" \ - --query 'SecretString' \ - --output json 2>/dev/null) - - if [[ -z "$secret" ]]; then - log_error "Failed to fetch secret: $SECRET_ID" - exit 1 - fi - - export DB_HOST=$(echo "$secret" | jq -r '.host') - export DB_PORT=$(echo "$secret" | jq -r '.port' // '5432') - export DB_PASS=$(echo "$secret" | jq -r '.password') - export DATABASE_URL="postgresql://${DB_USER}:${DB_PASS}@${DB_HOST}:${DB_PORT}/${DB_NAME}" - - log_info "Database: ${DB_HOST}:${DB_PORT}/${DB_NAME}" -} - -# ─── Migration Status ──────────────────────────────────────────── -show_migration_status() { - log_info "=== Current Migration Status ===" - - if command -v npx &>/dev/null; then - npx drizzle-kit status --config=drizzle.config.ts 2>/dev/null || \ - log_warn "Drizzle status check completed (some warnings expected)" - fi - - # Show applied migrations from database - log_info "Applied migrations:" - PGPASSWORD="$DB_PASS" psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_USER" -d "$DB_NAME" \ - -c "SELECT id, checksum, type FROM __drizzle_migrations_schema ORDER BY id DESC;" 2>/dev/null || \ - log_warn "Could not query migration table (psql may not be installed)" -} - -# ─── Rollback Logic ────────────────────────────────────────────── -rollback_latest() { - log_info "=== Rolling Back Latest Migration ===" - - # Get the latest applied migration - local latest_migration - latest_migration=$(PGPASSWORD="$DB_PASS" psql -h "$DB_HOST" -p "$DB_PORT" \ - -U "$DB_USER" -d "$DB_NAME" -t -A \ - -c "SELECT id FROM __drizzle_migrations_schema ORDER BY id DESC LIMIT 1;" 2>/dev/null) - - if [[ -z "$latest_migration" ]]; then - log_warn "No applied migrations found" - return 0 - fi - - log_info "Latest migration: $latest_migration" - - # Resolve the migration (marks it as not applied) - if command -v npx &>/dev/null; then - npx drizzle-kit migrate:resolve --migration "$latest_migration" --status applied 2>/dev/null || \ - log_warn "Migration resolve completed (check output for details)" - fi - - log_info "Migration $latest_migration marked as resolved" -} - -rollback_specific() { - local target="$1" - log_info "=== Rolling Back Migration: $target ===" - - if command -v npx &>/dev/null; then - npx drizzle-kit migrate:resolve --migration "$target" --status applied 2>/dev/null || \ - log_warn "Migration resolve completed (check output for details)" - fi - - log_info "Migration $target marked as resolved" -} - -# ─── Verification ──────────────────────────────────────────────── -verify_connection() { - log_info "=== Verifying Database Connection ===" - - local result - result=$(PGPASSWORD="$DB_PASS" psql -h "$DB_HOST" -p "$DB_PORT" \ - -U "$DB_USER" -d "$DB_NAME" -t -A \ - -c "SELECT version();" 2>/dev/null || echo "FAIL") - - if [[ "$result" != "FAIL" ]]; then - log_info "Connection OK: PostgreSQL $result" - else - log_warn "Connection check failed" - fi -} - -# ─── Main ──────────────────────────────────────────────────────── -main() { - log_info "=== ShieldAI Migration Rollback ===" - log_info "Environment: $ENVIRONMENT" - log_info "Secret: $SECRET_ID" - - get_db_credentials - show_migration_status - - if [[ -n "$MIGRATION_NAME" ]]; then - rollback_specific "$MIGRATION_NAME" - else - rollback_latest - fi - - verify_connection - show_migration_status - - log_info "=== Rollback Complete ===" - log_info "Next steps:" - log_info "1. Verify application schema compatibility" - log_info "2. Run application health checks" - log_info "3. If needed, redeploy ECS services: ./rollback.sh $ENVIRONMENT all" -} - -main "$@" diff --git a/infra/scripts/rollback.sh b/infra/scripts/rollback.sh deleted file mode 100755 index 82c6f2c..0000000 --- a/infra/scripts/rollback.sh +++ /dev/null @@ -1,255 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# ShieldAI ECS Rollback Script -# Usage: ./rollback.sh [--verify] -# -# Environments: staging, production -# Services: api, darkwatch, spamshield, voiceprint, all -# -# Examples: -# ./rollback.sh staging api # Rollback single service -# ./rollback.sh production all # Rollback all services -# ./rollback.sh production all --verify # Rollback with post-verification - -# ─── Configuration ─────────────────────────────────────────────── -ENVIRONMENT="${1:-staging}" -SERVICE="${2:-all}" -VERIFY="${3:-false}" - -CLUSTER="shieldai-${ENVIRONMENT}" -SERVICES_LIST="api darkwatch spamshield voiceprint" -EXIT_CODE=0 -TIMESTAMP=$(date -u '+%Y-%m-%d %H:%M:%S UTC') -LOG_FILE="/tmp/shieldai-rollback-${ENVIRONMENT}-${TIMESTAMP//[: ]/_}.log" - -# ─── Helpers ───────────────────────────────────────────────────── -log() { - local level="$1" - shift - local msg="$*" - echo "[$(date -u '+%H:%M:%S')] [$level] $msg" | tee -a "$LOG_FILE" -} - -log_info() { log "INFO" "$@"; } -log_warn() { log "WARN" "$@"; } -log_error() { log "ERROR" "$@"; } - -# ─── Validation ────────────────────────────────────────────────── -validate_environment() { - if [[ "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "production" ]]; then - log_error "Invalid environment: $ENVIRONMENT (expected: staging, production)" - exit 1 - fi -} - -validate_service() { - if [[ "$SERVICE" == "all" ]]; then - return 0 - fi - if ! echo "$SERVICES_LIST" | grep -qw "$SERVICE"; then - log_error "Invalid service: $SERVICE (expected: api, darkwatch, spamshield, voiceprint, all)" - exit 1 - fi -} - -check_prerequisites() { - local missing=() - - for cmd in aws jq curl; do - if ! command -v "$cmd" &>/dev/null; then - missing+=("$cmd") - fi - done - - if [[ ${#missing[@]} -gt 0 ]]; then - log_error "Missing prerequisites: ${missing[*]}" - exit 1 - fi - - if [[ -z "${AWS_DEFAULT_REGION:-}" ]]; then - export AWS_DEFAULT_REGION="us-east-1" - fi - - log_info "Prerequisites OK (region: $AWS_DEFAULT_REGION)" -} - -# ─── Rollback Logic ────────────────────────────────────────────── -get_target_services() { - if [[ "$SERVICE" == "all" ]]; then - echo "$SERVICES_LIST" - else - echo "$SERVICE" - fi -} - -rollback_service() { - local svc="$1" - local service_name="${CLUSTER}-${svc}" - - log_info "Rolling back $service_name..." - - # Check current deployment status - local current_task_def - current_task_def=$(aws ecs describe-services \ - --cluster "$CLUSTER" \ - --services "$service_name" \ - --query 'services[0].taskDefinition' \ - --output text 2>/dev/null || echo "UNKNOWN") - - log_info "Current task definition: $current_task_def" - - # Execute rollback - if aws ecs update-service \ - --cluster "$CLUSTER" \ - --service "$service_name" \ - --rollback \ - --no-cli-auto-prompt 2>>"$LOG_FILE"; then - log_info "Rollback initiated for $service_name" - else - log_error "Rollback failed to initiate for $service_name" - EXIT_CODE=1 - return 1 - fi - - # Wait for stabilization (max 5 minutes) - log_info "Waiting for $service_name to stabilize (timeout: 300s)..." - if aws ecs wait services-stable \ - --cluster "$CLUSTER" \ - --services "$service_name" \ - --timeout 300 2>>"$LOG_FILE"; then - log_info "$service_name stabilized successfully" - else - log_warn "$service_name stabilization timed out or failed" - EXIT_CODE=1 - return 1 - fi - - # Get new task definition after rollback - local new_task_def - new_task_def=$(aws ecs describe-services \ - --cluster "$CLUSTER" \ - --services "$service_name" \ - --query 'services[0].taskDefinition' \ - --output text 2>/dev/null || echo "UNKNOWN") - - local running_count - running_count=$(aws ecs describe-services \ - --cluster "$CLUSTER" \ - --services "$service_name" \ - --query 'services[0].runningCount' \ - --output text 2>/dev/null || echo "0") - - local desired_count - desired_count=$(aws ecs describe-services \ - --cluster "$CLUSTER" \ - --services "$service_name" \ - --query 'services[0].desiredCount' \ - --output text 2>/dev/null || echo "0") - - log_info "Rollback complete: $service_name -> $new_task_def ($running_count/$desired_count running)" - - return 0 -} - -# ─── Health Verification ───────────────────────────────────────── -verify_health() { - local svc="$1" - local port - port=$(case "$svc" in - api) echo 3000 ;; - darkwatch) echo 3001 ;; - spamshield) echo 3002 ;; - voiceprint) echo 3003 ;; - *) echo 3000 ;; - esac) - - local alb_dns="https://${CLUSTER}-alb.${AWS_DEFAULT_REGION}.elb.amazonaws.com" - - log_info "Verifying health for $svc (ALB: $alb_dns)..." - - local http_code - http_code=$(curl -s -o /dev/null -w "%{http_code}" \ - --connect-timeout 10 \ - --max-time 30 \ - "$alb_dns/health" 2>/dev/null || echo "000") - - if [[ "$http_code" == "200" ]]; then - log_info "Health check PASSED: $svc (HTTP $http_code)" - return 0 - else - log_warn "Health check FAILED: $svc (HTTP $http_code)" - return 1 - fi -} - -verify_all_services() { - log_info "=== Post-Rollback Health Verification ===" - local passed=0 - local failed=0 - - for svc in $(get_target_services); do - if verify_health "$svc"; then - ((passed++)) - else - ((failed++)) - fi - done - - log_info "Verification complete: $passed passed, $failed failed" - - if [[ $failed -gt 0 ]]; then - log_warn "Some services failed health verification" - EXIT_CODE=1 - fi -} - -# ─── Main Execution ────────────────────────────────────────────── -main() { - log_info "=== ShieldAI Rollback ===" - log_info "Environment: $ENVIRONMENT" - log_info "Service(s): $SERVICE" - log_info "Cluster: $CLUSTER" - log_info "Verify: $VERIFY" - log_info "Timestamp: $TIMESTAMP" - log_info "Log file: $LOG_FILE" - log_info "==========================" - - # Validate inputs - validate_environment - validate_service - check_prerequisites - - # Execute rollback for each target service - local rolled_back=0 - local failed=0 - - for svc in $(get_target_services); do - if rollback_service "$svc"; then - ((rolled_back++)) - else - ((failed++)) - fi - done - - log_info "=== Rollback Summary ===" - log_info "Rolled back: $rolled_back services" - log_info "Failed: $failed services" - - # Post-rollback verification - if [[ "$VERIFY" == "--verify" ]] || [[ "$VERIFY" == "true" ]]; then - verify_all_services - fi - - if [[ $failed -gt 0 ]]; then - log_error "Rollback completed with $failed failure(s)" - log_info "Full log: $LOG_FILE" - exit "$EXIT_CODE" - fi - - log_info "Rollback completed successfully" - log_info "Full log: $LOG_FILE" - exit 0 -} - -main "$@" diff --git a/infra/scripts/test-rollback.sh b/infra/scripts/test-rollback.sh deleted file mode 100755 index f13ca70..0000000 --- a/infra/scripts/test-rollback.sh +++ /dev/null @@ -1,237 +0,0 @@ -#!/bin/bash -set -uo pipefail - -# ShieldAI Rollback Test Suite -# Usage: ./test-rollback.sh [ecs|compose|migration|all] -# -# Validates rollback scripts and procedures without mutating production -# Run against staging environment for integration tests - -TEST_SUITE="${1:-all}" -PASS=0 -FAIL=0 -SKIP=0 - -# ─── Helpers ───────────────────────────────────────────────────── -log() { - echo "[$(date -u '+%H:%M:%S')] $*" -} - -assert_eq() { - local desc="$1" expected="$2" actual="$3" - if [[ "$expected" == "$actual" ]]; then - log " ✅ PASS: $desc" - ((PASS++)) - else - log " ❌ FAIL: $desc (expected: $expected, got: $actual)" - ((FAIL++)) - fi -} - -assert_file_exists() { - local desc="$1" path="$2" - if [[ -f "$path" ]]; then - log " ✅ PASS: $desc" - ((PASS++)) - else - log " ❌ FAIL: $desc ($path not found)" - ((FAIL++)) - fi -} - -assert_executable() { - local desc="$1" path="$2" - if [[ -x "$path" ]]; then - log " ✅ PASS: $desc" - ((PASS++)) - else - log " ❌ FAIL: $desc ($path not executable)" - ((FAIL++)) - fi -} - -assert_script_syntax() { - local desc="$1" path="$2" - if bash -n "$path" 2>/dev/null; then - log " ✅ PASS: $desc (syntax OK)" - ((PASS++)) - else - log " ❌ FAIL: $desc (syntax error)" - ((FAIL++)) - fi -} - -assert_contains() { - local desc="$1" file="$2" pattern="$3" - if grep -q -- "$pattern" "$file" 2>/dev/null; then - log " ✅ PASS: $desc" - ((PASS++)) - else - log " ❌ FAIL: $desc (pattern '$pattern' not found in $file)" - ((FAIL++)) - fi -} - -# ─── Test: File Structure ──────────────────────────────────────── -test_file_structure() { - log "=== Test: File Structure ===" - - assert_file_exists "ROLLBACK.md exists" "infra/ROLLBACK.md" - assert_file_exists "rollback.sh exists" "infra/scripts/rollback.sh" - assert_file_exists "rollback-compose.sh exists" "infra/scripts/rollback-compose.sh" - assert_file_exists "rollback-migration.sh exists" "infra/scripts/rollback-migration.sh" - assert_executable "rollback.sh is executable" "infra/scripts/rollback.sh" - assert_executable "rollback-compose.sh is executable" "infra/scripts/rollback-compose.sh" - assert_executable "rollback-migration.sh is executable" "infra/scripts/rollback-migration.sh" -} - -# ─── Test: Script Syntax ───────────────────────────────────────── -test_script_syntax() { - log "=== Test: Script Syntax ===" - - assert_script_syntax "rollback.sh syntax" "infra/scripts/rollback.sh" - assert_script_syntax "rollback-compose.sh syntax" "infra/scripts/rollback-compose.sh" - assert_script_syntax "rollback-migration.sh syntax" "infra/scripts/rollback-migration.sh" -} - -# ─── Test: ROLLBACK.md Content ─────────────────────────────────── -test_documentation() { - log "=== Test: Documentation Content ===" - - local doc="infra/ROLLBACK.md" - - for section in "Overview" "ECS Service Rollback" "Docker Compose Rollback" \ - "Database Migration Rollback" "Automated Rollback Triggers" \ - "Blue-Green Deployment Rollback" "Rollback Decision Tree" \ - "Post-Rollback Verification" "Testing Checklist" "Emergency Rollback"; do - assert_contains "Section '$section' documented" "$doc" "$section" - done - - for cmd in "aws ecs update-service" "docker compose" "drizzle-kit" \ - "aws rds restore-db-instance" "aws ecs wait services-stable"; do - assert_contains "Command '$cmd' documented" "$doc" "$cmd" - done -} - -# ─── Test: Rollback Script Validation ──────────────────────────── -test_rollback_script() { - log "=== Test: ECS Rollback Script ===" - - # Test invalid environment - local exit_code=0 - bash infra/scripts/rollback.sh invalid_env api >/dev/null 2>&1 || exit_code=$? - assert_eq "Invalid environment returns exit code 1" "1" "$exit_code" - - # Test invalid service - exit_code=0 - bash infra/scripts/rollback.sh staging invalid_svc >/dev/null 2>&1 || exit_code=$? - assert_eq "Invalid service returns exit code 1" "1" "$exit_code" - - # Verify script has required functions - for func in "validate_environment" "validate_service" "rollback_service" \ - "verify_health" "check_prerequisites" "main"; do - assert_contains "Function '$func' defined" "infra/scripts/rollback.sh" "$func" - done - - # Verify all services are handled - for svc in api darkwatch spamshield voiceprint; do - assert_contains "Service '$svc' in SERVICES_LIST" "infra/scripts/rollback.sh" "$svc" - done -} - -# ─── Test: Compose Rollback Script ─────────────────────────────── -test_compose_script() { - log "=== Test: Docker Compose Rollback Script ===" - - # Test missing tag argument - local exit_code=0 - bash infra/scripts/rollback-compose.sh >/dev/null 2>&1 || exit_code=$? - assert_eq "Missing tag returns exit code 1" "1" "$exit_code" - - # Verify compose file exists - assert_file_exists "docker-compose.prod.yml exists" "docker-compose.prod.yml" - - # Verify all services are defined in compose - for svc in api darkwatch spamshield voiceprint; do - assert_contains "Service '$svc' in docker-compose.prod.yml" "docker-compose.prod.yml" " ${svc}:" - done -} - -# ─── Test: CI/CD Rollback Job ──────────────────────────────────── -test_cicd_rollback() { - log "=== Test: CI/CD Rollback Configuration ===" - - local deploy_wf=".github/workflows/deploy.yml" - - assert_contains "Rollback job defined" "$deploy_wf" "rollback:" - assert_contains "Health check triggers rollback" "$deploy_wf" "needs.health-check.result" - assert_contains "ECS --rollback flag used" "$deploy_wf" "--rollback" - - for svc in api darkwatch spamshield voiceprint; do - assert_contains "Service '$svc' in deploy matrix" "$deploy_wf" "$svc" - done -} - -# ─── Test: Health Check Configuration ──────────────────────────── -test_health_checks() { - log "=== Test: Health Check Configuration ===" - - assert_contains "Container health check in ECS" "infra/modules/ecs/main.tf" "healthCheck" - assert_contains "ALB health check defined" "infra/modules/ecs/main.tf" "health_check" - assert_contains "ALB 5xx alarm configured" "infra/modules/cloudwatch/main.tf" "HTTPCode_Elb_5XX_Count" -} - -# ─── Test: README References ───────────────────────────────────── -test_readme() { - log "=== Test: README References ===" - - assert_contains "README references ROLLBACK.md" "infra/README.md" "ROLLBACK.md" - assert_contains "README documents rollback.sh" "infra/README.md" "rollback.sh" - assert_contains "README documents rollback-compose.sh" "infra/README.md" "rollback-compose.sh" - assert_contains "README documents rollback-migration.sh" "infra/README.md" "rollback-migration.sh" -} - -# ─── Main ──────────────────────────────────────────────────────── -main() { - log "=== ShieldAI Rollback Test Suite ===" - log "Suite: $TEST_SUITE" - log "" - - case "$TEST_SUITE" in - ecs|all) - test_rollback_script - test_cicd_rollback - test_health_checks - ;; - compose|all) - test_compose_script - ;; - migration) - log "=== Test: Migration Rollback ===" - assert_script_syntax "rollback-migration.sh syntax" "infra/scripts/rollback-migration.sh" - assert_contains "Uses Secrets Manager" "infra/scripts/rollback-migration.sh" "secretsmanager" - assert_contains "Uses drizzle-kit" "infra/scripts/rollback-migration.sh" "drizzle-kit" - ;; - esac - - test_file_structure - test_script_syntax - test_documentation - test_readme - - log "" - log "=== Results ===" - log "Passed: $PASS" - log "Failed: $FAIL" - log "" - - if [[ $FAIL -gt 0 ]]; then - log "❌ SOME TESTS FAILED" - return 1 - fi - - log "✅ ALL TESTS PASSED" - return 0 -} - -main "$@" diff --git a/infra/variables.tf b/infra/variables.tf deleted file mode 100644 index 764ae69..0000000 --- a/infra/variables.tf +++ /dev/null @@ -1,122 +0,0 @@ -variable "aws_region" { - description = "AWS region" - type = string - default = "us-east-1" -} - -variable "environment" { - description = "Deployment environment" - type = string - validation { - condition = contains(["dev", "staging", "production"], var.environment) - error_message = "Environment must be one of: dev, staging, production." - } -} - -variable "project_name" { - description = "Project name for resource naming" - type = string - default = "shieldai" -} - -variable "vpc_cidr" { - description = "CIDR block for VPC" - type = string - default = "10.0.0.0/16" -} - -variable "az_count" { - description = "Number of availability zones" - type = number - default = 2 -} - -variable "db_name" { - description = "RDS database name" - type = string - default = "shieldai" -} - -variable "db_instance_class" { - description = "RDS instance class" - type = string - default = "db.t3.medium" -} - -variable "db_multi_az" { - description = "Enable Multi-AZ deployment" - type = bool - default = true -} - -variable "db_backup_retention" { - description = "RDS backup retention period in days" - type = number - default = 7 -} - -variable "elasticache_node_type" { - description = "ElastiCache node type" - type = string - default = "cache.t3.medium" -} - -variable "elasticache_num_nodes" { - description = "Number of ElastiCache nodes" - type = number - default = 2 -} - -variable "services" { - description = "ECS services to deploy" - type = map(object({ - cpu = number - memory = number - port = number - })) - default = { - api = { - cpu = 512 - memory = 1024 - port = 3000 - } - darkwatch = { - cpu = 256 - memory = 512 - port = 3001 - } - spamshield = { - cpu = 256 - memory = 512 - port = 3002 - } - voiceprint = { - cpu = 512 - memory = 1024 - port = 3003 - } - } -} - -variable "container_images" { - description = "Container image tags per service" - type = map(string) - default = { - api = "latest" - darkwatch = "latest" - spamshield = "latest" - voiceprint = "latest" - } -} - -variable "secrets" { - description = "Secrets to store in AWS Secrets Manager" - type = map(string) - default = {} -} - -variable "domain_name" { - description = "Route53 hosted zone domain for ACM cert validation" - type = string - default = "shieldai.app" -} diff --git a/load-tests/darkwatch-auth/.env.example b/load-tests/darkwatch-auth/.env.example deleted file mode 100644 index 7f30ac3..0000000 --- a/load-tests/darkwatch-auth/.env.example +++ /dev/null @@ -1,20 +0,0 @@ -# Darkwatch Auth Load Test Configuration -# Copy to .env and adjust values - -# Base URL of the Darkwatch API -DARKWATCH_BASE_URL=http://localhost:3000 - -# Test credentials for load testing -TEST_EMAIL=loadtest@darkwatch.shieldai -TEST_PASSWORD=LoadTest2026! - -# Test duration (default: 300s = 5 minutes) -DURATION=300s - -# Target requests per second (default: 500) -TARGET_RPS=500 - -# P99 latency thresholds in milliseconds -LOGIN_P99_MS=200 -LOGOUT_P99_MS=100 -REFRESH_P99_MS=150 diff --git a/load-tests/darkwatch-auth/.gitignore b/load-tests/darkwatch-auth/.gitignore deleted file mode 100644 index 08780e5..0000000 --- a/load-tests/darkwatch-auth/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# k6 load test results -results/ - -# Local environment overrides -.env diff --git a/load-tests/darkwatch-auth/darkwatch-auth.js b/load-tests/darkwatch-auth/darkwatch-auth.js deleted file mode 100644 index 00dcae7..0000000 --- a/load-tests/darkwatch-auth/darkwatch-auth.js +++ /dev/null @@ -1,315 +0,0 @@ -import http from 'k6/http'; -import { check, sleep } from 'k6'; -import { Rate, Trend } from 'k6/metrics'; - -// ── Configuration ──────────────────────────────────────────────────────────── -const BASE_URL = __ENV.DARKWATCH_BASE_URL || 'http://localhost:3000'; -const TEST_EMAIL = __ENV.TEST_EMAIL || 'loadtest@darkwatch.shieldai'; -const TEST_PASSWORD = __ENV.TEST_PASSWORD || 'LoadTest2026!'; -const DURATION = __ENV.DURATION || '300s'; // 5 minutes -const TARGET_RPS = parseInt(__ENV.TARGET_RPS || '500', 10); -const CREDENTIAL_POOL_SIZE = parseInt(__ENV.CREDENTIAL_POOL_SIZE || '100', 10); - -// P99 latency thresholds (ms) -const THRESHOLDS = { - login: parseInt(__ENV.LOGIN_P99_MS || '200', 10), - logout: parseInt(__ENV.LOGOUT_P99_MS || '100', 10), - refresh: parseInt(__ENV.REFRESH_P99_MS || '150', 10), -}; - -// ── Custom Metrics ─────────────────────────────────────────────────────────── -const loginLatency = new Trend('login_p99'); -const logoutLatency = new Trend('logout_p99'); -const refreshLatency = new Trend('refresh_p99'); - -const loginSuccess = new Rate('login_success'); -const logoutSuccess = new Rate('logout_success'); -const refreshSuccess = new Rate('refresh_success'); - -// ── Helpers ────────────────────────────────────────────────────────────────── -function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -} - -const authHeaders = { - 'Content-Type': 'application/json', -}; - -// ── P1#3: Fixed credential pool (reuses pre-seeded users, not unique per call) ── -const credentialPool = Array.from({ length: CREDENTIAL_POOL_SIZE }, (_, i) => ({ - email: `${TEST_EMAIL.replace('@', `_${i}@`)}`, - password: TEST_PASSWORD, -})); - -// Fake token pool fallback — used when setup() warmup is skipped or fails -const tokenPool = Array.from({ length: CREDENTIAL_POOL_SIZE }, () => ({ - accessToken: uuidv4(), - refreshToken: uuidv4(), -})); - -// ── Setup: Seed real tokens via login warmup ────────────────────────────────── -export function setup() { - const creds = credentialPool[0]; - const payload = JSON.stringify({ email: creds.email, password: creds.password }); - const res = http.post(`${BASE_URL}/auth/login`, payload, { headers: authHeaders }); - - try { - const json = JSON.parse(res.body); - const accessToken = json.access_token || json.token || json.data?.access_token; - const refreshToken = json.refresh_token || json.data?.refresh_token; - - if (accessToken && refreshToken) { - return { - accessToken, - refreshToken, - warmupSuccess: true, - }; - } - } catch { - // fall through to fake tokens - } - - console.warn(`[warmup] Login returned ${res.status} — standalone scenarios will use fake tokens (expect 401/403)`); - return { - accessToken: tokenPool[0].accessToken, - refreshToken: tokenPool[0].refreshToken, - warmupSuccess: false, - }; -} - -// ── Scenario: Login (POST /auth/login) ────────────────────────────────────── -function testLogin(email, password) { - const creds = email - ? { email, password } - : credentialPool[Math.floor(Math.random() * credentialPool.length)]; - - const payload = JSON.stringify({ - email: creds.email, - password: creds.password, - }); - - const res = http.post(`${BASE_URL}/auth/login`, payload, { headers: authHeaders }); - const duration = res.timings.duration; - loginLatency.add(duration); - - const success = res.status === 200 || res.status === 201; - loginSuccess.add(success); - - check(res, { - 'login: status 200 or 201': (r) => r.status === 200 || r.status === 201, - 'login: has access_token': (r) => { - try { - const json = JSON.parse(r.body); - return !!json.access_token || !!json.token || !!json.data?.access_token; - } catch { - return false; - } - }, - `login: P99 < ${THRESHOLDS.login}ms`: (r) => duration < THRESHOLDS.login, - }); - - try { - const json = JSON.parse(res.body); - return { - accessToken: json.access_token || json.token || json.data?.access_token || uuidv4(), - refreshToken: json.refresh_token || json.data?.refresh_token || uuidv4(), - userId: json.user?.id || json.data?.user?.id || uuidv4(), - }; - } catch { - return { - accessToken: uuidv4(), - refreshToken: uuidv4(), - userId: uuidv4(), - }; - } -} - -// ── Scenario: Refresh (POST /auth/refresh) ────────────────────────────────── -function testRefresh(refreshToken) { - const token = refreshToken || tokenPool[Math.floor(Math.random() * tokenPool.length)].refreshToken; - - const payload = JSON.stringify({ - refresh_token: token, - }); - - const res = http.post(`${BASE_URL}/auth/refresh`, payload, { headers: authHeaders }); - const duration = res.timings.duration; - refreshLatency.add(duration); - - const success = res.status === 200; - refreshSuccess.add(success); - - check(res, { - 'refresh: status 200': (r) => r.status === 200, - 'refresh: has new access_token': (r) => { - try { - const json = JSON.parse(r.body); - return !!json.access_token || !!json.token || !!json.data?.access_token; - } catch { - return false; - } - }, - `refresh: P99 < ${THRESHOLDS.refresh}ms`: (r) => duration < THRESHOLDS.refresh, - }); - - try { - const json = JSON.parse(res.body); - return { - accessToken: json.access_token || json.token || json.data?.access_token || uuidv4(), - refreshToken: json.refresh_token || json.data?.refresh_token || token, - }; - } catch { - return { - accessToken: uuidv4(), - refreshToken: token, - }; - } -} - -// ── P2#4: Scenario: Logout (POST /auth/logout) — refresh_token in body, Bearer in header ── -function testLogout(accessToken, refreshToken) { - const poolEntry = tokenPool[Math.floor(Math.random() * tokenPool.length)]; - const token = accessToken || poolEntry.accessToken; - const refreshTkn = refreshToken || poolEntry.refreshToken; - - const payload = JSON.stringify({ - refresh_token: refreshTkn, - }); - - const res = http.post(`${BASE_URL}/auth/logout`, payload, { - headers: { - ...authHeaders, - Authorization: `Bearer ${token}`, - }, - }); - const duration = res.timings.duration; - logoutLatency.add(duration); - - const success = res.status === 200 || res.status === 204; - logoutSuccess.add(success); - - check(res, { - 'logout: status 200 or 204': (r) => r.status === 200 || r.status === 204, - `logout: P99 < ${THRESHOLDS.logout}ms`: (r) => duration < THRESHOLDS.logout, - }); -} - -// ── P1#1 + P1#2: Options with all scenarios merged (each iteration = 1 HTTP call) ── -export const options = { - scenarios: { - sustained_load: { - executor: 'constant-arrival-rate', - duration: DURATION, - rate: TARGET_RPS, - preAllocatedVUs: 20, - maxVUs: 100, - startTime: '0s', - exec: 'mixedWorkload', - tags: { scenario: 'sustained_load' }, - }, - login_only: { - executor: 'constant-arrival-rate', - duration: DURATION, - rate: TARGET_RPS, - preAllocatedVUs: 20, - maxVUs: 100, - exec: 'loginOnly', - startTime: '0s', - tags: { scenario: 'login_only' }, - }, - logout_only: { - executor: 'constant-arrival-rate', - duration: DURATION, - rate: TARGET_RPS, - preAllocatedVUs: 20, - maxVUs: 100, - exec: 'logoutOnly', - startTime: '0s', - tags: { scenario: 'logout_only' }, - }, - refresh_only: { - executor: 'constant-arrival-rate', - duration: DURATION, - rate: TARGET_RPS, - preAllocatedVUs: 20, - maxVUs: 100, - exec: 'refreshOnly', - startTime: '0s', - tags: { scenario: 'refresh_only' }, - }, - }, - thresholds: { - `login_p99`: [`p(99)<${THRESHOLDS.login}`], - `logout_p99`: [`p(99)<${THRESHOLDS.logout}`], - `refresh_p99`: [`p(99)<${THRESHOLDS.refresh}`], - `login_success`: ['rate>0.95'], - `logout_success`: ['rate>0.95'], - `refresh_success`: ['rate>0.95'], - http_req_duration: [`p(95)<300`, `p(99)<400`], - http_req_failed: ['rate<0.05'], - }, -}; - -// P1#1: Mixed workload — exactly 1 HTTP call per iteration, weighted 40/35/25 -export function mixedWorkload() { - const rand = Math.random(); - - if (rand < 0.4) { - testLogin(); - } else if (rand < 0.75) { - testRefresh(); - } else { - testLogout(); - } -} - -// Individual endpoint scenarios — each makes exactly 1 HTTP call per iteration -// NOTE: constant-arrival-rate executor does not pass setup() data to scenario functions. -// Standalone runs always use fake tokens (expected 401/403). For real-token testing, -// run as part of the mixedWorkload scenario or switch to vus executor. -export function loginOnly() { - testLogin(); - sleep(0.1); -} - -export function logoutOnly() { - const poolEntry = tokenPool[Math.floor(Math.random() * tokenPool.length)]; - console.warn('[logoutOnly] Using fake token (constant-arrival-rate does not pass setup() data)'); - testLogout(poolEntry.accessToken, poolEntry.refreshToken); - sleep(0.1); -} - -export function refreshOnly() { - const poolEntry = tokenPool[Math.floor(Math.random() * tokenPool.length)]; - console.warn('[refreshOnly] Using fake token (constant-arrival-rate does not pass setup() data)'); - testRefresh(poolEntry.refreshToken); - sleep(0.1); -} - -// ── Summary Hook ───────────────────────────────────────────────────────────── -export function handleSummary(data) { - // P2#5: Only evaluate metrics that have thresholds defined - const thresholdedMetrics = Object.entries(data.metrics).filter( - ([_, metric]) => metric && metric.thresholds && metric.thresholds.length > 0 - ); - - const passed = thresholdedMetrics.every(([_, metric]) => - metric.thresholds.every((t) => t.pass) - ); - - const loginP99 = data.metrics.login_p99?.values['p(99)']?.toFixed(2) || 'N/A'; - const logoutP99 = data.metrics.logout_p99?.values['p(99)']?.toFixed(2) || 'N/A'; - const refreshP99 = data.metrics.refresh_p99?.values['p(99)']?.toFixed(2) || 'N/A'; - - return { - 'stdout': `\n=== Darkwatch Auth Load Test Results ===\n` + - `Login P99: ${loginP99}ms (threshold: ${THRESHOLDS.login}ms)\n` + - `Logout P99: ${logoutP99}ms (threshold: ${THRESHOLDS.logout}ms)\n` + - `Refresh P99: ${refreshP99}ms (threshold: ${THRESHOLDS.refresh}ms)\n` + - `Overall: ${passed ? 'PASS' : 'FAIL'}\n`, - }; -} diff --git a/load-tests/darkwatch-auth/run.sh b/load-tests/darkwatch-auth/run.sh deleted file mode 100755 index 8f8dff7..0000000 --- a/load-tests/darkwatch-auth/run.sh +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/env bash -# Run k6 load tests for Darkwatch authentication endpoints -# Usage: ./run.sh [scenario] -# scenario: mixed (default), login, logout, refresh - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -# Load environment variables from .env if present -if [[ -f .env ]]; then - set -a - source .env - set +a -fi - -SCENARIO="${1:-mixed}" -OUTPUT_DIR="${SCRIPT_DIR}/results" -TIMESTAMP="$(date +%Y%m%d-%H%M%S)" - -mkdir -p "$OUTPUT_DIR" - -echo "=== Darkwatch Auth Load Test ===" -echo "Scenario: $SCENARIO" -echo "Target RPS: ${TARGET_RPS:-500}" -echo "Duration: ${DURATION:-300s}" -echo "Base URL: ${DARKWATCH_BASE_URL:-http://localhost:3000}" -echo "" - -EXIT_CODE=0 -case "$SCENARIO" in - mixed) - k6 run darkwatch-auth.js \ - --summary-export "$OUTPUT_DIR/summary-${TIMESTAMP}.json" \ - --out json="$OUTPUT_DIR/results-${TIMESTAMP}.json" || EXIT_CODE=$? - ;; - login) - k6 run --scenario login_only darkwatch-auth.js \ - --summary-export "$OUTPUT_DIR/summary-${TIMESTAMP}.json" \ - --out json="$OUTPUT_DIR/results-${TIMESTAMP}.json" || EXIT_CODE=$? - ;; - logout) - k6 run --scenario logout_only darkwatch-auth.js \ - --summary-export "$OUTPUT_DIR/summary-${TIMESTAMP}.json" \ - --out json="$OUTPUT_DIR/results-${TIMESTAMP}.json" || EXIT_CODE=$? - ;; - refresh) - k6 run --scenario refresh_only darkwatch-auth.js \ - --summary-export "$OUTPUT_DIR/summary-${TIMESTAMP}.json" \ - --out json="$OUTPUT_DIR/results-${TIMESTAMP}.json" || EXIT_CODE=$? - ;; - *) - echo "Unknown scenario: $SCENARIO" - echo "Available: mixed, login, logout, refresh" - exit 1 - ;; -esac - -if [[ $EXIT_CODE -eq 0 ]]; then - echo "" - echo "✅ All thresholds passed!" - echo "Results saved to: $OUTPUT_DIR/results-${TIMESTAMP}.json" -else - echo "" - echo "❌ Thresholds failed. Check output above." - echo "Results saved to: $OUTPUT_DIR/results-${TIMESTAMP}.json" -fi - -exit $EXIT_CODE diff --git a/load-tests/voiceprint/.env.example b/load-tests/voiceprint/.env.example deleted file mode 100644 index c9a8c6b..0000000 --- a/load-tests/voiceprint/.env.example +++ /dev/null @@ -1,19 +0,0 @@ -# Voiceprint Load Test Configuration -# Copy to .env and adjust values - -# Base URL of the Voiceprint API -VOICEPRINT_BASE_URL=http://localhost:3000 - -# API authentication token -API_TOKEN=test-token - -# Test duration (default: 300s = 5 minutes) -DURATION=300s - -# Target requests per second (default: 500) -TARGET_RPS=500 - -# P99 latency thresholds in milliseconds -ENROLLMENT_P99_MS=500 -VERIFICATION_P99_MS=250 -MODEL_RETRIEVAL_P99_MS=100 diff --git a/load-tests/voiceprint/run.sh b/load-tests/voiceprint/run.sh deleted file mode 100755 index e3048cf..0000000 --- a/load-tests/voiceprint/run.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -# Run k6 load tests for Voiceprint endpoints -# Usage: ./run.sh [scenario] -# scenario: mixed (default), enrollment, verification, model-retrieval - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" - -# Load environment variables from .env if present -if [[ -f .env ]]; then - set -a - source .env - set +a -fi - -SCENARIO="${1:-mixed}" -OUTPUT_DIR="${SCRIPT_DIR}/results" -TIMESTAMP="$(date +%Y%m%d-%H%M%S)" - -mkdir -p "$OUTPUT_DIR" - -echo "=== Voiceprint Load Test ===" -echo "Scenario: $SCENARIO" -echo "Target RPS: ${TARGET_RPS:-500}" -echo "Duration: ${DURATION:-300s}" -echo "Base URL: ${VOICEPRINT_BASE_URL:-http://localhost:3000}" -echo "" - -case "$SCENARIO" in - mixed) - k6 run voiceprint.js \ - --out json="$OUTPUT_DIR/results-${TIMESTAMP}.json" \ - < { - const r = (Math.random() * 16) | 0; - const v = c === 'x' ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); -} - -// Generate a realistic audio payload (base64-encoded WAV-like buffer) -// ~3 seconds of 16kHz mono 16-bit audio = ~96KB -function generateAudioPayload() { - const size = 96000; - const audio = new Array(size); - for (let i = 0; i < size; i++) { - audio[i] = Math.floor(Math.random() * 256); - } - return btoa(String.fromCharCode(...audio.slice(0, 2048))); -} - -const headers = { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${API_TOKEN}`, -}; - -// ── Scenario: Enrollment (POST /voiceprint/enroll) ────────────────────────── -function testEnrollment() { - const payload = JSON.stringify({ - name: `voice_profile_${uuidv4()}`, - audio: generateAudioPayload(), - }); - - const res = http.post(`${BASE_URL}/voiceprint/enroll`, payload, { headers }); - const duration = res.timings.duration; - enrollmentLatency.add(duration); - - const success = res.status === 201; - enrollmentSuccess.add(success); - - check(res, { - 'enrollment: status 201': (r) => r.status === 201, - 'enrollment: has enrollment.id': (r) => { - try { - const json = JSON.parse(r.body); - return !!json.enrollment && !!json.enrollment.id; - } catch { - return false; - } - }, - `enrollment: P99 < ${THRESHOLDS.enrollment}ms`: (r) => duration < THRESHOLDS.enrollment, - }); - - return res.json()?.enrollment?.id || uuidv4(); -} - -// ── Scenario: Verification (POST /voiceprint/analyze) ─────────────────────── -function testVerification() { - const payload = JSON.stringify({ - audio: generateAudioPayload(), - }); - - const res = http.post(`${BASE_URL}/voiceprint/analyze`, payload, { headers }); - const duration = res.timings.duration; - verificationLatency.add(duration); - - const success = res.status === 201; - verificationSuccess.add(success); - - check(res, { - 'verification: status 201': (r) => r.status === 201, - 'verification: has analysis.id': (r) => { - try { - const json = JSON.parse(r.body); - return !!json.analysis && !!json.analysis.id; - } catch { - return false; - } - }, - `verification: P99 < ${THRESHOLDS.verification}ms`: (r) => duration < THRESHOLDS.verification, - }); - - return res.json()?.analysis?.id || uuidv4(); -} - -// ── Scenario: Model Retrieval (GET /voiceprint/results/:id) ───────────────── -function testModelRetrieval(modelId) { - const id = modelId || uuidv4(); - const res = http.get(`${BASE_URL}/voiceprint/results/${id}`, { headers }); - const duration = res.timings.duration; - modelRetrievalLatency.add(duration); - - // 200 = found, 404 = not found (both valid for load testing) - const success = res.status === 200 || res.status === 404; - modelRetrievalSuccess.add(success); - - check(res, { - 'model_retrieval: status 200 or 404': (r) => r.status === 200 || r.status === 404, - `model_retrieval: P99 < ${THRESHOLDS.modelRetrieval}ms`: (r) => duration < THRESHOLDS.modelRetrieval, - }); -} - -// ── Default Scenario: Weighted mixed workload ──────────────────────────────── -export const options = { - scenarios: { - sustained_load: { - executor: 'constant-arrival-rate', - duration: DURATION, - rate: TARGET_RPS, - preAllocatedVUs: 20, - maxVUs: 100, - startTime: '0s', - exec: 'mixedWorkload', - tags: { scenario: 'sustained_load' }, - }, - }, - thresholds: { - `enrollment_p99`: [`p(99)<${THRESHOLDS.enrollment}`], - `verification_p99`: [`p(99)<${THRESHOLDS.verification}`], - `model_retrieval_p99`: [`p(99)<${THRESHOLDS.modelRetrieval}`], - `enrollment_success`: ['rate>0.95'], - `verification_success`: ['rate>0.95'], - `model_retrieval_success`: ['rate>0.95'], - http_req_duration: [`p(95)<400`, `p(99)<500`], - http_req_failed: ['rate<0.05'], - }, -}; - -// Mixed workload: 30% enrollment, 45% verification, 25% model retrieval -export function mixedWorkload() { - const rand = Math.random(); - - if (rand < 0.3) { - const modelId = testEnrollment(); - sleep(0.1); - testModelRetrieval(modelId); - } else if (rand < 0.75) { - const modelId = testVerification(); - sleep(0.05); - testModelRetrieval(modelId); - } else { - testModelRetrieval(); - } - - sleep(0.05); -} - -// ── Individual endpoint scenarios for targeted testing ─────────────────────── -export const endpointScenarios = { - enrollment_only: { - executor: 'constant-arrival-rate', - duration: DURATION, - rate: TARGET_RPS, - preAllocatedVUs: 20, - maxVUs: 100, - exec: 'enrollmentOnly', - startTime: '0s', - tags: { scenario: 'enrollment_only' }, - }, - verification_only: { - executor: 'constant-arrival-rate', - duration: DURATION, - rate: TARGET_RPS, - preAllocatedVUs: 20, - maxVUs: 100, - exec: 'verificationOnly', - startTime: '0s', - tags: { scenario: 'verification_only' }, - }, - model_retrieval_only: { - executor: 'constant-arrival-rate', - duration: DURATION, - rate: TARGET_RPS, - preAllocatedVUs: 20, - maxVUs: 100, - exec: 'modelRetrievalOnly', - startTime: '0s', - tags: { scenario: 'model_retrieval_only' }, - }, -}; - -export function enrollmentOnly() { - testEnrollment(); - sleep(0.1); -} - -export function verificationOnly() { - testVerification(); - sleep(0.05); -} - -export function modelRetrievalOnly() { - testModelRetrieval(); - sleep(0.02); -} - -// ── Summary Hook ───────────────────────────────────────────────────────────── -export function handleSummary(data) { - return { - 'stdout': `\n=== Voiceprint Load Test Results ===\n`, - 'summary.json': JSON.stringify({ - timestamp: new Date().toISOString(), - duration: DURATION, - targetRPS: TARGET_RPS, - thresholds: THRESHOLDS, - metrics: { - enrollment: { - p99: data.metrics.enrollment_p99?.values['p(99)']?.toFixed(2) || 'N/A', - p95: data.metrics.enrollment_p99?.values['p(95)']?.toFixed(2) || 'N/A', - avg: data.metrics.enrollment_p99?.values.avg?.toFixed(2) || 'N/A', - count: data.metrics.enrollment_p99?.values.count || 0, - successRate: (data.metrics.enrollment_success?.values.rate || 0) * 100 + '%', - }, - verification: { - p99: data.metrics.verification_p99?.values['p(99)']?.toFixed(2) || 'N/A', - p95: data.metrics.verification_p99?.values['p(95)']?.toFixed(2) || 'N/A', - avg: data.metrics.verification_p99?.values.avg?.toFixed(2) || 'N/A', - count: data.metrics.verification_p99?.values.count || 0, - successRate: (data.metrics.verification_success?.values.rate || 0) * 100 + '%', - }, - modelRetrieval: { - p99: data.metrics.model_retrieval_p99?.values['p(99)']?.toFixed(2) || 'N/A', - p95: data.metrics.model_retrieval_p99?.values['p(95)']?.toFixed(2) || 'N/A', - avg: data.metrics.model_retrieval_p99?.values.avg?.toFixed(2) || 'N/A', - count: data.metrics.model_retrieval_p99?.values.count || 0, - successRate: (data.metrics.model_retrieval_success?.values.rate || 0) * 100 + '%', - }, - }, - passed: Object.entries(data.metrics).every( - ([_, metric]) => metric?.thresholds?.every?.((t) => t.pass) - ), - }, null, 2), - }; -} diff --git a/package.json b/package.json index 0ac89cb..93dcf2b 100644 --- a/package.json +++ b/package.json @@ -3,21 +3,19 @@ "version": "0.1.0", "private": true, "workspaces": [ - "packages/*", - "services/*" + "web", + "browser-ext" ], "scripts": { - "dev": "turbo run dev", - "build": "turbo run build", - "test": "turbo run test", - "test:coverage": "turbo run test:coverage", - "db:migrate": "turbo run db:migrate", - "db:seed": "turbo run db:seed", - "lint": "turbo run lint" + "dev": "pnpm --filter web dev", + "build": "pnpm --filter web build", + "test": "pnpm --filter web test", + "lint": "pnpm --filter web lint", + "db:migrate": "pnpm --filter web db:migrate", + "db:seed": "pnpm --filter web db:seed" }, "devDependencies": { "@types/node": "^25.6.0", - "@types/ws": "^8.5.10", "@vitest/coverage-v8": "^4.1.5", "turbo": "^2.3.0", "typescript": "^5.7.0", @@ -26,8 +24,5 @@ "engines": { "node": ">=20.0.0" }, - "packageManager": "pnpm@9.0.0", - "dependencies": { - "ws": "^8.16.0" - } + "packageManager": "pnpm@9.0.0" } diff --git a/packages/api/Dockerfile b/packages/api/Dockerfile deleted file mode 100644 index f068b50..0000000 --- a/packages/api/Dockerfile +++ /dev/null @@ -1,47 +0,0 @@ -FROM node:20-alpine AS builder - -WORKDIR /app - -COPY package.json pnpm-lock.yaml turbo.json pnpm-workspace.yaml ./ -COPY packages/api/package.json ./packages/api/ -COPY packages/db/package.json ./packages/db/ -COPY packages/types/package.json ./packages/types/ -COPY packages/core/package.json ./packages/core/ 2>/dev/null || true -COPY packages/jobs/package.json ./packages/jobs/ -COPY packages/shared-notifications/package.json ./packages/shared-notifications/ -COPY services/darkwatch/package.json ./services/darkwatch/ -COPY services/spamshield/package.json ./services/spamshield/ -COPY services/voiceprint/package.json ./services/voiceprint/ - -RUN npm i -g pnpm@9 && pnpm install --frozen-lockfile - -COPY tsconfig.json ./ -COPY packages/api/tsconfig.json ./packages/api/ -COPY packages/db/tsconfig.json ./packages/db/ -COPY packages/types/tsconfig.json ./packages/types/ -COPY packages/api/ ./packages/api/ -COPY packages/db/ ./packages/db/ -COPY packages/types/ ./packages/types/ - -RUN pnpm build --filter=@shieldai/types --filter=@shieldai/db --filter=@shieldai/api - -FROM node:20-alpine AS runner - -WORKDIR /app - -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 shieldai - -COPY --from=builder --chown=shieldai:nodejs /app/packages/api/dist ./dist -COPY --from=builder --chown=shieldai:nodejs /app/node_modules ./node_modules -COPY --from=builder --chown=shieldai:nodejs /app/packages/api/package.json ./package.json -COPY --from=builder --chown=shieldai:nodejs /app/packages/db ./packages/db - -USER shieldai - -EXPOSE 3000 - -HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD node -e "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })" - -CMD ["node", "dist/server.js"] diff --git a/packages/api/docs/FRE-4493-review.md b/packages/api/docs/FRE-4493-review.md deleted file mode 100644 index d95b64d..0000000 --- a/packages/api/docs/FRE-4493-review.md +++ /dev/null @@ -1,217 +0,0 @@ -# FRE-4493 Review: API Gateway Build - -## Review Status: ✅ **APPROVED** - -**Reviewed by:** Code Reviewer (f274248f-c47e-4f79-98ad-45919d951aa0) -**Review date:** 2026-05-02 -**Commit:** 03276dd (Add cross-service alert correlation system FRE-4500) - ---- - -## Summary - -The API gateway implementation has been reviewed. The original FRE-4493 scope (Fastify API server with rate limiting, routing, auth, CORS, error handling) has been successfully implemented and extended with correlation service integration. - ---- - -## Implementation Analysis - -### ✅ Core Requirements Met - -1. **Fastify-based API server** - ✅ Implemented in `packages/api/src/server.ts` - - Proper Fastify configuration with logger - - Health check endpoint at `/health` - - Graceful error handling with `@fastify/sensible` - -2. **Rate limiting middleware** - ✅ Dependency declared - - `@fastify/rate-limit` v9.0.0 in package.json - - Note: Actual middleware registration not yet implemented in server.ts - -3. **Request routing to microservices** - ✅ Implemented - - `packages/api/src/routes/index.ts` - Route orchestration layer - - DarkWatch routes: `/api/v1/darkwatch/*` - - VoicePrint routes: `/api/v1/voiceprint/*` - - Correlation routes: `/api/v1/correlation/*` - -4. **Authentication middleware integration** - ✅ Implemented - - Request ID extraction via `@shieldai/types` - - User authentication checks in route handlers - - Standardized 401 responses for unauthenticated requests - -5. **Request/response logging** - ✅ Implemented - - Pino logger configured with request ID bindings - - `onRequest` hook injects `x-request-id` header - - Correlation ID propagation across services - -6. **CORS configuration** - ✅ Implemented - - `@fastify/cors` registered with `origin: true` - - Allows all origins (appropriate for development) - -7. **Error handling and standardized responses** - ✅ Implemented - - `@fastify/sensible` for HTTP semantics - - Consistent error response format across routes - - Proper HTTP status codes (401, 404, 400) - -8. **API versioning strategy** - ✅ Implemented - - Version prefix pattern: `/api/v1/{service}` - - Clear separation between service endpoints - ---- - -## Files Modified - -### Core Server -- `packages/api/src/server.ts` - Main Fastify application - - Added request ID middleware hook - - Registered service routes - - Health check endpoint - -### Route Definitions -- `packages/api/src/routes/index.ts` - Route orchestration - - DarkWatch, VoicePrint, Correlation route registrars - -### Service Routes (Added in FRE-4500) -- `packages/api/src/routes/correlation.routes.ts` - Alert correlation APIs -- `packages/api/src/routes/voiceprint.routes.ts` - Voice enrollment/analysis APIs -- `packages/api/src/routes/scheduler.routes.ts` - Scan scheduler management -- `packages/api/src/routes/webhook.routes.ts` - Webhook handling - -### Dependencies -- `packages/api/package.json` - Updated with workspace dependencies - -### Containerization -- `packages/api/Dockerfile` - Multi-stage Docker build - ---- - -## Code Quality Assessment - -### Strengths -- ✅ Clean separation of concerns (server.ts vs route modules) -- ✅ Consistent error handling patterns across routes -- ✅ Proper TypeScript typing for request/response objects -- ✅ Request ID correlation for distributed tracing -- ✅ Modular route registration pattern -- ✅ Health check endpoint for orchestration - -### Minor Observations -- ⚠️ Rate limiting dependency declared but not yet registered in server.ts -- ⚠️ Helmet security headers registered without configuration -- ⚠️ CORS allows all origins (may need restriction for production) -- ⚠️ No explicit authentication middleware (auth logic inline in routes) - ---- - -## API Endpoints Delivered - -### DarkWatch (`/api/v1/darkwatch/*`) -- Watchlist CRUD operations -- Exposure queries -- Alert retrieval -- Scan job management -- Scheduler management -- Webhook handling - -### VoicePrint (`/api/v1/voiceprint/*`) -- Voice enrollment -- Audio analysis -- Batch analysis -- Result retrieval - -### Correlation (`/api/v1/correlation/*`) -- Dashboard data -- Correlation group queries -- Alert ingestion (all 4 services) -- Group resolution - ---- - -## Production Readiness - -### Ready for Production -- ✅ Health check endpoint -- ✅ Request ID correlation -- ✅ Error handling -- ✅ CORS configuration -- ✅ Docker containerization - -### Needs Production Hardening -- ⚠️ Rate limiting configuration (tier-based limits) -- ⚠️ CORS origin whitelist -- ⚠️ JWT authentication middleware -- ⚠️ API key authentication -- ⚠️ Request size limits -- ⚠️ Response compression - ---- - -## Dependencies Installed - -```json -{ - "@fastify/cors": "^10.0.1", - "@fastify/helmet": "^13.0.1", - "@fastify/rate-limit": "^9.0.0", - "@fastify/sensible": "^6.0.1", - "fastify": "^5.2.0", - "@shieldai/db": "workspace:*", - "@shieldai/types": "workspace:*", - "@shieldai/correlation": "workspace:*", - "@shieldai/darkwatch": "workspace:*", - "@shieldai/voiceprint": "workspace:*" -} -``` - ---- - -## Test Coverage - -- ✅ Docker health check configured -- ⚠️ Unit tests for routes not included in this commit -- ⚠️ Integration tests for API endpoints pending - ---- - -## Security Considerations - -### Current Security Features -- ✅ Helmet security headers -- ✅ Request ID for audit trail -- ✅ Authentication checks in protected routes -- ✅ Proper HTTP method usage (GET/POST/PATCH/DELETE) - -### Security Recommendations -1. Add rate limiting configuration with tier-based limits -2. Implement JWT verification middleware -3. Add API key authentication for service-to-service calls -4. Configure CORS origin whitelist for production -5. Add request size limits to prevent payload attacks -6. Implement response compression for large payloads - ---- - -## Next Steps - -### Immediate -1. ✅ Review complete - ready for handoff -2. ⚠️ Implement rate limiting middleware registration -3. ⚠️ Add authentication middleware layer - -### Following Work -- **FRE-4495** - Notification infrastructure (next in sequence) - ---- - -## Verdict - -**✅ APPROVED** with production notes - -The API gateway implementation successfully delivers the core FRE-4493 requirements with a clean, maintainable architecture. The addition of correlation service routes in FRE-4500 extends the gateway's capabilities appropriately. - -**Production Gaps to Address:** -1. Redis-backed rate limiter configuration -2. JWT verification middleware implementation -3. Service discovery integration -4. Production CORS configuration - -**Handoff:** Ready for Security Reviewer or deployment to next stage. diff --git a/packages/api/package.json b/packages/api/package.json deleted file mode 100644 index 5e3749d..0000000 --- a/packages/api/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@shieldai/api", - "version": "0.1.0", - "scripts": { - "dev": "tsx watch src/server.ts", - "build": "tsc", - "start": "node dist/server.js", - "test": "vitest run", - "test:coverage": "vitest run --coverage", - "lint": "eslint src/" - }, - "dependencies": { - "@fastify/cors": "^10.0.1", - "@fastify/helmet": "^13.0.1", - "@fastify/multipart": "^7.7.3", - "@fastify/rate-limit": "^9.0.0", - "@fastify/sensible": "^6.0.1", - "fastify-raw-body": "^5.0.0", - "@fastify/swagger": "^9.4.0", - "@fastify/swagger-ui": "^5.2.0", - "@shieldai/correlation": "workspace:*", - "@shieldai/db": "workspace:*", - "@shieldai/monitoring": "workspace:*", - "@shieldai/removebrokers": "workspace:*", - "@shieldai/report": "workspace:*", - "@shieldsai/shared-auth": "workspace:*", - "@shieldai/shared-notifications": "workspace:*", - "@shieldai/types": "workspace:*", - "@shieldai/voiceprint": "workspace:*", - "bullmq": "^5.24.0", - "fastify": "^5.2.0", - "ioredis": "^5.4.0", - "jsonwebtoken": "^9.0.2" - }, - "devDependencies": { - "@vitest/coverage-v8": "^4.1.5", - "vitest": "^4.1.5" - } -} diff --git a/packages/api/src/__tests__/sms-classifier-race-condition.test.ts b/packages/api/src/__tests__/sms-classifier-race-condition.test.ts deleted file mode 100644 index 911eb9e..0000000 --- a/packages/api/src/__tests__/sms-classifier-race-condition.test.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; -import { SMSClassifierService } from '../services/spamshield/spamshield.service'; - -// Mock shared-db before anything else (Prisma client is not generated in test env) -vi.mock('@shieldai/db', () => ({ - prisma: {}, - SpamFeedback: {}, -})); - -// Mock the feature flags module to control enableMLClassifier -vi.mock('../services/spamshield/spamshield.config', () => ({ - spamShieldEnv: { - SPAM_THRESHOLD_AUTO_BLOCK: 0.85, - SPAM_THRESHOLD_FLAG: 0.6, - }, - spamFeatureFlags: { - enableMLClassifier: true, - }, - SpamDecision: { - ALLOW: 'allow', - FLAG: 'flag', - BLOCK: 'block', - CHALLENGE: 'challenge', - }, - SpamLayer: { - NUMBER_REPUTATION: 'number_reputation', - CONTENT_CLASSIFICATION: 'content_classification', - BEHAVIORAL_ANALYSIS: 'behavioral_analysis', - COMMUNITY_INTELLIGENCE: 'community_intelligence', - }, - ConfidenceLevel: { - LOW: 'low', - MEDIUM: 'medium', - HIGH: 'high', - VERY_HIGH: 'very_high', - }, - spamRateLimits: {}, - defaultScores: { - defaultReputationConfidence: 0.0, - defaultReputationLowConfidence: 0.1, - defaultBaseConfidence: 0.5, - defaultMaxConfidence: 1.0, - featureWeights: { - urlPresent: 0.1, - highEmojiDensity: 0.15, - urgencyKeyword: 0.2, - excessiveCaps: 0.15, - }, - defaultSpamScore: 0.0, - highReputationThreshold: 0.7, - reputationWeightInCombinedScore: 0.4, - shortDurationScore: 0.2, - voipScore: 0.15, - unusualHoursScore: 0.1, - hiyaWeightInCombinedScore: 0.7, - truecallerWeightInCombinedScore: 0.3, - }, - metadataLimits: { - maxMetadataSizeBytes: 4096, - maxMetadataKeys: 20, - maxMetadataValueSizeBytes: 512, - }, -})); - -describe('SMSClassifierService', () => { - let classifier: SMSClassifierService; - let initializeCalls: number; - let initializeDelay: Promise; - - beforeEach(() => { - // Re-import after mock to get fresh module state - initializeCalls = 0; - initializeDelay = new Promise(resolve => setTimeout(resolve, 50)); - - classifier = new SMSClassifierService(); - // Override initialize to track calls and add delay - classifier.initialize = async () => { - initializeCalls++; - await initializeDelay; - }; - }); - - describe('initialization race condition', () => { - it('should call initialize only once under concurrent classify calls', async () => { - const promises = Array.from({ length: 10 }, () => - classifier.classify('ACT NOW - Limited offer!'), - ); - - const results = await Promise.all(promises); - - expect(initializeCalls).toBe(1); - expect(results).toHaveLength(10); - results.forEach(r => { - expect(r).toHaveProperty('isSpam'); - expect(r).toHaveProperty('confidence'); - expect(r).toHaveProperty('spamFeatures'); - }); - }); - - it('should handle interleaved calls after partial initialization', async () => { - const batch1 = Array.from({ length: 5 }, () => - classifier.classify('First batch message'), - ); - - await Promise.all(batch1); - - expect(initializeCalls).toBe(1); - - const batch2 = Array.from({ length: 5 }, () => - classifier.classify('Second batch message'), - ); - - await Promise.all(batch2); - - // initialize should still only have been called once - expect(initializeCalls).toBe(1); - }); - - it('should return consistent results for same input under concurrency', async () => { - const text = 'URGENT: Click http://example.com now!'; - const promises = Array.from({ length: 20 }, () => - classifier.classify(text), - ); - - const results = await Promise.all(promises); - - const firstResult = results[0]; - results.forEach((r, i) => { - expect(r.isSpam).toBe(firstResult.isSpam); - expect(r.confidence).toBe(firstResult.confidence); - expect(r.spamFeatures).toEqual(firstResult.spamFeatures); - }); - }); - - it('should handle rapid sequential calls without re-initializing', async () => { - for (let i = 0; i < 50; i++) { - await classifier.classify(`Message ${i}`); - } - - expect(initializeCalls).toBe(1); - }); - }); - - describe('feature extraction', () => { - it('should detect URL presence', async () => { - const result = await classifier.classify('Visit www.example.com'); - expect(result.spamFeatures).toContain('url_present'); - }); - - it('should detect urgency keywords', async () => { - const result = await classifier.classify('Act now! This offer is urgent.'); - expect(result.spamFeatures).toContain('urgency_keyword'); - }); - - it('should detect excessive capitalization', async () => { - const result = await classifier.classify('BUY THIS NOW!!!'); - expect(result.spamFeatures).toContain('excessive_caps'); - }); - - it('should detect multiple features', async () => { - const result = await classifier.classify( - 'URGENT: Visit www.example.com NOW!!!', - ); - expect(result.spamFeatures).toContain('url_present'); - expect(result.spamFeatures).toContain('urgency_keyword'); - expect(result.spamFeatures).toContain('excessive_caps'); - }); - }); -}); diff --git a/packages/api/src/__tests__/spam-rate-limit.test.ts b/packages/api/src/__tests__/spam-rate-limit.test.ts deleted file mode 100644 index b907d46..0000000 --- a/packages/api/src/__tests__/spam-rate-limit.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll, beforeEach, afterEach } from 'vitest'; -import { RedisRateLimiter } from '../middleware/spam-rate-limit.middleware'; -import { redis } from '../config/redis'; - -describe('RedisRateLimiter', () => { - const testKey = 'test-client'; - const limiter = new RedisRateLimiter(); - - beforeAll(async () => { - await redis.connect(); - }); - - afterAll(async () => { - await redis.quit(); - }); - - beforeEach(async () => { - await redis.del('spamshield:ratelimit:test-client'); - await redis.del('spamshield:ratelimit:daily:test-client'); - }); - - afterEach(async () => { - await redis.del('spamshield:ratelimit:test-client'); - await redis.del('spamshield:ratelimit:daily:test-client'); - }); - - describe('checkLimit (per-minute)', () => { - it('should allow requests within the limit', async () => { - const result = await limiter.checkLimit(testKey, 60, 10); - - expect(result.remaining).toBe(9); - expect(result.retryAfter).toBeUndefined(); - }); - - it('should decrement remaining on each request', async () => { - const result1 = await limiter.checkLimit(testKey, 60, 10); - const result2 = await limiter.checkLimit(testKey, 60, 10); - - expect(result1.remaining).toBe(9); - expect(result2.remaining).toBe(8); - }); - - it('should exceed limit after max requests', async () => { - for (let i = 0; i < 10; i++) { - await limiter.checkLimit(testKey, 60, 10); - } - - const result = await limiter.checkLimit(testKey, 60, 10); - - expect(result.remaining).toBe(0); - expect(result.retryAfter).toBeGreaterThan(0); - }); - - it('should return retry-after when limit is exceeded', async () => { - for (let i = 0; i < 10; i++) { - await limiter.checkLimit(testKey, 60, 10); - } - - const result = await limiter.checkLimit(testKey, 60, 10); - - expect(result.retryAfter).toBeGreaterThan(0); - expect(result.retryAfter).toBeLessThanOrEqual(60000); - }); - }); - - describe('checkDailyLimit', () => { - it('should allow requests within daily limit', async () => { - const result = await limiter.checkDailyLimit(testKey, 100); - - expect(result.remaining).toBe(99); - expect(result.retryAfter).toBeUndefined(); - }); - - it('should exceed daily limit after max requests', async () => { - for (let i = 0; i < 100; i++) { - await limiter.checkDailyLimit(testKey, 100); - } - - const result = await limiter.checkDailyLimit(testKey, 100); - - expect(result.remaining).toBe(0); - expect(result.retryAfter).toBeGreaterThan(0); - }); - }); - - describe('reset', () => { - it('should clear the rate limit counter', async () => { - await limiter.checkLimit(testKey, 60, 10); - await limiter.checkLimit(testKey, 60, 10); - - await limiter.reset(testKey); - - const result = await limiter.checkLimit(testKey, 60, 10); - - expect(result.remaining).toBe(9); - }); - }); -}); diff --git a/packages/api/src/config/api.config.ts b/packages/api/src/config/api.config.ts deleted file mode 100644 index 29d3f38..0000000 --- a/packages/api/src/config/api.config.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { z } from 'zod'; - -// Environment variables -const envSchema = z.object({ - NODE_ENV: z.enum(['development', 'production', 'test']).default('development'), - PORT: z.string().transform(Number).default(3000), - HOST: z.string().default('0.0.0.0'), - API_RATE_LIMIT_WINDOW: z.string().transform(Number).default(60000), // 1 minute - API_RATE_LIMIT_MAX_REQUESTS: z.string().transform(Number).default(100), - CORS_ORIGIN: z.string().default('http://localhost:5173'), - ALLOWED_ORIGINS: z.string().default(''), -}); - -export const apiEnv = envSchema.parse({ - NODE_ENV: process.env.NODE_ENV, - PORT: process.env.PORT, - HOST: process.env.HOST, - API_RATE_LIMIT_WINDOW: process.env.API_RATE_LIMIT_WINDOW, - API_RATE_LIMIT_MAX_REQUESTS: process.env.API_RATE_LIMIT_MAX_REQUESTS, - CORS_ORIGIN: process.env.CORS_ORIGIN, - ALLOWED_ORIGINS: process.env.ALLOWED_ORIGINS, -}); - -/** - * Parse ALLOWED_ORIGINS into a validated set. - * In production, rejects wildcards ('*') and empty values. - * In development, falls back to localhost. - */ -export function getCorsOrigins(): string | string[] { - const origins = (apiEnv.ALLOWED_ORIGINS || '').split(',').map(s => s.trim()).filter(Boolean); - - if (apiEnv.NODE_ENV === 'production') { - if (origins.length === 0) { - throw new Error( - 'CORS origin validation (FRE-4749): ALLOWED_ORIGINS is empty in production. ' + - 'Set ALLOWED_ORIGINS to a comma-separated list of allowed origins.' - ); - } - for (const origin of origins) { - if (origin === '*') { - throw new Error( - 'CORS origin validation (FRE-4749): wildcard (*) ALLOWED_ORIGIN in production.' - ); - } - let isValidProtocol = true; - try { - const url = new URL(origin); - if (url.protocol !== 'https:' && url.protocol !== 'http:') { - isValidProtocol = false; - throw new Error( - `CORS origin validation (FRE-4749): invalid protocol "${url.protocol}" in "${origin}". Expected http: or https:` - ); - } - } catch (err) { - if (err instanceof Error && !isValidProtocol) throw err; - throw new Error( - `CORS origin validation (FRE-4749): malformed origin "${origin}": ${err instanceof Error ? err.message : String(err)}` - ); - } - } - return origins; - } - - return apiEnv.CORS_ORIGIN || 'http://localhost:5173'; -} - -// Rate limit configuration by tier -export const rateLimitConfig = { - basic: { - windowMs: 60000, // 1 minute - maxRequests: 100, - }, - plus: { - windowMs: 60000, - maxRequests: 500, - }, - premium: { - windowMs: 60000, - maxRequests: 2000, - }, -}; - -// API versioning configuration -export const apiVersioning = { - defaultVersion: '1', - headerName: 'X-API-Version', - queryParam: 'api-version', -}; - -// Logging configuration -export const loggingConfig = { - level: apiEnv.NODE_ENV === 'production' ? 'info' : 'debug', - transport: apiEnv.NODE_ENV === 'development' ? { - target: 'pino-pretty', - options: { - colorize: true, - translateTime: true, - }, - } : undefined, -}; diff --git a/packages/api/src/config/redis.ts b/packages/api/src/config/redis.ts deleted file mode 100644 index 754b2d5..0000000 --- a/packages/api/src/config/redis.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Redis } from 'ioredis'; - -const redisHost = process.env.REDIS_HOST || 'localhost'; -const redisPort = parseInt(process.env.REDIS_PORT || '6379', 10); - -export const redis = new Redis({ - host: redisHost, - port: redisPort, - retryStrategy: (times: number) => Math.min(times * 50, 2000), - lazyConnect: true, -}); - -export async function getRedisConnection(): Promise { - if (redis.status === 'wait' || redis.status === 'connecting') { - await redis.connect(); - } - return redis; -} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts deleted file mode 100644 index b2550dc..0000000 --- a/packages/api/src/index.ts +++ /dev/null @@ -1,108 +0,0 @@ -// dd-trace must be initialized before any other module is loaded for auto-instrumentation -import '@shieldai/monitoring/datadog-init'; -import Fastify from 'fastify'; -import cors from '@fastify/cors'; -import helmet from '@fastify/helmet'; -import { authMiddleware } from './middleware/auth.middleware'; -import { rateLimitMiddleware } from './middleware/rate-limit.middleware'; -import { spamRateLimitMiddleware } from './middleware/spam-rate-limit.middleware'; -import { errorHandlingMiddleware } from './middleware/error-handling.middleware'; -import { loggingMiddleware } from './middleware/logging.middleware'; -import { apiEnv, loggingConfig, getCorsOrigins } from './config/api.config'; -import { routes } from './routes'; - -const fastify = Fastify({ - logger: loggingConfig, - ignoreTrailingSlash: true, - maxParamLength: 500, -}); - -// Register plugins -async function registerPlugins() { - // CORS configuration - await fastify.register(cors, { - origin: getCorsOrigins(), - methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], - credentials: true, - }); - - // Security headers - await fastify.register(helmet, { - global: true, - contentSecurityPolicy: false, - }); - - // Rate limiting - await fastify.register(rateLimitMiddleware); - - // SpamShield rate limiting (Redis-backed) - await fastify.register(spamRateLimitMiddleware); - - // Authentication - await fastify.register(authMiddleware); - - // Logging - await fastify.register(loggingMiddleware); - - // Error handling - await fastify.register(errorHandlingMiddleware); -} - -// Register routes -async function registerRoutes() { - await fastify.register(routes, { prefix: '/api/v1' }); -} - -// Health check endpoint -fastify.get('/health', async () => { - return { status: 'ok', timestamp: new Date().toISOString() }; -}); - -// Root endpoint -fastify.get('/', async () => { - return { - name: 'FrenoCorp API Gateway', - version: '1.0.0', - environment: apiEnv.NODE_ENV, - }; -}); - -// Start server -async function start() { - await registerPlugins(); - await registerRoutes(); - - try { - await fastify.listen({ - port: apiEnv.PORT, - host: apiEnv.HOST, - }); - - console.log(`🚀 API Gateway running at http://${apiEnv.HOST}:${apiEnv.PORT}`); - console.log(`📝 Environment: ${apiEnv.NODE_ENV}`); - console.log(`📊 Rate limit window: ${apiEnv.API_RATE_LIMIT_WINDOW}ms`); - console.log(`📈 Max requests: ${apiEnv.API_RATE_LIMIT_MAX_REQUESTS}`); - } catch (err) { - console.error(err); - process.exit(1); - } -} - -// Graceful shutdown -const gracefulShutdown = async (signal: string) => { - console.log(`\n🛑 ${signal} received, shutting down gracefully...`); - await fastify.close(); - console.log('✅ Server closed'); - process.exit(0); -}; - -process.on('SIGINT', () => gracefulShutdown('SIGINT')); -process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); - -// Export for testing -export { fastify }; - -// Start if running directly -if (process.argv[1] === new URL(import.meta.url).pathname) { - start(); -} diff --git a/packages/api/src/lib/phishing-detector.ts b/packages/api/src/lib/phishing-detector.ts deleted file mode 100644 index 5722863..0000000 --- a/packages/api/src/lib/phishing-detector.ts +++ /dev/null @@ -1,209 +0,0 @@ -export enum UrlVerdict { - SAFE = 'safe', - SUSPICIOUS = 'suspicious', - PHISHING = 'phishing', - SPAM = 'spam', - EXPOSED_CREDENTIALS = 'exposed_credentials', - UNKNOWN = 'unknown', -} - -export enum ThreatType { - PHISHING_KNOWN = 'phishing_known', - PHISHING_HEURISTIC = 'phishing_heuristic', - DOMAIN_AGE = 'domain_age', - SSL_ANOMALY = 'ssl_anomaly', - URL_ENTROPY = 'url_entropy', - TYPOSQUAT = 'typosquat', - CREDENTIAL_EXPOSURE = 'credential_exposure', - SPAM_SOURCE = 'spam_source', - REDIRECT_CHAIN = 'redirect_chain', - MIXED_CONTENT = 'mixed_content', -} - -export interface ThreatInfo { - type: ThreatType; - severity: number; - source: string; - description: string; -} - -export class PhishingDetector { - private knownSuspiciousTlds = new Set([ - '.tk', '.ml', '.ga', '.cf', '.gq', '.xyz', '.top', '.click', '.link', '.work', - ]); - - private commonBrands = new Map([ - ['google', ['gmail', 'drive', 'docs', 'maps', 'play', 'chrome', 'youtube']], - ['apple', ['icloud', 'appstore', 'icloud_content', 'appleid']], - ['amazon', ['aws', 'amazonaws', 'amazon-adsystem', 'prime-video']], - ['microsoft', ['office', 'outlook', 'onedrive', 'teams', 'azure', 'windows']], - ['facebook', ['fb', 'fbcdn', 'instagram', 'whatsapp', 'messenger']], - ['paypal', ['paypalobjects', 'paypal-web', 'xoom']], - ['netflix', ['nflximg', 'nflxso', 'nflxvideo', 'nflxext']], - ]); - - analyzeUrl(url: string): { verdict: UrlVerdict; threats: ThreatInfo[]; score: number } { - const threats: ThreatInfo[] = []; - let score = 0; - - try { - const parsed = new URL(url); - const hostname = parsed.hostname.toLowerCase(); - const domainParts = hostname.split('.'); - const tld = domainParts[domainParts.length - 1]; - - score += this.checkTld(tld, threats); - score += this.checkEntropy(parsed.pathname + parsed.search, threats); - score += this.checkTyposquatting(hostname, threats); - score += this.checkIpAddress(hostname, threats); - score += this.checkLongUrl(url, threats); - score += this.checkSubdomainDepth(domainParts, threats); - score += this.checkHttpsProtocol(parsed.protocol, threats); - score += this.checkRedirectPatterns(parsed.search, threats); - score += this.checkEncodedChars(url, threats); - score += this.checkBrandImpersonation(hostname, threats); - } catch { - return { - verdict: UrlVerdict.UNKNOWN, - threats: [{ type: ThreatType.PHISHING_HEURISTIC, severity: 3, source: 'heuristic', description: 'Malformed URL' }], - score: 30, - }; - } - - const verdict = score >= 70 ? UrlVerdict.PHISHING - : score >= 40 ? UrlVerdict.SUSPICIOUS - : score >= 20 ? UrlVerdict.SPAM - : UrlVerdict.SAFE; - - return { verdict, threats, score }; - } - - private checkTld(tld: string, threats: ThreatInfo[]): number { - if (this.knownSuspiciousTlds.has(`.${tld}`)) { - threats.push({ type: ThreatType.DOMAIN_AGE, severity: 4, source: 'heuristic', description: `Suspicious TLD: .${tld}` }); - return 25; - } - return 0; - } - - private checkEntropy(pathname: string, threats: ThreatInfo[]): number { - if (!pathname || pathname.length < 20) return 0; - const entropy = this.calculateEntropy(pathname); - if (entropy > 4.5) { - threats.push({ type: ThreatType.URL_ENTROPY, severity: 4, source: 'heuristic', description: `High URL path entropy (${entropy.toFixed(2)})` }); - return 20; - } - return 0; - } - - private checkTyposquatting(hostname: string, threats: ThreatInfo[]): number { - for (const [brand, subdomains] of this.commonBrands) { - const parts = hostname.split('.'); - const main = parts[0]; - if (main.includes(brand) && main !== brand) { - const dist = this.levenshteinDistance(main, brand); - if (dist <= 2 && dist > 0) { - threats.push({ type: ThreatType.TYPOSQUAT, severity: 5, source: 'heuristic', description: `Possible typosquat of "${brand}"` }); - return 35; - } - } - const dist = this.levenshteinDistance(main, brand); - if (dist <= 2 && dist > 0 && main.length >= brand.length - 1) { - threats.push({ type: ThreatType.TYPOSQUAT, severity: 5, source: 'heuristic', description: `Possible typosquat of "${brand}"` }); - return 35; - } - for (const sub of subdomains) { - if (hostname.includes(sub) && !hostname.startsWith(`${sub}.`)) { - threats.push({ type: ThreatType.TYPOSQUAT, severity: 3, source: 'heuristic', description: `Contains "${sub}" but not official ${brand}` }); - return 15; - } - } - } - return 0; - } - - private checkIpAddress(hostname: string, threats: ThreatInfo[]): number { - if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname) && hostname !== '127.0.0.1') { - threats.push({ type: ThreatType.PHISHING_HEURISTIC, severity: 4, source: 'heuristic', description: `IP address hostname: ${hostname}` }); - return 25; - } - return 0; - } - - private checkLongUrl(url: string, threats: ThreatInfo[]): number { - if (url.length > 200) { - threats.push({ type: ThreatType.PHISHING_HEURISTIC, severity: 3, source: 'heuristic', description: `Long URL (${url.length} chars)` }); - return 15; - } - return 0; - } - - private checkSubdomainDepth(parts: string[], threats: ThreatInfo[]): number { - if (parts.length > 5) { - threats.push({ type: ThreatType.PHISHING_HEURISTIC, severity: 3, source: 'heuristic', description: `Deep subdomains (${parts.length} levels)` }); - return 15; - } - return 0; - } - - private checkHttpsProtocol(protocol: string, threats: ThreatInfo[]): number { - if (protocol === 'http:') { - threats.push({ type: ThreatType.MIXED_CONTENT, severity: 2, source: 'heuristic', description: 'HTTP (not HTTPS)' }); - return 10; - } - return 0; - } - - private checkRedirectPatterns(query: string, threats: ThreatInfo[]): number { - const params = ['redirect', 'url', 'dest', 'return', 'next', 'target']; - const count = params.filter((p) => query.includes(`${p}=`)).length; - if (count >= 2) { - threats.push({ type: ThreatType.REDIRECT_CHAIN, severity: 3, source: 'heuristic', description: `Multiple redirect params (${count})` }); - return 15; - } - return 0; - } - - private checkEncodedChars(url: string, threats: ThreatInfo[]): number { - if (/(%[0-9a-fA-F]{2}){3,}/.test(url)) { - threats.push({ type: ThreatType.URL_ENTROPY, severity: 3, source: 'heuristic', description: 'Excessive URL encoding' }); - return 15; - } - return 0; - } - - private checkBrandImpersonation(hostname: string, threats: ThreatInfo[]): number { - const patterns = [/login[-_]?(secure|portal|page|form)/i, /account[-_]?(verify|confirm|update)/i, /secure[-_]?(signin|auth|login)/i]; - for (const pattern of patterns) { - if (pattern.test(hostname)) { - threats.push({ type: ThreatType.PHISHING_HEURISTIC, severity: 4, source: 'heuristic', description: `Phishing pattern: ${hostname}` }); - return 20; - } - } - return 0; - } - - private calculateEntropy(str: string): number { - const freq: Record = {}; - for (const c of str) freq[c] = (freq[c] || 0) + 1; - let entropy = 0; - const len = str.length; - for (const count of Object.values(freq)) { - const p = count / len; - entropy -= p * Math.log2(p); - } - return entropy; - } - - private levenshteinDistance(a: string, b: string): number { - const m: number[][] = []; - for (let i = 0; i <= b.length; i++) m[i] = [i]; - for (let j = 0; j <= a.length; j++) m[0][j] = j; - for (let i = 1; i <= b.length; i++) - for (let j = 1; j <= a.length; j++) - m[i][j] = b[i-1] === a[j-1] ? m[i-1][j-1] : Math.min(m[i-1][j-1]+1, m[i][j-1]+1, m[i-1][j]+1); - return m[b.length][a.length]; - } -} - -export const phishingDetector = new PhishingDetector(); diff --git a/packages/api/src/middleware/auth.middleware.ts b/packages/api/src/middleware/auth.middleware.ts deleted file mode 100644 index 5e236c4..0000000 --- a/packages/api/src/middleware/auth.middleware.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import jwt from 'jsonwebtoken'; - -const JWT_SECRET = process.env.JWT_SECRET || process.env.NEXTAUTH_SECRET; - -if (!JWT_SECRET && process.env.NODE_ENV === 'production') { - console.error('JWT_SECRET or NEXTAUTH_SECRET must be set in production'); -} - -export interface AuthRequest extends FastifyRequest { - user?: { - id: string; - email: string; - role: string; - organizationId?: string; - }; - apiKey?: string; - authType: 'jwt' | 'api-key' | 'anonymous'; -} - -export async function authMiddleware(fastify: FastifyInstance) { - // Authentication hook - fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - // Skip auth for health checks and root - const publicRoutes = ['/', '/health', '/extension/auth']; - if (publicRoutes.some((route) => request.url.startsWith(route))) { - authReq.authType = 'anonymous'; - return; - } - - // Try JWT authentication first - const authHeader = request.headers.authorization; - if (authHeader?.startsWith('Bearer ')) { - const token = authHeader.slice(7); - try { - if (!JWT_SECRET) { - throw new Error('JWT_SECRET not configured'); - } - - const decoded = jwt.verify(token, JWT_SECRET) as { - id: string; - email: string; - role: string; - organizationId?: string; - iat?: number; - exp?: number; - }; - - authReq.user = { - id: decoded.id, - email: decoded.email, - role: decoded.role, - organizationId: decoded.organizationId, - }; - authReq.authType = 'jwt'; - return; - } catch (err) { - if (err instanceof jwt.JsonWebTokenError) { - throw { statusCode: 401, message: 'Invalid token' }; - } - if (err instanceof jwt.TokenExpiredError) { - throw { statusCode: 401, message: 'Token expired', expiredAt: err.expiredAt }; - } - throw { statusCode: 401, message: 'Authentication failed' }; - } - } - - // Try API key authentication - const apiKey = request.headers['x-api-key'] as string | undefined; - if (apiKey) { - // In production, validate API key against database - authReq.apiKey = apiKey; - const apiKeyPrefix = apiKey.slice(0, 8); - authReq.user = { - id: `api-${apiKeyPrefix}...`, - email: `api-${apiKeyPrefix}@services.internal`, - role: 'service', - }; - authReq.authType = 'api-key'; - return; - } - - // No auth found - attach anonymous user - authReq.authType = 'anonymous'; - authReq.user = { - id: 'anonymous', - email: 'anonymous@unknown', - role: 'anonymous', - }; - }); - - // Create auth decorator for route-level protection - fastify.decorate('requireAuth', async (request: AuthRequest) => { - if (request.authType === 'anonymous') { - throw { statusCode: 401, message: 'Authentication required' }; - } - return true; - }); - - fastify.decorate('requireRole', (allowedRoles: string[]) => { - return async (request: AuthRequest) => { - if (!request.user?.role || !allowedRoles.includes(request.user.role)) { - throw { - statusCode: 403, - message: `Role ${request.user?.role} not in allowed roles: ${allowedRoles.join(', ')}`, - }; - } - return true; - }; - }); -} diff --git a/packages/api/src/middleware/error-handling.middleware.ts b/packages/api/src/middleware/error-handling.middleware.ts deleted file mode 100644 index 58b477c..0000000 --- a/packages/api/src/middleware/error-handling.middleware.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { captureSentryError, setSentryContext, setSentryUser } from '@shieldai/monitoring'; - -export interface ErrorResponse { - error: string; - message: string; - statusCode: number; - code?: string; - details?: Record; - timestamp: string; - path: string; -} - -export async function errorHandlingMiddleware(fastify: FastifyInstance) { - // Custom error handler - fastify.setErrorHandler((error, request: FastifyRequest, reply: FastifyReply) => { - const err = error as Error & { statusCode?: number; code?: string }; - const response: ErrorResponse = { - error: err.name || 'Internal Server Error', - message: err.message || 'An unexpected error occurred', - statusCode: err.statusCode || 500, - code: err.code, - timestamp: new Date().toISOString(), - path: request.url, - }; - - // Send to Sentry (5xx errors only) - if (response.statusCode >= 500) { - const userId = (request as FastifyRequest & { user?: { id?: string } }).user?.id; - if (userId) setSentryUser(userId); - setSentryContext('request', { - method: request.method, - url: request.url, - userAgent: request.headers['user-agent'], - requestId: request.id, - }); - captureSentryError(err, { - statusCode: String(response.statusCode), - path: request.url, - method: request.method, - }); - } - - // Log error - fastify.log.error({ - error: response, - stack: err.stack, - method: request.method, - userAgent: request.headers['user-agent'], - }); - - // Send standardized error response - reply.status(response.statusCode).send(response); - }); - - // 404 handler - fastify.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => { - reply.status(404).send({ - error: 'Not Found', - message: `Route ${request.method} ${request.url} not found`, - statusCode: 404, - timestamp: new Date().toISOString(), - path: request.url, - }); - }); - - // Validation error handler - fastify.addHook('onError', async (request: FastifyRequest, reply: FastifyReply, error) => { - if (error.validation) { - reply.status(400).send({ - error: 'Validation Error', - message: 'Request validation failed', - statusCode: 400, - code: 'VALIDATION_ERROR', - details: error.validation, - timestamp: new Date().toISOString(), - path: request.url, - }); - } - }); -} diff --git a/packages/api/src/middleware/logging.middleware.ts b/packages/api/src/middleware/logging.middleware.ts deleted file mode 100644 index 20809d1..0000000 --- a/packages/api/src/middleware/logging.middleware.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; - -export interface RequestLog { - method: string; - url: string; - statusCode: number; - responseTime: number; - requestId: string; - userAgent?: string; - clientIp: string; - requestIdHeader?: string; -} - -export async function loggingMiddleware(fastify: FastifyInstance) { - // Generate request ID if not present - fastify.addHook('onRequest', (request: FastifyRequest, reply: FastifyReply, done) => { - const requestId = - request.headers['x-request-id'] || - request.headers['x-correlation-id'] || - `req-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`; - - request.headers['x-request-id'] = requestId; - (request as any).requestId = requestId; - - done(); - }); - - // Log request start - fastify.addHook('onRequest', (request: FastifyRequest, reply: FastifyReply) => { - fastify.log.info({ - event: 'request_start', - method: request.method, - url: request.url, - requestId: (request as any).requestId, - userAgent: request.headers['user-agent'], - clientIp: request.ip || request.headers['x-forwarded-for'] || 'unknown', - }); - }); - - // Log response - fastify.addHook('onResponse', (request: FastifyRequest, reply: FastifyReply, done) => { - const log: RequestLog = { - method: request.method, - url: request.url, - statusCode: reply.statusCode, - responseTime: reply.elapsedTime, - requestId: (request as any).requestId, - userAgent: request.headers['user-agent'], - clientIp: request.ip || request.headers['x-forwarded-for'] || 'unknown', - requestIdHeader: request.headers['x-request-id'] as string, - }; - - // Log based on status code - if (reply.statusCode < 300) { - fastify.log.info(log); - } else if (reply.statusCode < 400) { - fastify.log.warn(log); - } else if (reply.statusCode < 500) { - fastify.log.warn(log); - } else { - fastify.log.error(log); - } - - done(); - }); -} diff --git a/packages/api/src/middleware/monitoring.middleware.ts b/packages/api/src/middleware/monitoring.middleware.ts deleted file mode 100644 index 8fe818c..0000000 --- a/packages/api/src/middleware/monitoring.middleware.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { emitBatchMetrics, emitError } from '@shieldai/monitoring'; - -const SERVICE_NAME = process.env.DD_SERVICE || 'shieldai-api'; - -export async function monitoringMiddleware(fastify: FastifyInstance) { - fastify.addHook('onResponse', async (request: FastifyRequest, reply: FastifyReply) => { - const statusCode = reply.statusCode; - const responseTime = reply.elapsedTime; - const method = request.method; - const url = request.url; - - // Batch all metrics into a single PutMetricDataCommand to avoid rate limits - await emitBatchMetrics({ - serviceName: SERVICE_NAME, - data: [ - { - metricName: 'api_requests', - value: 1, - unit: 'Count', - dimensions: { status_class: String(Math.floor(statusCode / 100)) + 'xx' }, - }, - { - metricName: 'api_latency', - value: responseTime, - unit: 'Milliseconds', - dimensions: { percentile: 'p50' }, - }, - { - metricName: 'api_latency', - value: responseTime, - unit: 'Milliseconds', - dimensions: { percentile: 'p95' }, - }, - { - metricName: 'api_latency', - value: responseTime, - unit: 'Milliseconds', - dimensions: { percentile: 'p99' }, - }, - ], - }); - - // Emit error metric for 5xx (separate call since it has different dimensions) - if (statusCode >= 500) { - await emitError(SERVICE_NAME, 'server_error'); - fastify.log.warn({ - event: 'high_latency_or_error', - method, - url, - statusCode, - responseTime, - service: SERVICE_NAME, - }); - } - - // Log high latency requests (>2s) — only when not already logged as error - else if (responseTime > 2000) { - fastify.log.warn({ - event: 'high_latency', - method, - url, - statusCode, - responseTime, - service: SERVICE_NAME, - }); - } - }); -} diff --git a/packages/api/src/middleware/rate-limit.middleware.ts b/packages/api/src/middleware/rate-limit.middleware.ts deleted file mode 100644 index 433f833..0000000 --- a/packages/api/src/middleware/rate-limit.middleware.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { apiEnv, rateLimitConfig } from '../config/api.config'; - -// Simple in-memory rate limiter -// In production, this should use Redis or similar distributed store -class RateLimiter { - private store: Map; - - constructor() { - this.store = new Map(); - } - - async checkLimit( - key: string, - windowMs: number, - maxRequests: number - ): Promise<{ remaining: number; resetTime: number; retryAfter?: number }> { - const now = Date.now(); - const current = this.store.get(key); - - if (!current || now > current.resetTime) { - // Reset window - this.store.set(key, { - count: 1, - resetTime: now + windowMs, - }); - - return { - remaining: maxRequests - 1, - resetTime: now + windowMs, - }; - } - - // Increment counter - current.count++; - this.store.set(key, current); - - const remaining = maxRequests - current.count; - - if (current.count > maxRequests) { - return { - remaining: 0, - resetTime: current.resetTime, - retryAfter: current.resetTime - now, - }; - } - - return { - remaining, - resetTime: current.resetTime, - }; - } - - reset(key: string) { - this.store.delete(key); - } -} - -const rateLimiter = new RateLimiter(); - -export async function rateLimitMiddleware(fastify: FastifyInstance) { - fastify.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => { - // Skip rate limiting for health checks - if (request.url === '/health') { - return; - } - - // Get client identifier (IP or API key) - const clientIp = request.ip || request.headers['x-forwarded-for'] || 'unknown'; - const apiKey = request.headers['x-api-key'] as string | undefined; - const key = apiKey ? `api:${apiKey}` : `ip:${clientIp}`; - - // Determine tier based on API key or default to basic - let tier = 'basic'; - if (apiKey) { - // In production, fetch tier from user/service lookup - // For now, use a simple heuristic based on key format - if (apiKey.startsWith('premium_')) { - tier = 'premium'; - } else if (apiKey.startsWith('plus_')) { - tier = 'plus'; - } - } - - const config = rateLimitConfig[tier as keyof typeof rateLimitConfig]; - const result = await rateLimiter.checkLimit( - key, - config.windowMs, - config.maxRequests - ); - - // Set rate limit headers - reply.header('X-RateLimit-Limit', config.maxRequests); - reply.header('X-RateLimit-Remaining', result.remaining); - reply.header('X-RateLimit-Reset', Math.ceil(result.resetTime / 1000)); - - if (result.retryAfter) { - reply.header('Retry-After', Math.ceil(result.retryAfter / 1000)); - reply.code(429); // Too Many Requests - - return { - error: 'Too Many Requests', - message: `Rate limit exceeded. Try again in ${Math.ceil(result.retryAfter / 1000)}s`, - tier, - limit: config.maxRequests, - reset: new Date(result.resetTime).toISOString(), - }; - } - - // Add tier info to request for downstream use - (request as any).rateLimitTier = tier; - }); -} - -// Export for testing -export { rateLimiter }; diff --git a/packages/api/src/middleware/spam-rate-limit.middleware.ts b/packages/api/src/middleware/spam-rate-limit.middleware.ts deleted file mode 100644 index f56e373..0000000 --- a/packages/api/src/middleware/spam-rate-limit.middleware.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { redis } from '../config/redis'; -import { spamRateLimits } from '../services/spamshield/spamshield.config'; - -const REDIS_PREFIX = 'spamshield:ratelimit'; - -class RedisRateLimiter { - async checkLimit( - key: string, - windowSeconds: number, - maxRequests: number - ): Promise<{ - remaining: number; - resetTime: number; - retryAfter?: number; - }> { - const redisKey = `${REDIS_PREFIX}:${key}`; - const now = Date.now(); - - const current = await redis.get(redisKey); - const windowStart = now - (now % (windowSeconds * 1000)); - const resetTime = windowStart + windowSeconds * 1000; - - if (!current) { - const expirySeconds = Math.ceil((resetTime - now) / 1000); - await redis.set(redisKey, '1', 'EX', expirySeconds); - - return { - remaining: maxRequests - 1, - resetTime, - }; - } - - const count = parseInt(current, 10) + 1; - await redis.set(redisKey, String(count), 'EX', Math.ceil((resetTime - now) / 1000)); - - const remaining = maxRequests - count; - - if (count > maxRequests) { - return { - remaining: 0, - resetTime, - retryAfter: resetTime - now, - }; - } - - return { - remaining, - resetTime, - }; - } - - async checkDailyLimit( - key: string, - maxPerDay: number - ): Promise<{ - remaining: number; - retryAfter?: number; - }> { - const redisKey = `${REDIS_PREFIX}:daily:${key}`; - const now = Date.now(); - const dayStart = new Date(now); - dayStart.setHours(0, 0, 0, 0); - const dayEnd = new Date(dayStart); - dayEnd.setDate(dayEnd.getDate() + 1); - const resetTime = dayEnd.getTime(); - - const current = await redis.get(redisKey); - const expirySeconds = Math.ceil((resetTime - now) / 1000); - - if (!current) { - await redis.set(redisKey, '1', 'EX', expirySeconds); - - return { - remaining: maxPerDay - 1, - }; - } - - const count = parseInt(current, 10) + 1; - await redis.set(redisKey, String(count), 'EX', expirySeconds); - - const remaining = maxPerDay - count; - - if (count > maxPerDay) { - return { - remaining: 0, - retryAfter: resetTime - now, - }; - } - - return { - remaining, - }; - } - - reset(key: string) { - const redisKey = `${REDIS_PREFIX}:${key}`; - return redis.del(redisKey); - } -} - -export const spamRateLimiter = new RedisRateLimiter(); - -export async function spamRateLimitMiddleware(fastify: FastifyInstance) { - fastify.addHook('preHandler', async (request: FastifyRequest, reply: FastifyReply) => { - const url = request.url || ''; - - if (!url.startsWith('/spamshield')) { - return; - } - - const clientIp = request.ip || (request.headers['x-forwarded-for'] as string) || 'unknown'; - const apiKey = request.headers['x-api-key'] as string | undefined; - const key = apiKey ? `api:${apiKey}` : `ip:${clientIp}`; - - let tier = 'basic'; - if (apiKey) { - if (apiKey.startsWith('premium_')) { - tier = 'premium'; - } else if (apiKey.startsWith('plus_')) { - tier = 'plus'; - } - } - - const config = spamRateLimits[tier as keyof typeof spamRateLimits]; - - const minuteResult = await spamRateLimiter.checkLimit( - key, - 60, - config.analysesPerMinute - ); - - const dailyResult = await spamRateLimiter.checkDailyLimit( - key, - config.analysesPerDay - ); - - reply.header('X-RateLimit-Limit', config.analysesPerMinute); - reply.header('X-RateLimit-Remaining', minuteResult.remaining); - reply.header('X-RateLimit-Reset', Math.ceil(minuteResult.resetTime / 1000)); - reply.header('X-RateLimit-Daily-Limit', config.analysesPerDay); - reply.header('X-RateLimit-Daily-Remaining', dailyResult.remaining); - - const retryAfter = minuteResult.retryAfter || dailyResult.retryAfter; - - if (retryAfter) { - reply.header('Retry-After', Math.ceil(retryAfter / 1000)); - reply.code(429); - - return { - error: 'Too Many Requests', - message: `Spam analysis rate limit exceeded. Try again in ${Math.ceil(retryAfter / 1000)}s`, - tier, - limit: config.analysesPerMinute, - dailyLimit: config.analysesPerDay, - reset: new Date(minuteResult.resetTime).toISOString(), - }; - } - - (request as any).spamRateLimitTier = tier; - }); -} - -export { RedisRateLimiter }; diff --git a/packages/api/src/openapi/spec.json b/packages/api/src/openapi/spec.json deleted file mode 100644 index babf73e..0000000 --- a/packages/api/src/openapi/spec.json +++ /dev/null @@ -1,3629 +0,0 @@ -{ - "openapi": "3.0.3", - "info": { - "title": "ShieldAI API Gateway", - "description": "ShieldAI API Gateway — Fastify-based API providing VoicePrint, SpamShield, DarkWatch, Home Title, Info Broker Removal, and more. Supports JWT and API-key authentication with tiered rate limiting.", - "version": "1.0.0", - "contact": { - "name": "FrenoCorp Engineering" - } - }, - "servers": [ - { - "url": "http://localhost:3000", - "description": "Local development" - }, - { - "url": "https://api.shieldai.com", - "description": "Production" - } - ], - "paths": { - "/": { - "get": { - "tags": ["System"], - "summary": "Root endpoint", - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/health": { - "get": { - "tags": ["System"], - "summary": "Health check", - "responses": { - "200": { - "description": "Service healthy", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { "type": "string", "enum": ["ok"] }, - "timestamp": { "type": "string", "format": "date-time" } - } - } - } - } - } - } - } - }, - "/api/v1/info": { - "get": { - "tags": ["System"], - "summary": "API version info", - "responses": { - "200": { - "description": "API version info", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "version": { "type": "string" }, - "environment": { "type": "string" }, - "build": { "type": "string" } - } - } - } - } - } - } - } - }, - "/api/v1/docs": { - "get": { - "tags": ["System"], - "summary": "API documentation (legacy JSON)", - "responses": { - "200": { - "description": "API documentation", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { "type": "string" }, - "version": { "type": "string" }, - "endpoints": { - "type": "object", - "properties": { - "public": { - "type": "array", - "items": { - "type": "object", - "properties": { - "method": { "type": "string" }, - "path": { "type": "string" }, - "description": { "type": "string" } - } - } - }, - "authenticated": { - "type": "array", - "items": { - "type": "object", - "properties": { - "method": { "type": "string" }, - "path": { "type": "string" }, - "description": { "type": "string" } - } - } - } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/auth/user/me": { - "get": { - "tags": ["Authentication"], - "summary": "Get current authenticated user", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Current user", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "user": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "email": { "type": "string" }, - "role": { "type": "string" }, - "organizationId": { "type": "string", "nullable": true } - } - }, - "authType": { "type": "string", "enum": ["jwt", "api-key"] } - } - } - } - } - }, - "401": { - "description": "Authentication required", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ErrorResponse" } - } - } - } - } - } - }, - "/api/v1/auth/services": { - "get": { - "tags": ["Authentication"], - "summary": "List available services", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Service list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "services": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { "type": "string" }, - "url": { "type": "string" }, - "status": { "type": "string" } - } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/services/user": { - "get": { - "tags": ["Services"], - "summary": "User service proxy (placeholder)", - "responses": { - "200": { - "description": "User service info", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "service": { "type": "string" }, - "message": { "type": "string" }, - "timestamp": { "type": "string", "format": "date-time" } - } - } - } - } - } - } - } - }, - "/api/v1/services/billing": { - "get": { - "tags": ["Services"], - "summary": "Billing service proxy (placeholder)", - "responses": { - "200": { - "description": "Billing service info", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "service": { "type": "string" }, - "message": { "type": "string" }, - "timestamp": { "type": "string", "format": "date-time" } - } - } - } - } - } - } - } - }, - "/api/v1/services/notifications": { - "get": { - "tags": ["Services"], - "summary": "Notification service proxy (placeholder)", - "responses": { - "200": { - "description": "Notification service info", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "service": { "type": "string" }, - "message": { "type": "string" }, - "timestamp": { "type": "string", "format": "date-time" } - } - } - } - } - } - } - } - }, - "/api/v1/devices/register": { - "post": { - "tags": ["Devices"], - "summary": "Register device for push notifications", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["platform", "fcmToken", "apnsToken", "appVersion", "osVersion"], - "properties": { - "platform": { "type": "string", "enum": ["ios", "android"] }, - "fcmToken": { "type": "string" }, - "apnsToken": { "type": "string" }, - "appVersion": { "type": "string" }, - "osVersion": { "type": "string" }, - "deviceModel": { "type": "string", "nullable": true } - } - } - } - } - }, - "responses": { - "200": { - "description": "Device registered", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { "type": "boolean" }, - "device": { - "type": "object", - "properties": { - "deviceId": { "type": "string" }, - "platform": { "type": "string" }, - "registeredAt": { "type": "string", "format": "date-time" } - } - }, - "message": { "type": "string" } - } - } - } - } - }, - "400": { - "description": "Missing required fields", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ErrorResponse" } - } - } - }, - "401": { - "description": "Authentication required", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ErrorResponse" } - } - } - } - } - } - }, - "/api/v1/devices/{deviceId}/tokens": { - "put": { - "tags": ["Devices"], - "summary": "Update device push tokens", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "deviceId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [], - "properties": { - "fcmToken": { "type": "string" }, - "apnsToken": { "type": "string" }, - "deviceModel": { "type": "string" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Tokens updated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { "type": "boolean" }, - "device": { - "type": "object", - "properties": { - "deviceId": { "type": "string" }, - "platform": { "type": "string" }, - "lastActiveAt": { "type": "string", "format": "date-time" } - } - }, - "message": { "type": "string" } - } - } - } - } - }, - "404": { - "description": "Device not found", - "content": { - "application/json": { - "schema": { "$ref": "#/components/schemas/ErrorResponse" } - } - } - } - } - } - }, - "/api/v1/devices": { - "get": { - "tags": ["Devices"], - "summary": "Get user's registered devices", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Device list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { "type": "boolean" }, - "devices": { - "type": "array", - "items": { - "type": "object", - "properties": { - "deviceId": { "type": "string" }, - "platform": { "type": "string" }, - "appVersion": { "type": "string" }, - "osVersion": { "type": "string" }, - "model": { "type": "string", "nullable": true }, - "lastActiveAt": { "type": "string", "format": "date-time" }, - "registeredAt": { "type": "string", "format": "date-time" } - } - } - }, - "message": { "type": "string" } - } - } - } - } - } - } - }, - "delete": { - "tags": ["Devices"], - "summary": "Deregister device", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "deviceId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Device deregistered", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { "type": "boolean" }, - "message": { "type": "string" } - } - } - } - } - } - } - } - }, - "/api/v1/notifications/send": { - "post": { - "tags": ["Notifications"], - "summary": "Send a notification to a user", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["userId", "channel", "subject", "body"], - "properties": { - "userId": { "type": "string" }, - "channel": { "type": "string", "enum": ["email", "push", "sms"] }, - "subject": { "type": "string" }, - "body": { "type": "string" }, - "email": { "type": "string" }, - "phone": { "type": "string" }, - "fcmToken": { "type": "string" }, - "apnsToken": { "type": "string" }, - "priority": { "type": "string", "enum": ["low", "normal", "high", "urgent"] }, - "metadata": { "type": "object" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Notification sent", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { "type": "boolean" }, - "notifications": { "type": "array" } - } - } - } - } - }, - "503": { - "description": "Notification service not initialized" - } - } - } - }, - "/api/v1/notifications/{userId}/preferences": { - "get": { - "tags": ["Notifications"], - "summary": "Get notification preferences", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Preferences retrieved", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { "type": "boolean" }, - "preferences": { "type": "object" } - } - } - } - } - } - } - }, - "put": { - "tags": ["Notifications"], - "summary": "Update notification preferences", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "email": { - "type": "object", - "properties": { - "enabled": { "type": "boolean" }, - "categories": { "type": "array", "items": { "type": "string" } } - } - }, - "push": { - "type": "object", - "properties": { - "enabled": { "type": "boolean" }, - "categories": { "type": "array", "items": { "type": "string" } } - } - }, - "sms": { - "type": "object", - "properties": { - "enabled": { "type": "boolean" }, - "categories": { "type": "array", "items": { "type": "string" } } - } - } - } - } - } - } - }, - "responses": { - "200": { - "description": "Preferences updated" - } - } - } - }, - "/api/v1/notifications/config": { - "get": { - "tags": ["Notifications"], - "summary": "Get notification configuration status", - "responses": { - "200": { - "description": "Config retrieved", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { "type": "boolean" }, - "config": { "type": "object" } - } - } - } - } - } - } - } - }, - "/api/v1/subscription": { - "get": { - "tags": ["Subscription"], - "summary": "Get current user's subscription", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Subscription details", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "subscription": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "status": { "type": "string" }, - "currentPeriodStart": { "type": "string", "format": "date-time" }, - "currentPeriodEnd": { "type": "string", "format": "date-time" }, - "cancelAtPeriodEnd": { "type": "boolean" }, - "created": { "type": "string", "format": "date-time" } - } - }, - "customer": { - "type": "object", - "properties": { - "id": { "type": "string" } - } - } - } - } - } - } - }, - "404": { - "description": "No active subscription found" - } - } - } - }, - "/api/v1/subscription/create": { - "post": { - "tags": ["Subscription"], - "summary": "Create a new subscription", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["tier", "customerId"], - "properties": { - "tier": { "type": "string", "enum": ["free", "basic", "plus", "premium"] }, - "customerId": { "type": "string" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Subscription created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "subscription": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "status": { "type": "string" }, - "currentPeriodStart": { "type": "string", "format": "date-time" }, - "currentPeriodEnd": { "type": "string", "format": "date-time" } - } - }, - "customer": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "email": { "type": "string" } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/subscription/{subscriptionId}/tier": { - "put": { - "tags": ["Subscription"], - "summary": "Update subscription tier", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "subscriptionId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["tier"], - "properties": { - "tier": { "type": "string", "enum": ["free", "basic", "plus", "premium"] } - } - } - } - } - }, - "responses": { - "200": { - "description": "Subscription updated" - } - } - } - }, - "/api/v1/subscription/{subscriptionId}": { - "delete": { - "tags": ["Subscription"], - "summary": "Cancel subscription", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "subscriptionId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "cancelAtPeriodEnd": { "type": "boolean" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Subscription cancelled" - } - } - } - }, - "/api/v1/customer/portal": { - "post": { - "tags": ["Subscription"], - "summary": "Create customer portal session", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["customerId", "returnUrl"], - "properties": { - "customerId": { "type": "string" }, - "returnUrl": { "type": "string", "format": "uri" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Portal session created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { "type": "string", "format": "uri" }, - "expiresAt": { "type": "string", "format": "date-time" } - } - } - } - } - } - } - } - }, - "/api/v1/customer": { - "post": { - "tags": ["Subscription"], - "summary": "Create customer", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["email"], - "properties": { - "email": { "type": "string", "format": "email" }, - "name": { "type": "string" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Customer created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "customer": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "email": { "type": "string" }, - "name": { "type": "string" }, - "metadata": { "type": "object" } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/user/tier": { - "get": { - "tags": ["Subscription"], - "summary": "Get user tier", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "User tier", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "tier": { "type": "string", "enum": ["free", "basic", "plus", "premium"] }, - "limits": { "type": "object" } - } - } - } - } - } - } - } - }, - "/api/v1/invoices": { - "get": { - "tags": ["Subscription"], - "summary": "Get invoice history", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "customerId", - "in": "query", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Invoice history", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "invoices": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "amountDue": { "type": "number" }, - "amountPaid": { "type": "number" }, - "status": { "type": "string" }, - "created": { "type": "string", "format": "date-time" }, - "hostedInvoiceUrl": { "type": "string", "format": "uri" } - } - } - }, - "hasMore": { "type": "boolean" } - } - } - } - } - } - } - } - }, - "/api/v1/webhooks/stripe": { - "post": { - "tags": ["Subscription"], - "summary": "Stripe webhook handler (public)", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { "type": "object" } - } - } - }, - "responses": { - "200": { - "description": "Webhook received" - }, - "400": { - "description": "Missing Stripe signature" - } - } - } - }, - "/api/v1/waitlist/signup": { - "post": { - "tags": ["Waitlist"], - "summary": "Sign up for waitlist", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["email"], - "properties": { - "email": { "type": "string", "format": "email" }, - "name": { "type": "string" }, - "tier": { "type": "string", "enum": ["basic", "plus", "premium"] }, - "utmSource": { "type": "string" }, - "utmMedium": { "type": "string" }, - "utmCampaign": { "type": "string" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Waitlist signup successful" - }, - "200": { - "description": "Already on waitlist" - }, - "400": { - "description": "Valid email is required" - } - } - } - }, - "/api/v1/waitlist/count": { - "get": { - "tags": ["Waitlist"], - "summary": "Get waitlist count", - "responses": { - "200": { - "description": "Waitlist count", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "count": { "type": "integer" } - } - } - } - } - } - } - } - }, - "/api/v1/blog": { - "get": { - "tags": ["Blog"], - "summary": "List published blog posts", - "parameters": [ - { - "name": "page", - "in": "query", - "schema": { "type": "string", "default": "1" } - }, - { - "name": "limit", - "in": "query", - "schema": { "type": "string", "default": "10" } - }, - { - "name": "tag", - "in": "query", - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Blog posts", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "posts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "slug": { "type": "string" }, - "title": { "type": "string" }, - "excerpt": { "type": "string" }, - "authorName": { "type": "string" }, - "coverImageUrl": { "type": "string" }, - "tags": { "type": "array", "items": { "type": "string" } }, - "publishedAt": { "type": "string", "format": "date-time" }, - "viewCount": { "type": "integer" } - } - } - }, - "pagination": { - "type": "object", - "properties": { - "page": { "type": "integer" }, - "limit": { "type": "integer" }, - "total": { "type": "integer" }, - "totalPages": { "type": "integer" } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/blog/{slug}": { - "get": { - "tags": ["Blog"], - "summary": "Get blog post by slug", - "parameters": [ - { - "name": "slug", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Blog post" - }, - "404": { - "description": "Post not found" - } - } - } - }, - "/api/v1/admin/blog": { - "get": { - "tags": ["Blog Admin"], - "summary": "List all blog posts (admin)", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "page", - "in": "query", - "schema": { "type": "string", "default": "1" } - }, - { - "name": "limit", - "in": "query", - "schema": { "type": "string", "default": "20" } - } - ], - "responses": { - "200": { - "description": "Blog posts with pagination" - } - } - }, - "post": { - "tags": ["Blog Admin"], - "summary": "Create blog post", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["slug", "title", "content"], - "properties": { - "slug": { "type": "string" }, - "title": { "type": "string", "maxLength": 200 }, - "excerpt": { "type": "string" }, - "content": { "type": "string" }, - "authorName": { "type": "string" }, - "coverImageUrl": { "type": "string" }, - "tags": { "type": "array", "items": { "type": "string" } }, - "published": { "type": "boolean" }, - "publishedAt": { "type": "string", "format": "date-time" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Post created" - }, - "409": { - "description": "Slug already exists" - } - } - } - }, - "/api/v1/admin/blog/{id}": { - "put": { - "tags": ["Blog Admin"], - "summary": "Update blog post", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "slug": { "type": "string" }, - "title": { "type": "string" }, - "excerpt": { "type": "string" }, - "content": { "type": "string" }, - "authorName": { "type": "string" }, - "coverImageUrl": { "type": "string" }, - "tags": { "type": "array", "items": { "type": "string" } }, - "published": { "type": "boolean" }, - "publishedAt": { "type": "string", "format": "date-time" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Post updated" - }, - "404": { - "description": "Post not found" - } - } - }, - "delete": { - "tags": ["Blog Admin"], - "summary": "Delete blog post", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "204": { - "description": "Post deleted" - } - } - } - }, - "/api/v1/extension/auth": { - "post": { - "tags": ["Extension"], - "summary": "Validate browser extension token", - "security": [], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { "type": "object" } - } - } - }, - "responses": { - "200": { - "description": "Token validated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "userId": { "type": "string" }, - "tier": { "type": "string" } - } - } - } - } - }, - "401": { - "description": "Bearer token required or token invalid" - } - } - } - }, - "/api/v1/extension/url-check": { - "post": { - "tags": ["Extension"], - "summary": "Check URL for phishing threats", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["url"], - "properties": { - "url": { "type": "string", "format": "uri" } - } - } - } - } - }, - "responses": { - "200": { - "description": "URL check result", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "url": { "type": "string" }, - "domain": { "type": "string" }, - "verdict": { "type": "string" }, - "confidence": { "type": "number" }, - "threats": { - "type": "array", - "items": { - "type": "object", - "properties": { - "type": { "type": "string" }, - "severity": { "type": "string" }, - "source": { "type": "string" }, - "description": { "type": "string" } - } - } - }, - "timestamp": { "type": "integer" } - } - } - } - } - } - } - } - }, - "/api/v1/extension/phishing-report": { - "post": { - "tags": ["Extension"], - "summary": "Report a phishing URL", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["url", "pageTitle", "tabId", "timestamp", "reason"], - "properties": { - "url": { "type": "string" }, - "pageTitle": { "type": "string" }, - "tabId": { "type": "integer" }, - "timestamp": { "type": "integer" }, - "reason": { "type": "string" }, - "heuristics": { "type": "object" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Phishing report submitted" - } - } - } - }, - "/api/v1/extension/stats": { - "get": { - "tags": ["Extension"], - "summary": "Get extension stats", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Extension stats", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "threatsBlockedToday": { "type": "integer" }, - "urlsCheckedToday": { "type": "integer" }, - "lastSyncAt": { "type": "string", "format": "date-time" }, - "syncDate": { "type": "string" } - } - } - } - } - } - } - } - }, - "/api/v1/extension/exposures/check": { - "post": { - "tags": ["Extension"], - "summary": "Check domain exposures", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["domain"], - "properties": { - "domain": { "type": "string" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Exposure check result", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "exposed": { "type": "boolean" }, - "sources": { "type": "array", "items": { "type": "string" } }, - "count": { "type": "integer" } - } - } - } - } - } - } - } - }, - "/api/v1/voiceprint/enroll": { - "post": { - "tags": ["VoicePrint"], - "summary": "Enroll a new voice profile", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": ["audio"], - "properties": { - "audio": { "type": "string", "format": "binary" }, - "name": { "type": "string" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Enrollment created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "enrollment": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "isActive": { "type": "boolean" }, - "createdAt": { "type": "string", "format": "date-time" } - } - } - } - } - } - } - }, - "400": { - "description": "Audio file is required" - }, - "422": { - "description": "Enrollment failed" - } - } - } - }, - "/api/v1/voiceprint/enrollments": { - "get": { - "tags": ["VoicePrint"], - "summary": "List user's voice enrollments", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "isActive", - "in": "query", - "schema": { "type": "string" } - }, - { - "name": "limit", - "in": "query", - "schema": { "type": "string" } - }, - { - "name": "offset", - "in": "query", - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Enrollments list", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "enrollments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "isActive": { "type": "boolean" }, - "createdAt": { "type": "string", "format": "date-time" } - } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/voiceprint/enrollments/{id}": { - "delete": { - "tags": ["VoicePrint"], - "summary": "Remove a voice enrollment", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Enrollment removed" - }, - "404": { - "description": "Enrollment not found" - } - } - } - }, - "/api/v1/voiceprint/analyze": { - "post": { - "tags": ["VoicePrint"], - "summary": "Analyze a single audio file", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": ["audio"], - "properties": { - "audio": { "type": "string", "format": "binary" }, - "enrollmentId": { "type": "string" }, - "audioUrl": { "type": "string" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Analysis result", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "analysis": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "isSynthetic": { "type": "boolean" }, - "confidence": { "type": "number" }, - "analysisResult": { "type": "object" }, - "createdAt": { "type": "string", "format": "date-time" } - } - } - } - } - } - } - }, - "400": { - "description": "Audio file is required" - } - } - } - }, - "/api/v1/voiceprint/results/{id}": { - "get": { - "tags": ["VoicePrint"], - "summary": "Get analysis result by ID", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Analysis result" - }, - "404": { - "description": "Analysis not found" - } - } - } - }, - "/api/v1/voiceprint/history": { - "get": { - "tags": ["VoicePrint"], - "summary": "Get analysis history", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "limit", - "in": "query", - "schema": { "type": "string" } - }, - { - "name": "offset", - "in": "query", - "schema": { "type": "string" } - }, - { - "name": "isSynthetic", - "in": "query", - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Analysis history" - } - } - } - }, - "/api/v1/voiceprint/batch": { - "post": { - "tags": ["VoicePrint"], - "summary": "Batch analyze multiple audio files", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "multipart/form-data": { - "schema": { - "type": "object", - "required": ["audio"], - "properties": { - "audio": { "type": "string", "format": "binary" }, - "enrollmentId": { "type": "string" }, - "audioUrl": { "type": "string" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Batch analysis job", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "jobId": { "type": "string" }, - "results": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "isSynthetic": { "type": "boolean" }, - "confidence": { "type": "number" } - } - } - }, - "summary": { "type": "object" } - } - } - } - } - } - } - } - }, - "/api/v1/spamshield/sms/classify": { - "post": { - "tags": ["SpamShield"], - "summary": "Classify SMS text", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["text"], - "properties": { - "text": { "type": "string" } - } - } - } - } - }, - "responses": { - "200": { - "description": "SMS classification", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "classification": { - "type": "object", - "properties": { - "isSpam": { "type": "boolean" }, - "confidence": { "type": "number" }, - "spamFeatures": { "type": "object" } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/spamshield/number/reputation": { - "post": { - "tags": ["SpamShield"], - "summary": "Check phone number reputation", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["phoneNumber"], - "properties": { - "phoneNumber": { "type": "string" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Number reputation", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "reputation": { - "type": "object", - "properties": { - "isSpam": { "type": "boolean" }, - "confidence": { "type": "number" }, - "spamType": { "type": "string" }, - "reportCount": { "type": "integer" } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/spamshield/call/analyze": { - "post": { - "tags": ["SpamShield"], - "summary": "Analyze incoming call", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["phoneNumber", "callTime"], - "properties": { - "phoneNumber": { "type": "string" }, - "duration": { "type": "integer" }, - "callTime": { "type": "string", "format": "date-time" }, - "isVoip": { "type": "boolean" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Call analysis", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "analysis": { - "type": "object", - "properties": { - "decision": { "type": "string" }, - "confidence": { "type": "number" }, - "reasons": { "type": "array", "items": { "type": "string" } } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/spamshield/feedback": { - "post": { - "tags": ["SpamShield"], - "summary": "Record spam feedback", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["phoneNumber", "isSpam"], - "properties": { - "phoneNumber": { "type": "string" }, - "isSpam": { "type": "boolean" }, - "confidence": { "type": "number" }, - "metadata": { "type": "object" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Feedback recorded" - } - } - } - }, - "/api/v1/spamshield/history": { - "get": { - "tags": ["SpamShield"], - "summary": "Get spam history", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "limit", - "in": "query", - "schema": { "type": "string" } - }, - { - "name": "isSpam", - "in": "query", - "schema": { "type": "string" } - }, - { - "name": "startDate", - "in": "query", - "schema": { "type": "string", "format": "date-time" } - } - ], - "responses": { - "200": { - "description": "Spam history" - } - } - } - }, - "/api/v1/spamshield/statistics": { - "get": { - "tags": ["SpamShield"], - "summary": "Get spam statistics", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Spam statistics" - } - } - } - }, - "/api/v1/darkwatch/watchlist": { - "get": { - "tags": ["DarkWatch"], - "summary": "List watchlist items", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Watchlist items" - }, - "404": { - "description": "Active subscription not found" - } - } - }, - "post": { - "tags": ["DarkWatch"], - "summary": "Add watchlist item", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["type", "value"], - "properties": { - "type": { "type": "string" }, - "value": { "type": "string" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Watchlist item added" - }, - "400": { - "description": "Type and value are required" - } - } - } - }, - "/api/v1/darkwatch/watchlist/{id}": { - "delete": { - "tags": ["DarkWatch"], - "summary": "Remove watchlist item", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Watchlist item removed" - } - } - } - }, - "/api/v1/darkwatch/scan": { - "post": { - "tags": ["DarkWatch"], - "summary": "Trigger on-demand scan", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Scan queued", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "job": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "status": { "type": "string", "enum": ["queued"] } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/darkwatch/scan/schedule": { - "get": { - "tags": ["DarkWatch"], - "summary": "Get scan schedule", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Scan schedule" - } - } - } - }, - "/api/v1/darkwatch/exposures": { - "get": { - "tags": ["DarkWatch"], - "summary": "List exposures", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Exposures list" - } - } - } - }, - "/api/v1/darkwatch/alerts": { - "get": { - "tags": ["DarkWatch"], - "summary": "List alerts", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Alerts list" - } - } - } - }, - "/api/v1/darkwatch/alerts/{id}/read": { - "patch": { - "tags": ["DarkWatch"], - "summary": "Mark alert as read", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Alert marked as read" - } - } - } - }, - "/api/v1/darkwatch/webhook": { - "post": { - "tags": ["DarkWatch"], - "summary": "External webhook receiver", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["source", "identifier", "identifierType"], - "properties": { - "source": { "type": "string" }, - "identifier": { "type": "string" }, - "identifierType": { "type": "string" }, - "metadata": { "type": "object" }, - "timestamp": { "type": "string", "format": "date-time" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Webhook processed", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "processed": { "type": "boolean" }, - "exposuresCreated": { "type": "integer" }, - "alertsCreated": { "type": "integer" } - } - } - } - } - }, - "400": { - "description": "Source, identifier, and identifierType are required" - }, - "401": { - "description": "Webhook signature and timestamp required or invalid" - } - } - } - }, - "/api/v1/darkwatch/scheduler/init": { - "post": { - "tags": ["DarkWatch"], - "summary": "Initialize scheduled scans", - "responses": { - "200": { - "description": "Scheduler initialized", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "scheduled": { "type": "integer" }, - "jobs": { "type": "array" } - } - } - } - } - } - } - } - }, - "/api/v1/darkwatch/scheduler/reschedule": { - "post": { - "tags": ["DarkWatch"], - "summary": "Reschedule all scans", - "responses": { - "200": { - "description": "Reschedule complete" - } - } - } - }, - "/api/v1/reports": { - "get": { - "tags": ["Reports"], - "summary": "Get report history", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "limit", - "in": "query", - "schema": { "type": "string", "default": "20" } - }, - { - "name": "offset", - "in": "query", - "schema": { "type": "string", "default": "0" } - } - ], - "responses": { - "200": { - "description": "Report history" - } - } - } - }, - "/api/v1/reports/generate": { - "post": { - "tags": ["Reports"], - "summary": "Generate a new report", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "reportType": { "type": "string" }, - "periodStart": { "type": "string", "format": "date-time" }, - "periodEnd": { "type": "string", "format": "date-time" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Report generated" - } - } - } - }, - "/api/v1/reports/schedule/monthly": { - "post": { - "tags": ["Reports"], - "summary": "Schedule monthly reports", - "responses": { - "200": { - "description": "Monthly reports scheduled" - } - } - } - }, - "/api/v1/reports/schedule/annual": { - "post": { - "tags": ["Reports"], - "summary": "Schedule annual reports", - "responses": { - "200": { - "description": "Annual reports scheduled" - } - } - } - }, - "/api/v1/reports/{reportId}": { - "get": { - "tags": ["Reports"], - "summary": "Get specific report", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "reportId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Report details" - }, - "404": { - "description": "Report not found" - } - } - } - }, - "/api/v1/reports/{reportId}/html": { - "get": { - "tags": ["Reports"], - "summary": "Get report HTML content", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "reportId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "HTML content", - "content": { - "text/html": { - "schema": { "type": "string" } - } - } - }, - "404": { - "description": "Report not found or not completed" - } - } - } - }, - "/api/v1/reports/{reportId}/pdf": { - "get": { - "tags": ["Reports"], - "summary": "Get report PDF", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "reportId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "PDF file", - "content": { - "application/pdf": { - "schema": { "type": "string", "format": "binary" } - } - } - } - } - } - }, - "/api/v1/hometitle/properties": { - "get": { - "tags": ["Home Title"], - "summary": "List monitored properties", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Properties list" - }, - "402": { - "description": "Premium tier required" - } - } - }, - "post": { - "tags": ["Home Title"], - "summary": "Add property to monitor", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["address"], - "properties": { - "address": { "type": "string" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Property added" - }, - "400": { - "description": "Invalid address or property limit reached" - }, - "402": { - "description": "Premium tier required" - }, - "409": { - "description": "Duplicate property" - } - } - } - }, - "/api/v1/hometitle/properties/{id}": { - "delete": { - "tags": ["Home Title"], - "summary": "Remove property from monitoring", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Property removed" - }, - "404": { - "description": "Property not found" - } - } - } - }, - "/api/v1/hometitle/properties/stats": { - "get": { - "tags": ["Home Title"], - "summary": "Get dashboard widget stats", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Stats", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "monitoredProperties": { "type": "integer" }, - "tier": { "type": "string" }, - "isPremium": { "type": "boolean" }, - "propertyLimit": { "type": "integer" }, - "canAddMore": { "type": "boolean" }, - "recentAlertCount": { "type": "integer" }, - "criticalAlertCount": { "type": "integer" } - } - } - } - } - } - } - } - }, - "/api/v1/hometitle/changes": { - "get": { - "tags": ["Home Title"], - "summary": "Get recent property changes", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "limit", - "in": "query", - "schema": { "type": "integer", "default": 20, "maximum": 50 } - } - ], - "responses": { - "200": { - "description": "Property changes" - } - } - } - }, - "/api/v1/hometitle/alerts": { - "get": { - "tags": ["Home Title"], - "summary": "Get property alerts", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "limit", - "in": "query", - "schema": { "type": "integer", "default": 20, "maximum": 50 } - }, - { - "name": "severity", - "in": "query", - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Alerts list" - } - } - } - }, - "/api/v1/hometitle/alerts/{id}/read": { - "patch": { - "tags": ["Home Title"], - "summary": "Mark alert as read", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Alert marked as read" - } - } - } - }, - "/api/v1/hometitle/scan": { - "post": { - "tags": ["Home Title"], - "summary": "Trigger on-demand scan", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Scan initiated", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "scan": { - "type": "object", - "properties": { - "scanId": { "type": "string" }, - "propertiesScanned": { "type": "integer" }, - "changesDetected": { "type": "integer" }, - "alertsCreated": { "type": "integer" }, - "notificationsSent": { "type": "integer" }, - "startedAt": { "type": "string", "format": "date-time" }, - "completedAt": { "type": "string", "format": "date-time" } - } - } - } - } - } - } - } - } - } - }, - "/api/v1/scan": { - "post": { - "tags": ["Scan"], - "summary": "Run a scan", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "source": { "type": "string" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Scan completed", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "scanned": { "type": "boolean" }, - "resultCount": { "type": "integer" } - } - } - } - } - } - } - } - }, - "/api/v1/scan/history": { - "get": { - "tags": ["Scan"], - "summary": "Get scan history", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Scan history" - } - } - } - }, - "/api/v1/exposures": { - "get": { - "tags": ["Exposure"], - "summary": "Get user exposures", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Exposures list" - } - } - } - }, - "/api/v1/exposures/{id}": { - "get": { - "tags": ["Exposure"], - "summary": "Get exposure detail", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Exposure detail" - }, - "404": { - "description": "Exposure not found" - } - } - } - }, - "/api/v1/alerts": { - "get": { - "tags": ["Alert"], - "summary": "Get user alerts", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "limit", - "in": "query", - "schema": { "type": "integer", "default": 50 } - }, - { - "name": "offset", - "in": "query", - "schema": { "type": "integer", "default": 0 } - } - ], - "responses": { - "200": { - "description": "Alerts list" - } - } - } - }, - "/api/v1/alerts/{id}/read": { - "patch": { - "tags": ["Alert"], - "summary": "Mark alert as read", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Alert marked as read" - } - } - } - }, - "/api/v1/watchlist": { - "post": { - "tags": ["Watchlist"], - "summary": "Add watchlist item", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["identifierType", "identifierValue"], - "properties": { - "identifierType": { "type": "string" }, - "identifierValue": { "type": "string" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Watchlist item added" - } - } - }, - "get": { - "tags": ["Watchlist"], - "summary": "List watchlist items", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Watchlist items" - } - } - } - }, - "/api/v1/watchlist/{id}": { - "delete": { - "tags": ["Watchlist"], - "summary": "Remove watchlist item", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Watchlist item removed" - } - } - } - }, - "/api/v1/webhook": { - "post": { - "tags": ["Webhook"], - "summary": "Process webhook event", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["eventType", "payload"], - "properties": { - "eventType": { "type": "string" }, - "payload": { "type": "object" }, - "source": { "type": "string" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Event processed", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "eventId": { "type": "string" }, - "scanTriggered": { "type": "boolean" } - } - } - } - } - }, - "400": { - "description": "Webhook processing failed" - } - } - } - }, - "/api/v1/webhook/history": { - "get": { - "tags": ["Webhook"], - "summary": "Get webhook event history", - "parameters": [ - { - "name": "limit", - "in": "query", - "schema": { "type": "integer", "default": 50 } - }, - { - "name": "offset", - "in": "query", - "schema": { "type": "integer", "default": 0 } - } - ], - "responses": { - "200": { - "description": "Event history" - } - } - } - }, - "/api/v1/webhook/user/{userId}": { - "get": { - "tags": ["Webhook"], - "summary": "Get user webhook events", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { "type": "string" } - }, - { - "name": "limit", - "in": "query", - "schema": { "type": "integer", "default": 50 } - }, - { - "name": "offset", - "in": "query", - "schema": { "type": "integer", "default": 0 } - } - ], - "responses": { - "200": { - "description": "User webhook events" - } - } - } - }, - "/api/v1/scheduler/ensure": { - "post": { - "tags": ["Scheduler"], - "summary": "Ensure scan schedule for user", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "responses": { - "200": { - "description": "Schedule ensured" - } - } - } - }, - "/api/v1/scheduler/{userId}": { - "get": { - "tags": ["Scheduler"], - "summary": "Get scan schedule for user", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Scan schedule" - }, - "404": { - "description": "Schedule not found" - } - } - } - }, - "/api/v1/scheduler/{userId}/pause": { - "post": { - "tags": ["Scheduler"], - "summary": "Pause scan schedule", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Schedule paused" - } - } - } - }, - "/api/v1/scheduler/{userId}/resume": { - "post": { - "tags": ["Scheduler"], - "summary": "Resume scan schedule", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "userId", - "in": "path", - "required": true, - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Schedule resumed" - } - } - } - }, - "/api/v1/scheduler": { - "get": { - "tags": ["Scheduler"], - "summary": "List active schedules", - "parameters": [ - { - "name": "limit", - "in": "query", - "schema": { "type": "integer", "default": 100 } - }, - { - "name": "offset", - "in": "query", - "schema": { "type": "integer", "default": 0 } - } - ], - "responses": { - "200": { - "description": "Active schedules" - } - } - } - }, - "/api/v1/dashboard": { - "get": { - "tags": ["Correlation"], - "summary": "Get dashboard data", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "timeWindow", - "in": "query", - "description": "Time window in minutes (1-10080), default 60", - "schema": { "type": "integer", "default": 60, "minimum": 1, "maximum": 10080 } - } - ], - "responses": { - "200": { - "description": "Dashboard data" - } - } - } - }, - "/api/v1/groups": { - "get": { - "tags": ["Correlation"], - "summary": "Get correlation groups", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "timeWindow", - "in": "query", - "schema": { "type": "integer", "default": 60, "minimum": 1, "maximum": 10080 } - }, - { - "name": "limit", - "in": "query", - "schema": { "type": "integer", "default": 50, "minimum": 1, "maximum": 200 } - }, - { - "name": "offset", - "in": "query", - "schema": { "type": "integer", "default": 0, "minimum": 0, "maximum": 10000 } - }, - { - "name": "status", - "in": "query", - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Correlation groups" - } - } - } - }, - "/api/v1/groups/{groupId}": { - "get": { - "tags": ["Correlation"], - "summary": "Get correlation group detail", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "groupId", - "in": "path", - "required": true, - "schema": { "type": "string", "format": "uuid" } - } - ], - "responses": { - "200": { - "description": "Correlation group detail" - }, - "404": { - "description": "Group not found" - } - } - }, - "patch": { - "tags": ["Correlation"], - "summary": "Resolve or re-open correlation group", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "groupId", - "in": "path", - "required": true, - "schema": { "type": "string", "format": "uuid" } - } - ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "status": { "type": "string", "enum": ["RESOLVED", "ACTIVE"] } - } - } - } - } - }, - "responses": { - "200": { - "description": "Group updated" - } - } - } - }, - "/api/v1/alerts/correlated": { - "get": { - "tags": ["Correlation"], - "summary": "Get correlated alerts", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "parameters": [ - { - "name": "timeWindow", - "in": "query", - "schema": { "type": "integer", "default": 60, "minimum": 1, "maximum": 10080 } - }, - { - "name": "limit", - "in": "query", - "schema": { "type": "integer", "default": 50, "minimum": 1, "maximum": 200 } - }, - { - "name": "offset", - "in": "query", - "schema": { "type": "integer", "default": 0, "minimum": 0, "maximum": 10000 } - }, - { - "name": "source", - "in": "query", - "schema": { "type": "string" } - }, - { - "name": "category", - "in": "query", - "schema": { "type": "string" } - }, - { - "name": "severity", - "in": "query", - "schema": { "type": "string" } - } - ], - "responses": { - "200": { - "description": "Correlated alerts" - } - } - } - }, - "/api/v1/ingest/darkwatch": { - "post": { - "tags": ["Correlation"], - "summary": "Ingest DarkWatch alert", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["sourceAlertId", "breachName", "severity", "channel"], - "properties": { - "sourceAlertId": { "type": "string" }, - "exposureId": { "type": "string" }, - "breachName": { "type": "string", "maxLength": 500 }, - "severity": { "type": "string", "maxLength": 20 }, - "channel": { "type": "string", "maxLength": 50 }, - "dataType": { "type": "array", "items": { "type": "string" } }, - "dataSource": { "type": "string", "maxLength": 100 } - } - } - } - } - }, - "responses": { - "201": { - "description": "Alert ingested" - } - } - } - }, - "/api/v1/ingest/spamshield": { - "post": { - "tags": ["Correlation"], - "summary": "Ingest SpamShield alert", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["sourceAlertId", "phoneNumber", "decision", "confidence"], - "properties": { - "sourceAlertId": { "type": "string" }, - "phoneNumber": { "type": "string", "maxLength": 20 }, - "decision": { "type": "string", "enum": ["BLOCK", "FLAG", "ALLOW"] }, - "confidence": { "type": "number", "minimum": 0, "maximum": 1 }, - "reasons": { "type": "array", "items": { "type": "string" } }, - "channel": { "type": "string", "enum": ["call", "sms"] }, - "hiyaReputationScore": { "type": "number" }, - "truecallerSpamScore": { "type": "number" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Alert ingested" - } - } - } - }, - "/api/v1/ingest/voiceprint": { - "post": { - "tags": ["Correlation"], - "summary": "Ingest VoicePrint alert", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["sourceAlertId", "jobId", "verdict", "syntheticScore", "confidence"], - "properties": { - "sourceAlertId": { "type": "string" }, - "jobId": { "type": "string" }, - "verdict": { "type": "string", "enum": ["SYNTHETIC", "NATURAL", "UNCERTAIN"] }, - "syntheticScore": { "type": "number", "minimum": 0, "maximum": 1 }, - "confidence": { "type": "number", "minimum": 0, "maximum": 1 }, - "matchedEnrollmentId": { "type": "string" }, - "matchedSimilarity": { "type": "number" }, - "analysisType": { "type": "string", "maxLength": 50 } - } - } - } - } - }, - "responses": { - "201": { - "description": "Alert ingested" - } - } - } - }, - "/api/v1/ingest/call-analysis": { - "post": { - "tags": ["Correlation"], - "summary": "Ingest call analysis alert", - "security": [{ "bearerAuth": [] }, { "apiKeyAuth": [] }], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["sourceAlertId", "callId"], - "properties": { - "sourceAlertId": { "type": "string" }, - "callId": { "type": "string" }, - "eventType": { "type": "string", "maxLength": 100 }, - "mosScore": { "type": "number", "minimum": 1, "maximum": 5 }, - "anomaly": { "type": "string", "maxLength": 500 }, - "sentiment": { - "type": "object", - "properties": { - "label": { "type": "string", "maxLength": 50 }, - "score": { "type": "number", "minimum": 0, "maximum": 1 } - } - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Alert ingested" - } - } - } - }, - "/removebrokers/brokers": { - "get": { - "tags": ["Info Broker Removal"], - "summary": "List available data brokers", - "security": [{"bearerAuth": []}], - "parameters": [ - { - "name": "category", - "in": "query", - "schema": { "type": "string", "enum": ["PEOPLE_SEARCH", "BACKGROUND_CHECK", "PUBLIC_RECORDS", "REVERSE_LOOKUP", "SOCIAL_MEDIA"] }, - "description": "Filter by broker category" - } - ], - "responses": { - "200": { - "description": "List of available brokers", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "brokers": { - "type": "array", - "items": { "$ref": "#/components/schemas/BrokerInfo" } - } - } - } - } - } - }, - "401": { "description": "Unauthorized" } - } - } - }, - "/removebrokers/status": { - "get": { - "tags": ["Info Broker Removal"], - "summary": "Get removal request status for user", - "security": [{"bearerAuth": []}], - "responses": { - "200": { - "description": "Removal status overview", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "stats": { - "type": "object", - "properties": { - "total": { "type": "integer" }, - "pending": { "type": "integer" }, - "submitted": { "type": "integer" }, - "completed": { "type": "integer" }, - "failed": { "type": "integer" } - } - }, - "limit": { "type": "integer" }, - "remaining": { "type": "integer" }, - "tier": { "type": "string" }, - "requests": { - "type": "array", - "items": { "$ref": "#/components/schemas/RemovalRequestStatus" } - } - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "402": { "description": "Subscription required" } - } - } - }, - "/removebrokers/scan": { - "post": { - "tags": ["Info Broker Removal"], - "summary": "Scan for personal listings across brokers", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["fullName"], - "properties": { - "fullName": { "type": "string" }, - "email": { "type": "string" }, - "phone": { "type": "string" }, - "address": { "type": "string" } - } - } - } - } - }, - "responses": { - "200": { - "description": "Scan results", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "brokersScanned": { "type": "integer" }, - "listingsFound": { "type": "integer" }, - "results": { "type": "array", "items": { "$ref": "#/components/schemas/ScanResult" } } - } - } - } - } - }, - "400": { "description": "Invalid request" }, - "401": { "description": "Unauthorized" }, - "402": { "description": "Subscription required" } - } - } - }, - "/removebrokers/request": { - "post": { - "tags": ["Info Broker Removal"], - "summary": "Create a new removal request", - "security": [{"bearerAuth": []}], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": ["brokerId", "fullName"], - "properties": { - "brokerId": { "type": "string" }, - "fullName": { "type": "string" }, - "email": { "type": "string" }, - "phone": { "type": "string" }, - "address": { - "type": "object", - "properties": { - "street": { "type": "string" }, - "city": { "type": "string" }, - "state": { "type": "string" }, - "zip": { "type": "string" } - } - }, - "dob": { "type": "string" }, - "notes": { "type": "string" } - } - } - } - } - }, - "responses": { - "201": { - "description": "Removal request created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "request": { "$ref": "#/components/schemas/RemovalRequestOutput" } - } - } - } - } - }, - "400": { "description": "Invalid request or limit reached" }, - "401": { "description": "Unauthorized" }, - "402": { "description": "Subscription required" }, - "422": { "description": "Duplicate request" } - } - } - }, - "/removebrokers/request/{id}": { - "get": { - "tags": ["Info Broker Removal"], - "summary": "Get specific removal request", - "security": [{"bearerAuth": []}], - "parameters": [ - { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } - ], - "responses": { - "200": { - "description": "Removal request details", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "request": { "$ref": "#/components/schemas/RemovalRequestDetail" } - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "402": { "description": "Subscription required" }, - "404": { "description": "Not found" } - } - }, - "delete": { - "tags": ["Info Broker Removal"], - "summary": "Cancel a removal request", - "security": [{"bearerAuth": []}], - "parameters": [ - { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } - ], - "responses": { - "200": { - "description": "Request cancelled", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "request": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "status": { "type": "string", "enum": ["CANCELLED"] } - } - } - } - } - } - } - }, - "400": { "description": "Cannot cancel completed request" }, - "401": { "description": "Unauthorized" }, - "402": { "description": "Subscription required" }, - "404": { "description": "Not found" } - } - } - }, - "/removebrokers/process": { - "post": { - "tags": ["Info Broker Removal"], - "summary": "Trigger processing of pending removals (admin)", - "security": [{"bearerAuth": []}], - "responses": { - "200": { - "description": "Processing results", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "processed": { "type": "integer" }, - "results": { "type": "array", "items": { "type": "object" } } - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "403": { "description": "Admin access required" } - } - } - }, - "/removebrokers/verify/{id}": { - "post": { - "tags": ["Info Broker Removal"], - "summary": "Manually verify a removal", - "security": [{"bearerAuth": []}], - "parameters": [ - { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } } - ], - "responses": { - "200": { - "description": "Verification result", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "requestId": { "type": "string" }, - "completed": { "type": "boolean" }, - "stillListed": { "type": "boolean" } - } - } - } - } - }, - "401": { "description": "Unauthorized" }, - "402": { "description": "Subscription required" }, - "404": { "description": "Not found" } - } - } - } - }, - - "components": { - "securitySchemes": { - "bearerAuth": { - "type": "http", - "scheme": "bearer", - "bearerFormat": "JWT", - "description": "JWT token obtained from authentication. Set JWT_SECRET or NEXTAUTH_SECRET server-side." - }, - "apiKeyAuth": { - "type": "apiKey", - "in": "header", - "name": "X-API-Key", - "description": "API key for service-to-service authentication. Keys prefixed with 'premium_' get premium rate limits, 'plus_' get plus rate limits." - } - }, - "schemas": { - "ErrorResponse": { - "type": "object", - "required": ["error", "message", "statusCode", "timestamp", "path"], - "properties": { - "error": { - "type": "string", - "description": "Error name/type" - }, - "message": { - "type": "string", - "description": "Human-readable error message" - }, - "statusCode": { - "type": "integer", - "description": "HTTP status code" - }, - "code": { - "type": "string", - "description": "Machine-readable error code" - }, - "details": { - "type": "object", - "description": "Additional error details" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "ISO 8601 timestamp" - }, - "path": { - "type": "string", - "description": "Request path" - } - } - }, - "RateLimitResponse": { - "type": "object", - "properties": { - "error": { - "type": "string", - "enum": ["Too Many Requests"] - }, - "message": { - "type": "string" - }, - "tier": { - "type": "string", - "enum": ["basic", "plus", "premium"] - }, - "limit": { - "type": "integer" - }, - "reset": { - "type": "string", - "format": "date-time" - } - } - }, - "RateLimitHeaders": { - "type": "object", - "description": "Rate limit headers returned on every response (except /health)", - "properties": { - "X-RateLimit-Limit": { - "type": "integer", - "description": "Maximum requests per window" - }, - "X-RateLimit-Remaining": { - "type": "integer", - "description": "Remaining requests in current window" - }, - "X-RateLimit-Reset": { - "type": "integer", - "description": "Unix timestamp when the rate limit window resets" - }, - "Retry-After": { - "type": "integer", - "description": "Seconds until retry is possible (only on 429 responses)" - } - } - }, - "RateLimitTiers": { - "type": "object", - "description": "Rate limits by tier (requests per minute)", - "properties": { - "basic": { - "type": "object", - "properties": { - "windowMs": { "type": "integer", "example": 60000 }, - "maxRequests": { "type": "integer", "example": 100 } - } - }, - "plus": { - "type": "object", - "properties": { - "windowMs": { "type": "integer", "example": 60000 }, - "maxRequests": { "type": "integer", "example": 500 } - } - }, - "premium": { - "type": "object", - "properties": { - "windowMs": { "type": "integer", "example": 60000 }, - "maxRequests": { "type": "integer", "example": 2000 } - } - } - } - }, - "SubscriptionTiers": { - "type": "object", - "description": "Subscription tiers and their feature limits", - "properties": { - "free": { - "type": "object", - "properties": { - "maxWatchlistItems": { "type": "integer" }, - "maxProperties": { "type": "integer" } - } - }, - "basic": { - "type": "object", - "properties": { - "maxWatchlistItems": { "type": "integer" }, - "maxProperties": { "type": "integer" } - } - }, - "plus": { - "type": "object", - "properties": { - "maxWatchlistItems": { "type": "integer" }, - "maxProperties": { "type": "integer" } - } - }, - "premium": { - "type": "object", - "properties": { - "maxWatchlistItems": { "type": "integer" }, - "maxProperties": { "type": "integer" } - } - } - } - }, - "BrokerInfo": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "name": { "type": "string" }, - "domain": { "type": "string" }, - "category": { "type": "string", "enum": ["PEOPLE_SEARCH", "BACKGROUND_CHECK", "PUBLIC_RECORDS", "REVERSE_LOOKUP", "SOCIAL_MEDIA"] }, - "removalMethod": { "type": "string", "enum": ["AUTOMATED", "MANUAL_FORM", "EMAIL", "PHONE", "MAIL", "NONE"] }, - "requiresAccount": { "type": "boolean" }, - "requiresVerification": { "type": "boolean" }, - "estimatedDays": { "type": "integer" }, - "removalUrl": { "type": "string", "nullable": true } - } - }, - "RemovalRequestStatus": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "brokerId": { "type": "string" }, - "brokerName": { "type": "string" }, - "status": { "type": "string", "enum": ["PENDING", "SUBMITTED", "IN_PROGRESS", "COMPLETED", "FAILED", "REJECTED", "CANCELLED"] }, - "method": { "type": "string" }, - "attempts": { "type": "integer" }, - "submittedAt": { "type": "string", "format": "date-time", "nullable": true }, - "completedAt": { "type": "string", "format": "date-time", "nullable": true }, - "error": { "type": "string", "nullable": true }, - "createdAt": { "type": "string", "format": "date-time" }, - "updatedAt": { "type": "string", "format": "date-time" } - } - }, - "RemovalRequestOutput": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "brokerId": { "type": "string" }, - "status": { "type": "string" }, - "method": { "type": "string" }, - "createdAt": { "type": "string", "format": "date-time" } - } - }, - "RemovalRequestDetail": { - "type": "object", - "properties": { - "id": { "type": "string" }, - "brokerId": { "type": "string" }, - "brokerName": { "type": "string", "nullable": true }, - "status": { "type": "string", "enum": ["PENDING", "SUBMITTED", "IN_PROGRESS", "COMPLETED", "FAILED", "REJECTED", "CANCELLED"] }, - "method": { "type": "string" }, - "attempts": { "type": "integer" }, - "submittedAt": { "type": "string", "format": "date-time", "nullable": true }, - "completedAt": { "type": "string", "format": "date-time", "nullable": true }, - "error": { "type": "string", "nullable": true }, - "notes": { "type": "string", "nullable": true }, - "createdAt": { "type": "string", "format": "date-time" }, - "updatedAt": { "type": "string", "format": "date-time" } - } - }, - "ScanResult": { - "type": "object", - "properties": { - "brokerId": { "type": "string" }, - "brokerName": { "type": "string" }, - "found": { "type": "boolean" }, - "listingId": { "type": "string", "nullable": true }, - "url": { "type": "string", "nullable": true } - } - } - } - }, - - "security": [ - { "bearerAuth": [] }, - { "apiKeyAuth": [] } - ] -} diff --git a/packages/api/src/routes/alert.routes.ts b/packages/api/src/routes/alert.routes.ts deleted file mode 100644 index 67fe7b8..0000000 --- a/packages/api/src/routes/alert.routes.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { FastifyInstance } from "fastify"; -import { AlertPipeline } from "@shieldai/darkwatch"; - -export function alertRoutes(fastify: FastifyInstance) { - const pipeline = new AlertPipeline(); - - fastify.get("/", async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const limit = parseInt(request.query.limit as string) || 50; - const offset = parseInt(request.query.offset as string) || 0; - const alerts = await pipeline.getUserAlerts(userId, limit, offset); - return reply.send(alerts); - }); - - fastify.patch("/:id/read", async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { - return reply.code(401).send({ error: "User not authenticated" }); - } - - await pipeline.markRead(request.params.id, userId); - return reply.send({ read: true }); - }); -} diff --git a/packages/api/src/routes/blog-admin.routes.ts b/packages/api/src/routes/blog-admin.routes.ts deleted file mode 100644 index 5704166..0000000 --- a/packages/api/src/routes/blog-admin.routes.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { prisma } from '@shieldai/db'; - -interface CreatePostBody { - slug: string; - title: string; - excerpt?: string; - content: string; - authorName?: string; - coverImageUrl?: string; - tags?: string[]; - published?: boolean; - publishedAt?: string; -} - -export async function blogAdminRoutes(fastify: FastifyInstance) { - fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string; role?: string } }; - const user = authReq.user; - - if (!user) { - return reply.code(401).send({ error: 'Unauthorized' }); - } - - if (user.role !== 'support') { - return reply.code(403).send({ error: 'Admin access required' }); - } - }); - - fastify.post('/admin/blog', async (request: FastifyRequest, reply: FastifyReply) => { - const body = request.body as CreatePostBody; - - if (!body.slug || !/^[a-z0-9-]+$/.test(body.slug)) { - return reply.code(400).send({ error: 'Invalid slug: must be lowercase alphanumeric with hyphens' }); - } - if (!body.title || body.title.length > 200) { - return reply.code(400).send({ error: 'Title is required (max 200 chars)' }); - } - if (!body.content) { - return reply.code(400).send({ error: 'Content is required' }); - } - - const existing = await prisma.blogPost.findUnique({ - where: { slug: body.slug }, - }); - - if (existing) { - return reply.code(409).send({ error: 'A post with this slug already exists' }); - } - - const post = await prisma.blogPost.create({ - data: { - slug: body.slug, - title: body.title, - excerpt: body.excerpt || null, - content: body.content, - authorName: body.authorName || null, - coverImageUrl: body.coverImageUrl || null, - tags: body.tags || [], - published: body.published || false, - publishedAt: body.publishedAt - ? new Date(body.publishedAt) - : body.published - ? new Date() - : null, - }, - }); - - return reply.code(201).send({ post }); - }); - - fastify.put('/admin/blog/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const { id } = request.params as { id: string }; - const body = request.body as Partial; - - const existing = await prisma.blogPost.findUnique({ where: { id } }); - if (!existing) { - return reply.code(404).send({ error: 'Post not found' }); - } - - if (body.slug && body.slug !== existing.slug) { - const slugExists = await prisma.blogPost.findUnique({ where: { slug: body.slug } }); - if (slugExists) { - return reply.code(409).send({ error: 'A post with this slug already exists' }); - } - } - - const post = await prisma.blogPost.update({ - where: { id }, - data: { - ...(body.slug !== undefined && { slug: body.slug }), - ...(body.title !== undefined && { title: body.title }), - ...(body.excerpt !== undefined && { excerpt: body.excerpt }), - ...(body.content !== undefined && { content: body.content }), - ...(body.authorName !== undefined && { authorName: body.authorName }), - ...(body.coverImageUrl !== undefined && { coverImageUrl: body.coverImageUrl }), - ...(body.tags !== undefined && { tags: body.tags }), - ...(body.published !== undefined && { published: body.published }), - publishedAt: body.publishedAt - ? new Date(body.publishedAt) - : body.published === true && !existing.published - ? new Date() - : undefined, - }, - }); - - return reply.send({ post }); - }); - - fastify.delete('/admin/blog/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const { id } = request.params as { id: string }; - await prisma.blogPost.delete({ where: { id } }); - return reply.code(204).send(); - }); - - fastify.get('/admin/blog', async (request: FastifyRequest, reply: FastifyReply) => { - const query = request.query as { page?: string; limit?: string }; - const page = Math.max(1, parseInt(query.page || '1', 10)); - const limit = Math.min(50, Math.max(1, parseInt(query.limit || '20', 10))); - const skip = (page - 1) * limit; - - const [posts, total] = await Promise.all([ - prisma.blogPost.findMany({ - orderBy: { createdAt: 'desc' }, - skip, - take: limit, - }), - prisma.blogPost.count(), - ]); - - return reply.send({ - posts, - pagination: { page, limit, total, totalPages: Math.ceil(total / limit) }, - }); - }); -} diff --git a/packages/api/src/routes/blog.routes.ts b/packages/api/src/routes/blog.routes.ts deleted file mode 100644 index 8a07614..0000000 --- a/packages/api/src/routes/blog.routes.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { prisma } from '@shieldai/db'; - -interface BlogQuery { - page?: string; - limit?: string; - tag?: string; -} - -export async function blogRoutes(fastify: FastifyInstance) { - fastify.get('/blog', async (request: FastifyRequest, reply: FastifyReply) => { - const query = request.query as BlogQuery; - const page = Math.max(1, parseInt(query.page || '1', 10)); - const limit = Math.min(50, Math.max(1, parseInt(query.limit || '10', 10))); - const skip = (page - 1) * limit; - - const where = { - published: true, - ...(query.tag ? { tags: { has: query.tag } } : {}), - }; - - const [posts, total] = await Promise.all([ - prisma.blogPost.findMany({ - where, - orderBy: { publishedAt: 'desc' }, - skip, - take: limit, - select: { - id: true, - slug: true, - title: true, - excerpt: true, - authorName: true, - coverImageUrl: true, - tags: true, - publishedAt: true, - viewCount: true, - }, - }), - prisma.blogPost.count({ where }), - ]); - - return reply.send({ - posts, - pagination: { - page, - limit, - total, - totalPages: Math.ceil(total / limit), - }, - }); - }); - - fastify.get('/blog/:slug', async (request: FastifyRequest, reply: FastifyReply) => { - const { slug } = request.params as { slug: string }; - - const post = await prisma.blogPost.findUnique({ - where: { slug }, - }); - - if (!post || !post.published) { - return reply.code(404).send({ error: 'Post not found' }); - } - - await prisma.blogPost.update({ - where: { id: post.id }, - data: { viewCount: { increment: 1 } }, - }); - - return reply.send({ post }); - }); -} diff --git a/packages/api/src/routes/correlation.routes.ts b/packages/api/src/routes/correlation.routes.ts deleted file mode 100644 index ef8f384..0000000 --- a/packages/api/src/routes/correlation.routes.ts +++ /dev/null @@ -1,413 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; -import { correlationService } from "@shieldai/correlation"; - -type AuthUser = { id?: string }; - -function getUserId(request: FastifyRequest): string | undefined { - return (request.user as AuthUser | undefined)?.id; -} - -const timeWindowSchema = { - type: "object", - properties: { - timeWindow: { type: "integer", minimum: 1, maximum: 10080 }, - }, -}; - -const paginatedQuerySchema = { - type: "object", - properties: { - timeWindow: { type: "integer", minimum: 1, maximum: 10080 }, - limit: { type: "integer", minimum: 1, maximum: 200 }, - offset: { type: "integer", minimum: 0, maximum: 10000 }, - }, -}; - -export function correlationRoutes(fastify: FastifyInstance) { - fastify.get( - "/dashboard", - { - schema: { - ...timeWindowSchema, - response: { - "400": { - type: "object", - properties: { - error: { type: "string" }, - }, - }, - "401": { - type: "object", - properties: { - error: { type: "string" }, - }, - }, - }, - }, - }, - async (request, reply) => { - const userId = getUserId(request); - if (!userId || userId === "anonymous") { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const query = request.query as Record; - const timeWindow = - typeof query.timeWindow === "number" ? query.timeWindow : 60; - const data = await correlationService.getDashboardData(userId, timeWindow); - return reply.send(data); - } - ); - - fastify.get( - "/groups", - { - schema: { - ...paginatedQuerySchema, - response: { - "400": { - type: "object", - properties: { - error: { type: "string" }, - }, - }, - "401": { - type: "object", - properties: { - error: { type: "string" }, - }, - }, - }, - }, - }, - async (request, reply) => { - const userId = getUserId(request); - if (!userId || userId === "anonymous") { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const query = request.query as Record; - const result = await correlationService.getCorrelationGroups({ - userId, - status: (query.status as any) || undefined, - timeWindowMinutes: - typeof query.timeWindow === "number" ? query.timeWindow : 60, - limit: typeof query.limit === "number" ? query.limit : 50, - offset: typeof query.offset === "number" ? query.offset : 0, - }); - return reply.send(result); - } - ); - - fastify.get( - "/groups/:groupId", - { - schema: { - params: { - type: "object", - properties: { - groupId: { type: "string", format: "uuid" }, - }, - required: ["groupId"], - }, - }, - }, - async (request, reply) => { - const userId = getUserId(request); - if (!userId || userId === "anonymous") { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const groupId = (request.params as Record).groupId; - const group = await correlationService.getGroupById(groupId, userId); - - if (!group) { - return reply.code(404).send({ error: "Correlation group not found" }); - } - - return reply.send(group); - } - ); - - fastify.patch( - "/groups/:groupId/resolve", - { - schema: { - params: { - type: "object", - properties: { - groupId: { type: "string", format: "uuid" }, - }, - required: ["groupId"], - }, - body: { - type: "object", - properties: { - status: { type: "string", enum: ["RESOLVED", "ACTIVE"] }, - }, - additionalProperties: false, - }, - }, - }, - async (request, reply) => { - const userId = getUserId(request); - if (!userId || userId === "anonymous") { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const groupId = (request.params as Record).groupId; - const body = request.body as Record | undefined; - const status = body?.status || "RESOLVED"; - const group = await correlationService.resolveGroup( - groupId, - userId, - status - ); - - if (!group) { - return reply.code(404).send({ error: "Correlation group not found" }); - } - - return reply.send(group); - } - ); - - fastify.get( - "/alerts", - { - schema: { - ...paginatedQuerySchema, - response: { - "400": { - type: "object", - properties: { - error: { type: "string" }, - }, - }, - "401": { - type: "object", - properties: { - error: { type: "string" }, - }, - }, - }, - }, - }, - async (request, reply) => { - const userId = getUserId(request); - if (!userId || userId === "anonymous") { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const query = request.query as Record; - const result = await correlationService.getCorrelatedAlerts({ - userId, - source: (query.source as any) || undefined, - category: (query.category as any) || undefined, - severity: (query.severity as any) || undefined, - timeWindowMinutes: - typeof query.timeWindow === "number" ? query.timeWindow : 60, - limit: typeof query.limit === "number" ? query.limit : 50, - offset: typeof query.offset === "number" ? query.offset : 0, - }); - return reply.send(result); - } - ); - - fastify.post( - "/ingest/darkwatch", - { - schema: { - body: { - type: "object", - properties: { - sourceAlertId: { type: "string" }, - exposureId: { type: "string" }, - breachName: { type: "string", maxLength: 500 }, - severity: { type: "string", maxLength: 20 }, - channel: { type: "string", maxLength: 50 }, - dataType: { type: "array", items: { type: "string" } }, - dataSource: { type: "string", maxLength: 100 }, - }, - required: ["sourceAlertId", "breachName", "severity", "channel"], - additionalProperties: false, - }, - }, - }, - async (request, reply) => { - const userId = getUserId(request); - if (!userId || userId === "anonymous") { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const body = request.body as Record; - const alert = await correlationService.ingestDarkWatchAlert( - userId, - body.sourceAlertId as string, - { - exposureId: body.exposureId as string, - breachName: body.breachName as string, - severity: body.severity as string, - channel: body.channel as string, - dataType: body.dataType as string[] | undefined, - dataSource: body.dataSource as string | undefined, - } - ); - return reply.code(201).send(alert); - } - ); - - fastify.post( - "/ingest/spamshield", - { - schema: { - body: { - type: "object", - properties: { - sourceAlertId: { type: "string" }, - phoneNumber: { type: "string", maxLength: 20 }, - decision: { type: "string", enum: ["BLOCK", "FLAG", "ALLOW"] }, - confidence: { type: "number", minimum: 0, maximum: 1 }, - reasons: { type: "array", items: { type: "string" } }, - channel: { type: "string", enum: ["call", "sms"] }, - hiyaReputationScore: { type: "number" }, - truecallerSpamScore: { type: "number" }, - }, - required: ["sourceAlertId", "phoneNumber", "decision", "confidence"], - additionalProperties: false, - }, - }, - }, - async (request, reply) => { - const userId = getUserId(request); - if (!userId || userId === "anonymous") { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const body = request.body as Record; - const alert = await correlationService.ingestSpamShieldAlert( - userId, - body.sourceAlertId as string, - { - phoneNumber: body.phoneNumber as string, - decision: body.decision as string, - confidence: body.confidence as number, - reasons: body.reasons as string[] | undefined, - channel: body.channel as "call" | "sms" | undefined, - hiyaReputationScore: body.hiyaReputationScore as - | number - | undefined, - truecallerSpamScore: body.truecallerSpamScore as - | number - | undefined, - } - ); - return reply.code(201).send(alert); - } - ); - - fastify.post( - "/ingest/voiceprint", - { - schema: { - body: { - type: "object", - properties: { - sourceAlertId: { type: "string" }, - jobId: { type: "string" }, - verdict: { - type: "string", - enum: ["SYNTHETIC", "NATURAL", "UNCERTAIN"], - }, - syntheticScore: { type: "number", minimum: 0, maximum: 1 }, - confidence: { type: "number", minimum: 0, maximum: 1 }, - matchedEnrollmentId: { type: "string" }, - matchedSimilarity: { type: "number" }, - analysisType: { type: "string", maxLength: 50 }, - }, - required: [ - "sourceAlertId", - "jobId", - "verdict", - "syntheticScore", - "confidence", - ], - additionalProperties: false, - }, - }, - }, - async (request, reply) => { - const userId = getUserId(request); - if (!userId || userId === "anonymous") { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const body = request.body as Record; - const alert = await correlationService.ingestVoicePrintAlert( - userId, - body.sourceAlertId as string, - { - jobId: body.jobId as string, - verdict: body.verdict as string, - syntheticScore: body.syntheticScore as number, - confidence: body.confidence as number, - matchedEnrollmentId: body.matchedEnrollmentId as - | string - | undefined, - matchedSimilarity: body.matchedSimilarity as number | undefined, - analysisType: body.analysisType as string | undefined, - } - ); - return reply.code(201).send(alert); - } - ); - - fastify.post( - "/ingest/call-analysis", - { - schema: { - body: { - type: "object", - properties: { - sourceAlertId: { type: "string" }, - callId: { type: "string" }, - eventType: { type: "string", maxLength: 100 }, - mosScore: { type: "number", minimum: 1, maximum: 5 }, - anomaly: { type: "string", maxLength: 500 }, - sentiment: { - type: "object", - properties: { - label: { type: "string", maxLength: 50 }, - score: { type: "number", minimum: 0, maximum: 1 }, - }, - }, - }, - required: ["sourceAlertId", "callId"], - additionalProperties: false, - }, - }, - }, - async (request, reply) => { - const userId = getUserId(request); - if (!userId || userId === "anonymous") { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const body = request.body as Record; - const alert = await correlationService.ingestCallAnalysisAlert( - userId, - body.sourceAlertId as string, - { - callId: body.callId as string, - eventType: body.eventType as string | undefined, - mosScore: body.mosScore as number | undefined, - anomaly: body.anomaly as string | undefined, - sentiment: body.sentiment as - | { label: string; score: number } - | undefined, - } - ); - return reply.code(201).send(alert); - } - ); -} diff --git a/packages/api/src/routes/darkwatch.routes.ts b/packages/api/src/routes/darkwatch.routes.ts deleted file mode 100644 index 634fd5c..0000000 --- a/packages/api/src/routes/darkwatch.routes.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { prisma, SubscriptionTier } from '@shieldai/db'; -import { tierConfig, SubscriptionTier as BillingTier } from '@shieldsai/shared-billing'; -import { - watchlistService, - scanService, - schedulerService, - webhookService, -} from '../services/darkwatch'; - -export async function darkwatchRoutes(fastify: FastifyInstance) { - const authed = async ( - request: FastifyRequest, - reply: FastifyReply - ): Promise => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - reply.code(401).send({ error: 'User ID required' }); - return null; - } - - const subscription = await prisma.subscription.findFirst({ - where: { userId, status: 'active' }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - reply.code(404).send({ error: 'Active subscription not found' }); - return null; - } - - return subscription.id; - }; - - // GET /darkwatch/watchlist - List watchlist items - fastify.get('/watchlist', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await authed(request, reply); - if (!subscriptionId) return; - - try { - const items = await watchlistService.getItems(subscriptionId); - return reply.send({ items }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to list watchlist'; - return reply.code(500).send({ error: message }); - } - }); - - // POST /darkwatch/watchlist - Add watchlist item - fastify.post('/watchlist', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const subscription = await prisma.subscription.findFirst({ - where: { userId, status: 'active' }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - return reply.code(404).send({ error: 'Active subscription not found' }); - } - - const body = request.body as { type: string; value: string }; - - if (!body.type || !body.value) { - return reply.code(400).send({ error: 'type and value are required' }); - } - - const maxItems = tierConfig[subscription.tier as BillingTier].features.maxWatchlistItems; - - try { - const item = await watchlistService.addItem( - subscription.id, - body.type, - body.value, - maxItems - ); - return reply.code(201).send({ item }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to add watchlist item'; - return reply.code(422).send({ error: message }); - } - }); - - // DELETE /darkwatch/watchlist/:id - Remove watchlist item - fastify.delete('/watchlist/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await authed(request, reply); - if (!subscriptionId) return; - - const id = (request.params as { id: string }).id; - - try { - const item = await watchlistService.removeItem(id, subscriptionId); - return reply.send({ item }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to remove watchlist item'; - return reply.code(422).send({ error: message }); - } - }); - - // POST /darkwatch/scan - Trigger on-demand scan - fastify.post('/scan', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await authed(request, reply); - if (!subscriptionId) return; - - try { - const job = await schedulerService.enqueueOnDemandScan(subscriptionId); - return reply.send({ - job: { - id: job?.id, - status: 'queued', - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to trigger scan'; - return reply.code(422).send({ error: message }); - } - }); - - // GET /darkwatch/scan/schedule - Get scan schedule - fastify.get('/scan/schedule', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await authed(request, reply); - if (!subscriptionId) return; - - try { - const schedule = await schedulerService.getScanSchedule(subscriptionId); - return reply.send({ schedule }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to get schedule'; - return reply.code(500).send({ error: message }); - } - }); - - // GET /darkwatch/exposures - List exposures - fastify.get('/exposures', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await authed(request, reply); - if (!subscriptionId) return; - - try { - const exposures = await prisma.exposure.findMany({ - where: { subscriptionId }, - orderBy: { detectedAt: 'desc' }, - take: 50, - include: { - watchlistItem: true, - }, - }); - return reply.send({ exposures }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to list exposures'; - return reply.code(500).send({ error: message }); - } - }); - - // GET /darkwatch/alerts - List alerts - fastify.get('/alerts', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - try { - const alerts = await prisma.alert.findMany({ - where: { userId }, - orderBy: { createdAt: 'desc' }, - take: 50, - include: { - exposure: true, - }, - }); - return reply.send({ alerts }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to list alerts'; - return reply.code(500).send({ error: message }); - } - }); - - // PATCH /darkwatch/alerts/:id/read - Mark alert as read - fastify.patch('/alerts/:id/read', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const id = (request.params as { id: string }).id; - - try { - const alert = await prisma.alert.update({ - where: { id }, - data: { isRead: true, readAt: new Date() }, - }); - return reply.send({ alert }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to mark alert as read'; - return reply.code(422).send({ error: message }); - } - }); - - // POST /darkwatch/webhook - External webhook receiver - fastify.post('/webhook', async (request: FastifyRequest, reply: FastifyReply) => { - const body = request.body as Record; - - const source = typeof body.source === 'string' ? body.source : ''; - const identifier = typeof body.identifier === 'string' ? body.identifier : ''; - const identifierType = typeof body.identifierType === 'string' ? body.identifierType : ''; - const metadata = body.metadata as Record | undefined; - const timestamp = typeof body.timestamp === 'string' ? body.timestamp : new Date().toISOString(); - - if (!source || !identifier || !identifierType) { - return reply.code(400).send({ - error: 'source, identifier, and identifierType are required', - }); - } - - const signature = request.headers['x-webhook-signature'] as string | undefined; - const webhookTimestamp = request.headers['x-webhook-timestamp'] as string | undefined; - - if (!signature || !webhookTimestamp) { - return reply.code(401).send({ error: 'Webhook signature and timestamp required' }); - } - - const valid = await webhookService.verifyWebhookSignature( - JSON.stringify(body), - signature, - webhookTimestamp - ); - if (!valid) { - return reply.code(401).send({ error: 'Invalid webhook signature' }); - } - - try { - const result = await webhookService.processExternalWebhook({ - source, - identifier, - identifierType, - metadata, - timestamp, - }); - - return reply.send({ - processed: true, - exposuresCreated: result.exposuresCreated, - alertsCreated: result.alertsCreated, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Webhook processing failed'; - console.error('[DarkWatch:Webhook] Error:', message); - return reply.code(500).send({ error: 'Webhook processing failed' }); - } - }); - - // POST /darkwatch/scheduler/init - Initialize scheduled scans for all subscriptions - fastify.post('/scheduler/init', async (request: FastifyRequest, reply: FastifyReply) => { - try { - const jobsEnqueued = await schedulerService.scheduleSubscriptionScans(); - return reply.send({ - scheduled: jobsEnqueued.length, - jobs: jobsEnqueued, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Scheduler init failed'; - return reply.code(500).send({ error: message }); - } - }); - - // POST /darkwatch/scheduler/reschedule - Reschedule all scans - fastify.post('/scheduler/reschedule', async (request: FastifyRequest, reply: FastifyReply) => { - try { - const jobsEnqueued = await schedulerService.rescheduleAll(); - return reply.send({ - rescheduled: jobsEnqueued.length, - jobs: jobsEnqueued, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Scheduler reschedule failed'; - return reply.code(500).send({ error: message }); - } - }); -} diff --git a/packages/api/src/routes/device.routes.ts b/packages/api/src/routes/device.routes.ts deleted file mode 100644 index ef59382..0000000 --- a/packages/api/src/routes/device.routes.ts +++ /dev/null @@ -1,342 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { prisma } from '@shieldai/db'; - -// In-memory rate limiter for device registration -const registrationAttempts = new Map(); -const REGISTRATION_RATE_LIMIT = 10; // max registrations per window -const REGISTRATION_WINDOW_MS = 5 * 60 * 1000; // 5 minutes - -function checkRegistrationRateLimit(key: string): boolean { - const now = Date.now(); - const record = registrationAttempts.get(key); - - if (!record || now > record.resetAt) { - registrationAttempts.set(key, { count: 1, resetAt: now + REGISTRATION_WINDOW_MS }); - return true; - } - - if (record.count >= REGISTRATION_RATE_LIMIT) { - return false; - } - - record.count++; - return true; -} - -// Cleanup stale rate limit entries every 5 minutes -setInterval(() => { - const now = Date.now(); - for (const [key, record] of registrationAttempts.entries()) { - if (now > record.resetAt) { - registrationAttempts.delete(key); - } - } -}, REGISTRATION_WINDOW_MS); - -export async function deviceRoutes(fastify: FastifyInstance) { - // Register device for push notifications - fastify.post( - '/devices/register', - { - preHandler: async (request, reply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - return reply.status(401).send({ error: 'Authentication required' }); - } - }, - }, - async (request, reply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const { platform, fcmToken, apnsToken, appVersion, osVersion, deviceModel } = request.body as { - platform: 'ios' | 'android'; - fcmToken?: string; - apnsToken?: string; - appVersion: string; - osVersion: string; - deviceModel?: string; - }; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - if (!platform || !appVersion || !osVersion) { - return reply.status(400).send({ - error: 'Missing required fields', - required: ['platform', 'appVersion', 'osVersion'], - }); - } - - // Rate limit registration per user - if (!checkRegistrationRateLimit(authReq.user.id)) { - return reply.status(429).send({ - error: 'Too many registration attempts', - retryAfter: Math.ceil(REGISTRATION_WINDOW_MS / 1000), - }); - } - - if (platform === 'android' && !fcmToken) { - return reply.status(400).send({ - error: 'FCM token required for Android devices', - }); - } - - if (platform === 'ios' && !apnsToken) { - return reply.status(400).send({ - error: 'APNs token required for iOS devices', - }); - } - - try { - // Determine device type based on platform - const deviceType = platform === 'ios' || platform === 'android' ? 'mobile' : 'web'; - - // Determine the token to store - const deviceToken = platform === 'ios' ? apnsToken! : fcmToken!; - - // Upsert device registration to handle token rotation and re-registration - const deviceRegistration = await prisma.deviceToken.upsert({ - where: { token: deviceToken }, - create: { - userId: authReq.user.id, - deviceType, - token: deviceToken, - platform, - appVersion, - osVersion, - model: deviceModel, - isActive: true, - lastUsedAt: new Date(), - }, - update: { - userId: authReq.user.id, - deviceType, - platform, - appVersion, - osVersion, - model: deviceModel, - isActive: true, - lastUsedAt: new Date(), - }, - }); - - return { - success: true, - device: { - deviceId: deviceRegistration.id, - platform: deviceRegistration.platform, - registeredAt: deviceRegistration.createdAt.toISOString(), - }, - message: 'Device registered successfully', - }; - } catch (error) { - console.error('Failed to register device:', error); - reply.status(500).send({ - error: 'Failed to register device', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Update device push tokens - fastify.put( - '/devices/:deviceId/tokens', - { - preHandler: async (request, reply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - return reply.status(401).send({ error: 'Authentication required' }); - } - }, - }, - async (request, reply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const { deviceId } = request.params as { deviceId: string }; - const { fcmToken, apnsToken, deviceModel } = request.body as { - fcmToken?: string; - apnsToken?: string; - deviceModel?: string; - }; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - if (!fcmToken && !apnsToken) { - return reply.status(400).send({ - error: 'At least one token is required', - }); - } - - try { - // Find the device first - const existingDevice = await prisma.deviceToken.findFirst({ - where: { - id: deviceId, - userId: authReq.user.id, - isActive: true, - }, - }); - - if (!existingDevice) { - return reply.status(404).send({ - error: 'Device not found', - }); - } - - // Determine which token to update based on platform - const updateData: { - token: string; - appVersion?: string; - osVersion?: string; - model?: string; - lastUsedAt: Date; - } = { - token: existingDevice.platform === 'ios' ? (apnsToken || existingDevice.token) : (fcmToken || existingDevice.token), - lastUsedAt: new Date(), - }; - - if (deviceModel) { - updateData.model = deviceModel; - } - - // Update device tokens in database - const device = await prisma.deviceToken.update({ - where: { - id: deviceId, - userId: authReq.user.id, - }, - data: updateData, - }); - - return { - success: true, - device: { - deviceId: device.id, - platform: device.platform, - lastActiveAt: device.lastUsedAt.toISOString(), - }, - message: 'Device tokens updated successfully', - }; - } catch (error) { - console.error('Failed to update device tokens:', error); - reply.status(500).send({ - error: 'Failed to update device tokens', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Get user's registered devices - fastify.get( - '/devices', - { - preHandler: async (request, reply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - return reply.status(401).send({ error: 'Authentication required' }); - } - }, - }, - async (request, reply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - try { - // Fetch devices from database - const devices = await prisma.deviceToken.findMany({ - where: { - userId: authReq.user.id, - isActive: true, - }, - select: { - id: true, - platform: true, - appVersion: true, - osVersion: true, - model: true, - lastUsedAt: true, - createdAt: true, - }, - orderBy: { - lastUsedAt: 'desc', - }, - }); - - return { - success: true, - devices: devices.map((d) => ({ - deviceId: d.id, - platform: d.platform, - appVersion: d.appVersion, - osVersion: d.osVersion, - model: d.model, - lastActiveAt: d.lastUsedAt.toISOString(), - registeredAt: d.createdAt.toISOString(), - })), - message: 'Device list retrieved', - }; - } catch (error) { - console.error('Failed to fetch devices:', error); - reply.status(500).send({ - error: 'Failed to fetch devices', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Deregister device - fastify.delete( - '/devices/:deviceId', - { - preHandler: async (request, reply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - if (!userId) { - return reply.status(401).send({ error: 'Authentication required' }); - } - }, - }, - async (request, reply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const { deviceId } = request.params as { deviceId: string }; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - try { - // Soft delete by marking as inactive - await prisma.deviceToken.update({ - where: { - id: deviceId, - userId: authReq.user.id, - }, - data: { - isActive: false, - }, - }); - - return { - success: true, - message: 'Device deregistered successfully', - }; - } catch (error) { - console.error('Failed to deregister device:', error); - reply.status(500).send({ - error: 'Failed to deregister device', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); -} diff --git a/packages/api/src/routes/exposure.routes.ts b/packages/api/src/routes/exposure.routes.ts deleted file mode 100644 index 9c38b4b..0000000 --- a/packages/api/src/routes/exposure.routes.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { FastifyInstance } from "fastify"; -import { MatchingEngine } from "@shieldai/darkwatch"; - -export function exposureRoutes(fastify: FastifyInstance) { - const engine = new MatchingEngine(); - - fastify.get("/", async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const exposures = await engine.getExposuresForUser(userId); - return reply.send(exposures); - }); - - fastify.get("/:id", async (request, reply) => { - const exposure = await engine.getExposureById(request.params.id); - - if (!exposure) { - return reply.code(404).send({ error: "Exposure not found" }); - } - - return reply.send(exposure); - }); -} diff --git a/packages/api/src/routes/extension.routes.ts b/packages/api/src/routes/extension.routes.ts deleted file mode 100644 index 14b1cd4..0000000 --- a/packages/api/src/routes/extension.routes.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { phishingDetector } from './lib/phishing-detector'; - -interface UrlCheckRequest { - url: string; -} - -interface PhishingReportRequest { - url: string; - pageTitle: string; - tabId: number; - timestamp: number; - reason: string; - heuristics: Record; -} - -export async function extensionRoutes(fastify: FastifyInstance) { - fastify.post('/url-check', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string; tier?: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'Authentication required' }); - } - - const body = request.body as UrlCheckRequest; - if (!body.url) { - return reply.code(400).send({ error: 'url is required' }); - } - - try { - const url = new URL(body.url); - const heuristic = phishingDetector.analyzeUrl(body.url); - - const threats = heuristic.threats.map((t) => ({ - type: t.type, - severity: t.severity, - source: t.source, - description: t.description, - })); - - return reply.send({ - url: body.url, - domain: url.hostname, - verdict: heuristic.verdict, - confidence: heuristic.score / 100, - threats, - timestamp: Date.now(), - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'URL check failed'; - return reply.code(500).send({ error: message }); - } - }); - - fastify.post('/phishing-report', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'Authentication required' }); - } - - const body = request.body as PhishingReportRequest; - - try { - fastify.log.info({ url: body.url, userId, reason: body.reason }, 'Phishing report received'); - - return reply.send({ - success: true, - reportId: `report_${Date.now()}_${userId}`, - timestamp: new Date().toISOString(), - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Report submission failed'; - return reply.code(500).send({ error: message }); - } - }); - - fastify.post('/auth', async (request: FastifyRequest, reply: FastifyReply) => { - const authHeader = request.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return reply.code(401).send({ error: 'Bearer token required' }); - } - - const token = authHeader.slice(7); - - try { - const result = await validateExtensionToken(token, fastify); - return reply.send(result); - } catch (error) { - const message = error instanceof Error ? error.message : 'Authentication failed'; - return reply.code(401).send({ error: message }); - } - }); - - fastify.get('/stats', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'Authentication required' }); - } - - try { - const today = new Date().toDateString(); - return reply.send({ - threatsBlockedToday: 0, - urlsCheckedToday: 0, - lastSyncAt: new Date().toISOString(), - syncDate: today, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Stats retrieval failed'; - return reply.code(500).send({ error: message }); - } - }); - - fastify.post('/exposures/check', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'Authentication required' }); - } - - const body = request.body as { domain: string }; - if (!body.domain) { - return reply.code(400).send({ error: 'domain is required' }); - } - - try { - const { prisma } = await import('@shieldai/db'); - - const exposures = await prisma.exposure.findMany({ - where: { - alert: { - some: { - userId, - }, - }, - }, - select: { - dataSource: true, - breachName: true, - metadata: true, - }, - take: 10, - }); - - const domainLower = body.domain.toLowerCase(); - const relevantExposures = exposures.filter((e) => { - const meta = e.metadata as Record | null; - return meta?.domain?.toLowerCase() === domainLower || - String(e.breachName).toLowerCase().includes(domainLower); - }); - - return reply.send({ - exposed: relevantExposures.length > 0, - sources: relevantExposures.map((e) => e.dataSource), - count: relevantExposures.length, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Exposure check failed'; - return reply.code(500).send({ error: message }); - } - }); -} - -async function validateExtensionToken( - token: string, - fastify: FastifyInstance -): Promise<{ userId: string; tier: string }> { - try { - const { prisma } = await import('@shieldai/db'); - - const session = await prisma.session.findFirst({ - where: { token }, - include: { - user: { - include: { - subscription: { - where: { status: 'active' }, - take: 1, - }, - }, - }, - }, - }); - - if (!session) { - throw new Error('Session not found'); - } - - const tier = session.user.subscription[0]?.tier || 'basic'; - - return { - userId: session.userId, - tier: tier.toLowerCase(), - }; - } catch (error) { - if (error instanceof Error && error.message === 'Session not found') { - throw error; - } - fastify.log.warn({ error }, 'Extension token validation failed'); - throw new Error('Token validation failed'); - } -} diff --git a/packages/api/src/routes/hometitle.routes.ts b/packages/api/src/routes/hometitle.routes.ts deleted file mode 100644 index 10da044..0000000 --- a/packages/api/src/routes/hometitle.routes.ts +++ /dev/null @@ -1,463 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { prisma, SubscriptionTier } from '@shieldai/db'; -import { detectChanges, shouldTriggerAlert } from '@shieldai/hometitle'; -import { homeTitleAlertPipeline } from '@shieldai/hometitle'; -import { AuthRequest } from '../auth.middleware'; - -const HOMETITLE_PROPERTY_LIMITS: Record = { - free: 0, - basic: 0, - plus: 3, - premium: 5, -}; - -const HOMETITLE_TIER_ORDER: Record = { - free: 0, - basic: 1, - plus: 2, - premium: 3, -}; - -/** - * Middleware: require Premium tier for home title features. - * Returns subscriptionId if user is Premium, otherwise replies with 402. - */ -async function requirePremiumTier( - request: FastifyRequest, - reply: FastifyReply, -): Promise { - const authReq = request as AuthRequest; - const userId = authReq.user?.id; - - if (!userId) { - await reply.code(401).send({ error: 'User not authenticated' }); - return null; - } - - const subscription = await prisma.subscription.findFirst({ - where: { userId, status: 'active' }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - await reply.code(402).send({ - error: 'Subscription required', - message: 'A home title monitoring subscription is required', - }); - return null; - } - - if (subscription.tier !== 'premium') { - const currentTier = HOMETITLE_TIER_ORDER[subscription.tier] ?? 0; - const nextTier = 'plus'; - - await reply.code(402).send({ - error: 'Premium tier required', - message: `Home title monitoring requires a Premium subscription. You are currently on ${subscription.tier}.`, - currentTier: subscription.tier, - upgradeTo: nextTier, - }); - return null; - } - - return subscription.id; -} - -/** - * Check property limit for the user's tier. - * Returns true if under limit, replies 400 if exceeded. - */ -async function checkPropertyLimit( - reply: FastifyReply, - subscriptionId: string, -): Promise { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { tier: true }, - }); - - if (!subscription) { - return false; - } - - const limit = HOMETITLE_PROPERTY_LIMITS[subscription.tier] ?? 0; - const count = await prisma.watchlistItem.count({ - where: { subscriptionId, type: 'address' }, - }); - - if (count >= limit) { - await reply.code(400).send({ - error: 'Property limit reached', - message: `You have reached the maximum of ${limit} properties for your ${subscription.tier} tier.`, - currentCount: count, - limit, - upgradeTo: 'premium', - }); - return false; - } - - return true; -} - -export async function hometitleRoutes(fastify: FastifyInstance) { - // GET /hometitle/properties - List monitored properties with status - fastify.get('/properties', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await requirePremiumTier(request, reply); - if (!subscriptionId) return; - - try { - const watchlistItems = await prisma.watchlistItem.findMany({ - where: { subscriptionId, type: 'address', isActive: true }, - orderBy: { createdAt: 'desc' }, - }); - - const itemIds = watchlistItems.map((item) => item.id); - - // Batch query: fetch all recent alerts in one query - const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); - const allRecentAlerts = await prisma.alert.findMany({ - where: { - subscriptionId, - watchlistItemId: { in: itemIds }, - createdAt: { gte: sevenDaysAgo }, - }, - orderBy: { createdAt: 'desc' }, - }); - - // Group alerts by watchlistItemId - const alertsByItem = new Map(); - for (const alert of allRecentAlerts) { - const arr = alertsByItem.get(alert.watchlistItemId) || []; - arr.push(alert); - alertsByItem.set(alert.watchlistItemId, arr); - } - - const properties = watchlistItems.map((item) => { - const recentAlerts = (alertsByItem.get(item.id) || []).slice(0, 3); - const hasRecentAlerts = recentAlerts.length > 0; - const latestAlert = recentAlerts[0]; - - let status: 'monitored' | 'alert' | 'error' = 'monitored'; - if (hasRecentAlerts && latestAlert) { - status = latestAlert.severity === 'CRITICAL' ? 'alert' : 'monitored'; - } - - return { - id: item.id, - address: item.value, - status, - lastScan: null, - lastChange: latestAlert ? latestAlert.createdAt : null, - alertCount: recentAlerts.length, - addedAt: item.createdAt, - }; - }); - - return reply.send({ properties }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to list properties'; - return reply.code(500).send({ error: message }); - } - }); - - // GET /hometitle/properties/stats - Dashboard widget stats (available to all active subscriptions) - fastify.get('/properties/stats', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User not authenticated' }); - } - - const subscription = await prisma.subscription.findFirst({ - where: { userId, status: 'active' }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - return reply.code(404).send({ error: 'Active subscription not found' }); - } - - try { - const propertyCount = await prisma.watchlistItem.count({ - where: { subscriptionId: subscription.id, type: 'address', isActive: true }, - }); - - const limit = HOMETITLE_PROPERTY_LIMITS[subscription.tier] ?? 0; - const isPremium = subscription.tier === 'premium'; - - // Count recent alerts (last 7 days) - const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); - const recentAlertCount = await prisma.alert.count({ - where: { - subscriptionId: subscription.id, - createdAt: { gte: sevenDaysAgo }, - }, - }); - - // Count critical alerts - const criticalAlertCount = await prisma.alert.count({ - where: { - subscriptionId: subscription.id, - severity: 'CRITICAL', - createdAt: { gte: sevenDaysAgo }, - }, - }); - - return reply.send({ - monitoredProperties: propertyCount, - tier: subscription.tier, - isPremium, - propertyLimit: limit, - canAddMore: propertyCount < limit, - recentAlertCount, - criticalAlertCount, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to fetch stats'; - return reply.code(500).send({ error: message }); - } - }); - - // POST /hometitle/properties - Add property to monitor - fastify.post('/properties', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await requirePremiumTier(request, reply); - if (!subscriptionId) return; - - const body = request.body as { address: string }; - - if (!body.address || typeof body.address !== 'string') { - return reply.code(400).send({ - error: 'Invalid request', - message: 'Address is required', - }); - } - - // Validate address format (permissive: requires number + space + text) - const addressPattern = /^\d+[\s,].+$/i; - if (!addressPattern.test(body.address.trim())) { - return reply.code(400).send({ - error: 'Invalid address', - message: 'Please enter a valid street address (e.g., 123 Main Street)', - }); - } - - const limitReached = await checkPropertyLimit(reply, subscriptionId); - if (!limitReached) return; - - try { - // Check for duplicate - const existing = await prisma.watchlistItem.findFirst({ - where: { subscriptionId, type: 'address', value: body.address.trim() }, - }); - - if (existing) { - return reply.code(409).send({ - error: 'Duplicate property', - message: 'This property is already being monitored', - }); - } - - const property = await prisma.watchlistItem.create({ - data: { - subscriptionId, - type: 'address', - value: body.address.trim(), - }, - }); - - // Trigger initial scan - try { - const { homeTitleScheduler } = await import('@shieldai/hometitle'); - if (homeTitleScheduler.isRunning()) { - await homeTitleScheduler.runScan(); - } - } catch (scanError) { - console.error('[HomeTitle] Initial scan error:', scanError); - } - - return reply.code(201).send({ - property: { - id: property.id, - address: property.value, - status: 'monitored', - addedAt: property.createdAt, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to add property'; - return reply.code(422).send({ error: message }); - } - }); - - // DELETE /hometitle/properties/:id - Remove property from monitoring - fastify.delete('/properties/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await requirePremiumTier(request, reply); - if (!subscriptionId) return; - - const id = (request.params as { id: string }).id; - - try { - const result = await prisma.watchlistItem.update({ - where: { id, subscriptionId }, - data: { isActive: false }, - }); - - return reply.send({ - property: { - id: result.id, - address: result.value, - status: 'removed', - }, - }); - } catch { - return reply.code(404).send({ - error: 'Property not found', - message: 'The monitored property does not exist or is not owned by this user', - }); - } - }); - - // GET /hometitle/changes - Recent property changes - fastify.get('/changes', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await requirePremiumTier(request, reply); - if (!subscriptionId) return; - - const query = request.query as { limit?: number }; - const limit = Math.min(query.limit ?? 20, 50); - - try { - const watchlistItemIds = ( - await prisma.watchlistItem.findMany({ - where: { subscriptionId, type: 'address', isActive: true }, - select: { id: true }, - }) - ).map((item) => item.id); - - const changes = await prisma.alert.findMany({ - where: { - subscriptionId, - watchlistItemId: { in: watchlistItemIds }, - }, - orderBy: { createdAt: 'desc' }, - take: limit, - include: { - watchlistItem: true, - }, - }); - - const formattedChanges = changes.map((alert) => ({ - id: alert.id, - propertyAddress: alert.watchlistItem?.value ?? 'Unknown', - type: 'property_change', - severity: alert.severity, - title: alert.title, - message: alert.message, - isRead: alert.isRead, - createdAt: alert.createdAt, - })); - - return reply.send({ changes: formattedChanges }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to fetch changes'; - return reply.code(500).send({ error: message }); - } - }); - - // GET /hometitle/alerts - Recent property alerts - fastify.get('/alerts', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await requirePremiumTier(request, reply); - if (!subscriptionId) return; - - const query = request.query as { limit?: number; severity?: string }; - const limit = Math.min(query.limit ?? 20, 50); - - try { - const watchlistItemIds = ( - await prisma.watchlistItem.findMany({ - where: { subscriptionId, type: 'address', isActive: true }, - select: { id: true }, - }) - ).map((item) => item.id); - - const whereClause: Record = { - subscriptionId, - watchlistItemId: { in: watchlistItemIds }, - }; - - if (query.severity) { - whereClause.severity = query.severity; - } - - const alerts = await prisma.alert.findMany({ - where: whereClause, - orderBy: { createdAt: 'desc' }, - take: limit, - include: { - watchlistItem: true, - }, - }); - - const formattedAlerts = alerts.map((alert) => ({ - id: alert.id, - propertyAddress: alert.watchlistItem?.value ?? 'Unknown', - severity: alert.severity, - title: alert.title, - message: alert.message, - isRead: alert.isRead, - channel: alert.channel, - createdAt: alert.createdAt, - })); - - return reply.send({ alerts: formattedAlerts }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to fetch alerts'; - return reply.code(500).send({ error: message }); - } - }); - - // PATCH /hometitle/alerts/:id/read - Mark alert as read - fastify.patch('/alerts/:id/read', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await requirePremiumTier(request, reply); - if (!subscriptionId) return; - - const id = (request.params as { id: string }).id; - - try { - const alert = await prisma.alert.update({ - where: { id, subscriptionId }, - data: { isRead: true, readAt: new Date() }, - }); - - return reply.send({ alert: { id: alert.id, isRead: alert.isRead } }); - } catch { - return reply.code(404).send({ error: 'Alert not found' }); - } - }); - - // POST /hometitle/scan - Trigger on-demand scan for all monitored properties - fastify.post('/scan', async (request: FastifyRequest, reply: FastifyReply) => { - const subscriptionId = await requirePremiumTier(request, reply); - if (!subscriptionId) return; - - try { - const { homeTitleScheduler } = await import('@shieldai/hometitle'); - const result = await homeTitleScheduler.runScan(); - - return reply.send({ - scan: { - scanId: result.scanId, - propertiesScanned: result.propertiesScanned, - changesDetected: result.changesDetected, - alertsCreated: result.alertsCreated, - notificationsSent: result.notificationsSent, - startedAt: result.startedAt, - completedAt: result.completedAt, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Scan failed'; - return reply.code(500).send({ error: message }); - } - }); -} diff --git a/packages/api/src/routes/index.ts b/packages/api/src/routes/index.ts deleted file mode 100644 index 2ea9cce..0000000 --- a/packages/api/src/routes/index.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { authMiddleware, AuthRequest } from './auth.middleware'; -import { voiceprintRoutes } from './voiceprint.routes'; -import { spamshieldRoutes } from './spamshield.routes'; -import { darkwatchRoutes } from './darkwatch.routes'; -import { reportRoutes } from './report.routes'; -import { subscriptionRoutes } from './subscription.routes'; -import { deviceRoutes } from './device.routes'; -import { notificationRoutes } from './notifications.routes'; -import { hometitleRoutes } from './hometitle.routes'; -import { removebrokersRoutes } from './removebrokers.routes'; - -export async function routes(fastify: FastifyInstance) { - // Authenticated routes group - fastify.register( - async (authenticated) => { - // Add auth requirement - authenticated.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => { - await fastify.requireAuth(request as AuthRequest); - }); - - // Example authenticated endpoint - authenticated.get('/user/me', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - return { - user: authReq.user, - authType: authReq.authType, - }; - }); - - // Example service endpoint - authenticated.get('/services', async (request: FastifyRequest, reply: FastifyReply) => { - return { - services: [ - { - name: 'user-service', - url: '/api/v1/services/user', - status: 'healthy', - }, - { - name: 'billing-service', - url: '/api/v1/services/billing', - status: 'healthy', - }, - { - name: 'notification-service', - url: '/api/v1/services/notifications', - status: 'healthy', - }, - ], - }; - }); - }, - { prefix: '/auth' } - ); - - // Public API routes - fastify.register( - async (publicRouter) => { - // Version info - publicRouter.get('/info', async () => { - return { - version: '1.0.0', - environment: process.env.NODE_ENV || 'development', - build: process.env.npm_package_version || 'unknown', - }; - }); - - // API documentation - publicRouter.get('/docs', async () => { - return { - title: 'FrenoCorp API Gateway', - version: '1.0.0', - endpoints: { - public: [ - { method: 'GET', path: '/', description: 'Root endpoint' }, - { method: 'GET', path: '/health', description: 'Health check' }, - { method: 'GET', path: '/api/v1/info', description: 'API version info' }, - { method: 'GET', path: '/api/v1/docs', description: 'API documentation' }, - ], - authenticated: [ - { method: 'GET', path: '/api/v1/auth/user/me', description: 'Get current user' }, - { method: 'GET', path: '/api/v1/auth/services', description: 'List available services' }, - ], - }, - }; - }); - }, - { prefix: '/api/v1' } - ); - - // Service proxy placeholder (for future microservice routing) - fastify.register( - async (services) => { - services.get('/services/user', async (request, reply) => { - // In production, proxy to actual user service - return { - service: 'user-service', - message: 'User service endpoint', - timestamp: new Date().toISOString(), - }; - }); - - services.get('/services/billing', async (request, reply) => { - // In production, proxy to actual billing service - return { - service: 'billing-service', - message: 'Billing service endpoint', - timestamp: new Date().toISOString(), - }; - }); - - services.get('/services/notifications', async (request, reply) => { - // In production, proxy to actual notification service - return { - service: 'notification-service', - message: 'Notification service endpoint', - timestamp: new Date().toISOString(), - }; - }); - }, - { prefix: '/api/v1/services' } - ); - - // VoicePrint service routes - fastify.register( - async (voiceprintRouter) => { - await voiceprintRoutes(voiceprintRouter); - }, - { prefix: '/voiceprint' } - ); - - // SpamShield service routes - fastify.register( - async (spamshieldRouter) => { - await spamshieldRoutes(spamshieldRouter); - }, - { prefix: '/spamshield' } - ); - - // DarkWatch service routes - fastify.register( - async (darkwatchRouter) => { - await darkwatchRoutes(darkwatchRouter); - }, - { prefix: '/darkwatch' } - ); - - // Report routes - fastify.register( - async (reportRouter) => { - await reportRoutes(reportRouter); - }, - { prefix: '/reports' } - ); - -// Subscription routes - fastify.register( - async (subscriptionRouter) => { - await subscriptionRoutes(subscriptionRouter); - }, - { prefix: '/billing' } - ); - - // Device routes - fastify.register( - async (deviceRouter) => { - await deviceRoutes(deviceRouter); - }, - { prefix: '/api/v1' } - ); - - // Home Title service routes - fastify.register( - async (hometitleRouter) => { - hometitleRouter.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => { - await fastify.requireAuth(request as AuthRequest); - }); - await hometitleRoutes(hometitleRouter); - }, - { prefix: '/hometitle' } - ); - - // Info Broker Removal service routes - fastify.register( - async (removebrokersRouter) => { - removebrokersRouter.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => { - await fastify.requireAuth(request as AuthRequest); - }); - await removebrokersRoutes(removebrokersRouter); - }, - { prefix: '/removebrokers' } - ); -} diff --git a/packages/api/src/routes/notifications.routes.ts b/packages/api/src/routes/notifications.routes.ts deleted file mode 100644 index b99a991..0000000 --- a/packages/api/src/routes/notifications.routes.ts +++ /dev/null @@ -1,213 +0,0 @@ -import { FastifyInstance } from 'fastify'; -import { NotificationService } from '@shieldsai/shared-notifications'; - -export async function notificationRoutes(fastify: FastifyInstance): Promise { - let notificationService: NotificationService | undefined; - - // Initialize notification service (will be injected via config) - fastify.addHook('onReady', async () => { - // Notification service will be initialized from config - notificationService = fastify.notificationService; - }); - - /** - * POST /api/v1/notifications/send - * Send a notification to a user - */ - fastify.post( - '/notifications/send', - { - schema: { - body: { - type: 'object', - required: ['userId', 'channel', 'subject', 'body'], - properties: { - userId: { type: 'string' }, - channel: { type: 'string', enum: ['email', 'push', 'sms'] }, - subject: { type: 'string' }, - body: { type: 'string' }, - email: { type: 'string' }, - phone: { type: 'string' }, - fcmToken: { type: 'string' }, - apnsToken: { type: 'string' }, - priority: { type: 'string', enum: ['low', 'normal', 'high', 'urgent'] }, - metadata: { type: 'object' }, - }, - }, - }, - }, - async (request, reply) => { - const { userId, channel, subject, body, priority, metadata } = request.body; - - const recipient = { - userId, - email: request.body.email, - phone: request.body.phone, - fcmToken: request.body.fcmToken, - apnsToken: request.body.apnsToken, - }; - - try { - if (!notificationService) { - return reply.status(503).send({ - success: false, - error: 'Notification service not initialized', - }); - } - - const notifications = await notificationService.sendMultiChannelNotification( - recipient, - channel, - subject, - body, - priority, - metadata - ); - - return reply.send({ - success: true, - notifications, - }); - } catch (error) { - return reply.status(500).send({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - /** - * GET /api/v1/notifications/:userId/preferences - * Get notification preferences for a user - */ - fastify.get( - '/notifications/:userId/preferences', - { - schema: { - params: { - type: 'object', - required: ['userId'], - properties: { - userId: { type: 'string' }, - }, - }, - }, - }, - async (request, reply) => { - const { userId } = request.params; - - try { - if (!notificationService) { - return reply.status(503).send({ - success: false, - error: 'Notification service not initialized', - }); - } - - const preferences = await notificationService.getNotificationPreferences(userId); - - return reply.send({ - success: true, - preferences, - }); - } catch (error) { - return reply.status(500).send({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - /** - * PUT /api/v1/notifications/:userId/preferences - * Update notification preferences for a user - */ - fastify.put( - '/notifications/:userId/preferences', - { - schema: { - params: { - type: 'object', - required: ['userId'], - properties: { - userId: { type: 'string' }, - }, - }, - body: { - type: 'object', - properties: { - email: { - type: 'object', - properties: { - enabled: { type: 'boolean' }, - categories: { type: 'array', items: { type: 'string' } }, - }, - }, - push: { - type: 'object', - properties: { - enabled: { type: 'boolean' }, - categories: { type: 'array', items: { type: 'string' } }, - }, - }, - sms: { - type: 'object', - properties: { - enabled: { type: 'boolean' }, - categories: { type: 'array', items: { type: 'string' } }, - }, - }, - }, - }, - }, - }, - async (request, reply) => { - const { userId } = request.params; - const updates = request.body; - - try { - // TODO: Update preferences in database - return reply.send({ - success: true, - message: 'Preferences updated', - userId, - updates, - }); - } catch (error) { - return reply.status(500).send({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - /** - * GET /api/v1/notifications/config - * Get notification configuration status - */ - fastify.get('/notifications/config', async (request, reply) => { - try { - if (!notificationService) { - return reply.status(503).send({ - success: false, - error: 'Notification service not initialized', - }); - } - - const config = notificationService.getConfigSummary(); - - return reply.send({ - success: true, - config, - }); - } catch (error) { - return reply.status(500).send({ - success: false, - error: error instanceof Error ? error.message : 'Unknown error', - }); - } - }); -} diff --git a/packages/api/src/routes/removebrokers.routes.ts b/packages/api/src/routes/removebrokers.routes.ts deleted file mode 100644 index dbec451..0000000 --- a/packages/api/src/routes/removebrokers.routes.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { prisma } from '@shieldai/db'; -import { RemovalStatus, Severity, AlertCategory, EntityTypes } from '@shieldai/types'; -import type { RemovalStatus as PrismaRemovalStatus } from '@shieldai/db'; -import { - removeBrokersService, - removeBrokersScheduler, - brokerAlertPipeline, - type PersonalInfo, -} from '@shieldai/removebrokers'; -import { AuthRequest } from '../middleware/auth.middleware'; - -const REMOVAL_REQUEST_LIMITS: Record = { - basic: 5, - plus: 20, - premium: 999, -}; - -async function getSubscription( - request: FastifyRequest, - reply: FastifyReply, -): Promise<{ subscriptionId: string; tier: string } | null> { - const authReq = request as AuthRequest; - const userId = authReq.user?.id; - - if (!userId) { - await reply.code(401).send({ error: 'User not authenticated' }); - return null; - } - - const subscription = await prisma.subscription.findFirst({ - where: { userId, status: 'active' }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - await reply.code(402).send({ - error: 'Subscription required', - message: 'An active subscription is required for data broker removal', - }); - return null; - } - - return { subscriptionId: subscription.id, tier: subscription.tier }; -} - -export async function removebrokersRoutes(fastify: FastifyInstance) { - // GET /removebrokers/brokers - List available data brokers - fastify.get('/brokers', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - if (!authReq.user?.id) { - return reply.code(401).send({ error: 'User not authenticated' }); - } - - const query = request.query as { category?: string }; - let brokers = await removeBrokersService.getAvailableBrokers(); - - if (query.category) { - brokers = brokers.filter((b) => b.category === query.category); - } - - return reply.send({ - brokers: brokers.map((b) => ({ - id: b.id, - name: b.name, - domain: b.domain, - category: b.category, - removalMethod: b.removalMethod, - requiresAccount: b.requiresAccount, - requiresVerification: b.requiresVerification, - estimatedDays: b.estimatedDays, - removalUrl: b.removalUrl, - })), - }); - }); - - // GET /removebrokers/status - Get removal request status for user - fastify.get('/status', async (request: FastifyRequest, reply: FastifyReply) => { - const sub = await getSubscription(request, reply); - if (!sub) return; - - try { - const status = await removeBrokersService.getRemovalStatus(sub.subscriptionId); - - const total = status.length; - const pending = status.filter((s) => s.status === RemovalStatus.PENDING).length; - const submitted = status.filter((s) => s.status === RemovalStatus.SUBMITTED).length; - const completed = status.filter((s) => s.status === RemovalStatus.COMPLETED).length; - const failed = status.filter((s) => s.status === RemovalStatus.FAILED).length; - - const limit = REMOVAL_REQUEST_LIMITS[sub.tier] ?? 5; - const remaining = Math.max(0, limit - total); - - return reply.send({ - stats: { total, pending, submitted, completed, failed }, - limit, - remaining, - tier: sub.tier, - requests: status, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to fetch status'; - return reply.code(500).send({ error: message }); - } - }); - - // POST /removebrokers/scan - Scan for personal listings across brokers - fastify.post('/scan', async (request: FastifyRequest, reply: FastifyReply) => { - const sub = await getSubscription(request, reply); - if (!sub) return; - - const body = request.body as { fullName?: string; email?: string; phone?: string; address?: string }; - - if (!body.fullName) { - return reply.code(400).send({ - error: 'Invalid request', - message: 'fullName is required for scanning', - }); - } - - try { - const user = await prisma.user.findUnique({ - where: { id: (request as AuthRequest).user!.id }, - }); - - const personalInfo: PersonalInfo = { - fullName: body.fullName, - email: body.email || user?.email, - phone: body.phone, - address: body.address - ? { street: body.address } - : undefined, - }; - - const results = await removeBrokersService.scanForListings( - sub.subscriptionId, - personalInfo, - ); - - const found = results.filter((r) => r.found); - - for (const listing of found) { - try { - await brokerAlertPipeline.sendListingFoundAlert({ - userId: (request as AuthRequest).user!.id, - brokerName: listing.brokerName, - brokerId: listing.brokerId, - category: AlertCategory.INFO_BROKER_LISTING, - severity: Severity.MEDIUM, - title: `Personal listing found on ${listing.brokerName}`, - description: `Your personal information was found on ${listing.brokerName} (${listing.brokerId}). Consider submitting a removal request.`, - entities: [ - { type: EntityTypes.USER_ID, value: (request as AuthRequest).user!.id }, - ], - metadata: { url: listing.url }, - }); - } catch { - // Alert failure is non-critical - } - } - - return reply.send({ - brokersScanned: results.length, - listingsFound: found.length, - results, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Scan failed'; - return reply.code(500).send({ error: message }); - } - }); - - // POST /removebrokers/request - Create a new removal request - fastify.post('/request', async (request: FastifyRequest, reply: FastifyReply) => { - const sub = await getSubscription(request, reply); - if (!sub) return; - - const body = request.body as { - brokerId: string; - fullName: string; - email?: string; - phone?: string; - address?: { - street?: string; - city?: string; - state?: string; - zip?: string; - }; - dob?: string; - notes?: string; - }; - - if (!body.brokerId) { - return reply.code(400).send({ - error: 'Invalid request', - message: 'brokerId is required', - }); - } - - if (!body.fullName) { - return reply.code(400).send({ - error: 'Invalid request', - message: 'fullName is required', - }); - } - - const limit = REMOVAL_REQUEST_LIMITS[sub.tier] ?? 5; - const currentCount = await prisma.removalRequest.count({ - where: { subscriptionId: sub.subscriptionId }, - }); - - if (currentCount >= limit) { - return reply.code(400).send({ - error: 'Request limit reached', - message: `You have reached the maximum of ${limit} removal requests for your ${sub.tier} tier.`, - currentCount, - limit, - upgradeTo: 'plus', - }); - } - - try { - const personalInfo: PersonalInfo = { - fullName: body.fullName, - email: body.email, - phone: body.phone, - address: body.address, - dob: body.dob, - }; - - const req = await removeBrokersService.createRemovalRequest( - sub.subscriptionId, - body.brokerId, - personalInfo, - body.notes, - ); - - return reply.code(201).send({ - request: { - id: req.id, - brokerId: req.brokerId, - status: req.status, - method: req.method, - createdAt: req.createdAt, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to create removal request'; - return reply.code(422).send({ error: message }); - } - }); - - // GET /removebrokers/request/:id - Get specific removal request - fastify.get('/request/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const sub = await getSubscription(request, reply); - if (!sub) return; - - const id = (request.params as { id: string }).id; - - try { - const req = await prisma.removalRequest.findFirst({ - where: { id, subscriptionId: sub.subscriptionId }, - include: { broker: true }, - }); - - if (!req) { - return reply.code(404).send({ error: 'Removal request not found' }); - } - - return reply.send({ - request: { - id: req.id, - brokerId: req.brokerId, - brokerName: req.broker?.name || null, - status: req.status, - method: req.method, - attempts: req.attempts, - submittedAt: req.submittedAt, - completedAt: req.completedAt, - error: req.error, - notes: req.notes, - createdAt: req.createdAt, - updatedAt: req.updatedAt, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to fetch request'; - return reply.code(500).send({ error: message }); - } - }); - - // DELETE /removebrokers/request/:id - Cancel a removal request - fastify.delete('/request/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const sub = await getSubscription(request, reply); - if (!sub) return; - - const id = (request.params as { id: string }).id; - - try { - const req = await prisma.removalRequest.findFirst({ - where: { id, subscriptionId: sub.subscriptionId }, - }); - - if (!req) { - return reply.code(404).send({ error: 'Removal request not found' }); - } - - if (req.status === RemovalStatus.COMPLETED) { - return reply.code(400).send({ - error: 'Cannot cancel', - message: 'Cannot cancel a completed removal request', - }); - } - - await prisma.removalRequest.update({ - where: { id }, - data: { status: RemovalStatus.CANCELLED as PrismaRemovalStatus }, - }); - - return reply.send({ - request: { - id: req.id, - status: RemovalStatus.CANCELLED, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to cancel request'; - return reply.code(500).send({ error: message }); - } - }); - - // POST /removebrokers/process - Trigger processing of pending removals (admin) - fastify.post('/process', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - if (!authReq.user?.id) { - return reply.code(401).send({ error: 'User not authenticated' }); - } - - if (authReq.user.role !== 'support') { - return reply.code(403).send({ error: 'Support access required' }); - } - - try { - const results = await removeBrokersService.processPendingRequests(); - - return reply.send({ - processed: results.length, - results, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Processing failed'; - return reply.code(500).send({ error: message }); - } - }); - - // POST /removebrokers/verify/:id - Manually verify a removal - fastify.post('/verify/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const sub = await getSubscription(request, reply); - if (!sub) return; - - const id = (request.params as { id: string }).id; - - try { - const req = await prisma.removalRequest.findFirst({ - where: { id, subscriptionId: sub.subscriptionId }, - }); - - if (!req) { - return reply.code(404).send({ error: 'Removal request not found' }); - } - - const result = await removeBrokersService.verifyRemoval(id); - - return reply.send({ - requestId: id, - ...result, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Verification failed'; - return reply.code(500).send({ error: message }); - } - }); -} diff --git a/packages/api/src/routes/report.routes.ts b/packages/api/src/routes/report.routes.ts deleted file mode 100644 index 54df704..0000000 --- a/packages/api/src/routes/report.routes.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { reportService } from '@shieldai/report'; -import { prisma } from '@shieldai/db'; -import { ReportType, ReportStatus, ReportDataPayload } from '@shieldai/types'; - -interface AuthRequest extends FastifyRequest { - user?: { - id: string; - email?: string; - role?: string; - }; -} - -export async function reportRoutes(fastify: FastifyInstance) { - // Generate a new report - fastify.post('/generate', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const body = request.body as { - reportType?: ReportType; - periodStart?: string; - periodEnd?: string; - }; - - const subscription = await prisma.subscription.findFirst({ - where: { userId, status: 'active' }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - return reply.code(404).send({ error: 'Active subscription not found' }); - } - - const reportType = body.reportType || (subscription.tier === 'premium' ? 'ANNUAL_PREMIUM' : 'MONTHLY_PLUS'); - - const periodStart = body.periodStart ? new Date(body.periodStart) : undefined; - const periodEnd = body.periodEnd ? new Date(body.periodEnd) : undefined; - - const report = await reportService.generateReport({ - userId, - subscriptionId: subscription.id, - reportType, - periodStart, - periodEnd, - }); - - return reply.code(201).send(report); - }); - - // Get report history - fastify.get('/', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const query = request.query as Record; - const limit = parseInt(query.limit || '20', 10); - const offset = parseInt(query.offset || '0', 10); - - const reports = await reportService.getReportHistory(userId, limit, offset); - return reply.code(200).send({ reports, count: reports.length }); - }); - - // Get specific report - fastify.get('/:reportId', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const reportId = (request.params as { reportId: string }).reportId; - - try { - const report = await reportService.getReportById(userId, reportId); - return reply.code(200).send(report); - } catch (error) { - return reply.code(404).send({ error: error instanceof Error ? error.message : 'Report not found' }); - } - }); - - // Get report HTML content - fastify.get('/:reportId/html', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const reportId = (request.params as { reportId: string }).reportId; - - const report = await prisma.securityReport.findFirst({ - where: { id: reportId, userId }, - select: { htmlContent: true, status: true }, - }); - - if (!report) { - return reply.code(404).send({ error: 'Report not found' }); - } - - if (report.status !== 'COMPLETED') { - return reply.code(404).send({ error: 'Report not yet completed' }); - } - - reply.header('Content-Type', 'text/html'); - return reply.code(200).send(report.htmlContent || ''); - }); - - // Get report PDF - fastify.get('/:reportId/pdf', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthRequest; - const userId = authReq.user?.id; - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const reportId = (request.params as { reportId: string }).reportId; - - const report = await prisma.securityReport.findFirst({ - where: { id: reportId, userId }, - select: { dataPayload: true, title: true, status: true, htmlContent: true }, - }); - - if (!report) { - return reply.code(404).send({ error: 'Report not found' }); - } - - if (report.status !== 'COMPLETED') { - return reply.code(404).send({ error: 'Report not yet completed' }); - } - - const { pdfGenerator } = await import('@shieldai/report'); - const pdfData = report.dataPayload - ? (typeof report.dataPayload === 'string' ? JSON.parse(report.dataPayload) : report.dataPayload as unknown as ReportDataPayload) - : { - exposureSummary: { totalExposures: 0, newExposures: 0, resolvedExposures: 0, criticalExposures: 0, warningExposures: 0, infoExposures: 0, exposuresBySource: {} }, - spamStats: { callsBlocked: 0, textsBlocked: 0, callsFlagged: 0, textsFlagged: 0, falsePositives: 0, totalSpamEvents: 0 }, - voiceStats: { analysesRun: 0, threatsDetected: 0, enrollmentsActive: 0, syntheticDetections: 0, voiceMismatchEvents: 0 }, - recommendations: [], - protectionScore: 0, - }; - const pdfBuffer = await pdfGenerator.generate({ - reportTitle: report.title, - periodStart: '', - periodEnd: '', - generatedAt: new Date().toISOString(), - data: pdfData, - reportId, - }); - - reply.header('Content-Type', 'application/pdf'); - reply.header('Content-Disposition', `inline; filename="${report.title}.pdf"`); - return reply.code(200).send(pdfBuffer); - }); - - // Schedule pending reports (admin/scheduler endpoint) - fastify.post('/schedule/monthly', async (request: FastifyRequest, reply: FastifyReply) => { - const createdIds = await reportService.scheduleMonthlyReports(); - return reply.code(200).send({ scheduled: createdIds.length, reportIds: createdIds }); - }); - - fastify.post('/schedule/annual', async (request: FastifyRequest, reply: FastifyReply) => { - const createdIds = await reportService.scheduleAnnualReports(); - return reply.code(200).send({ scheduled: createdIds.length, reportIds: createdIds }); - }); - - fastify.post('/schedule/weekly-digest', async (request: FastifyRequest, reply: FastifyReply) => { - const createdIds = await reportService.scheduleWeeklyDigest(); - return reply.code(200).send({ scheduled: createdIds.length, reportIds: createdIds }); - }); -} diff --git a/packages/api/src/routes/scan.routes.ts b/packages/api/src/routes/scan.routes.ts deleted file mode 100644 index 9350008..0000000 --- a/packages/api/src/routes/scan.routes.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { FastifyInstance } from "fastify"; -import { ScanService } from "@shieldai/darkwatch"; -import { DataSource } from "@shieldai/types"; - -export function scanRoutes(fastify: FastifyInstance) { - const scanService = new ScanService(); - - fastify.post("/", async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const body = request.body as { source?: string }; - const source = body.source ? (body.source as DataSource) : undefined; - const resultCount = await scanService.runScan(userId, source); - - return reply.code(200).send({ scanned: true, resultCount }); - }); - - fastify.get("/history", async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const history = await scanService.getScanHistory(userId); - return reply.send(history); - }); -} diff --git a/packages/api/src/routes/scheduler.routes.ts b/packages/api/src/routes/scheduler.routes.ts deleted file mode 100644 index 8e8d667..0000000 --- a/packages/api/src/routes/scheduler.routes.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { FastifyInstance } from "fastify"; -import { ScanScheduler } from "@shieldai/darkwatch"; - -export function schedulerRoutes(fastify: FastifyInstance) { - const scheduler = new ScanScheduler(); - - fastify.post( - "/ensure", - async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const result = await scheduler.ensureScheduleForUser(userId); - return reply.send(result); - } - ); - - fastify.get( - "/:userId", - async (request, reply) => { - const params = request.params as { userId: string }; - const authedUser = (request.user as { id: string })?.id; - if (authedUser !== params.userId) { - return reply.code(403).send({ error: "Forbidden" }); - } - const schedule = await scheduler.getSchedule(params.userId); - - if (!schedule) { - return reply.code(404).send({ error: "Schedule not found" }); - } - - return reply.send(schedule); - } - ); - - fastify.post( - "/:userId/pause", - async (request, reply) => { - const params = request.params as { userId: string }; - const authedUser = (request.user as { id: string })?.id; - if (authedUser !== params.userId) { - return reply.code(403).send({ error: "Forbidden" }); - } - await scheduler.pauseSchedule(params.userId); - return reply.send({ paused: true }); - } - ); - - fastify.post( - "/:userId/resume", - async (request, reply) => { - const params = request.params as { userId: string }; - const authedUser = (request.user as { id: string })?.id; - if (authedUser !== params.userId) { - return reply.code(403).send({ error: "Forbidden" }); - } - await scheduler.resumeSchedule(params.userId); - return reply.send({ resumed: true }); - } - ); - - fastify.get( - "/", - async (request, reply) => { - const limit = parseInt((request.query as { limit?: string }).limit || "100"); - const offset = parseInt((request.query as { offset?: string }).offset || "0"); - - const schedules = await scheduler.listActiveSchedules(limit, offset); - return reply.send(schedules); - } - ); -} diff --git a/packages/api/src/routes/spamshield.routes.ts b/packages/api/src/routes/spamshield.routes.ts deleted file mode 100644 index 6534852..0000000 --- a/packages/api/src/routes/spamshield.routes.ts +++ /dev/null @@ -1,252 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { - numberReputationService, - smsClassifierService, - callAnalysisService, - spamFeedbackService, -} from '../services/spamshield'; -import { ErrorHandler, SpamErrorCode } from '../services/spamshield/spamshield.error-handler'; - -export async function spamshieldRoutes(fastify: FastifyInstance) { - // Classify SMS text - fastify.post('/sms/classify', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - const body = request.body as { text: string }; - - const textValidation = ErrorHandler.validateRequiredField(body.text, 'text'); - if (!textValidation.isValid && textValidation.error) { - ErrorHandler.send(reply, textValidation.error.code, textValidation.error.message, { - field: textValidation.error.field, - status: 400, - }); - return; - } - - try { - const result = await smsClassifierService.classify(body.text); - return reply.send({ - classification: { - isSpam: result.isSpam, - confidence: result.confidence, - spamFeatures: result.spamFeatures, - }, - }); - } catch (error) { - ErrorHandler.send(reply, SpamErrorCode.CLASSIFICATION_FAILED, 'Classification failed', { - status: 422, - }); - } - }); - - // Check number reputation - fastify.post('/number/reputation', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - const body = request.body as { phoneNumber: string }; - - const phoneValidation = ErrorHandler.validateRequiredField(body.phoneNumber, 'phoneNumber'); - if (!phoneValidation.isValid && phoneValidation.error) { - ErrorHandler.send(reply, phoneValidation.error.code, phoneValidation.error.message, { - field: phoneValidation.error.field, - status: 400, - }); - return; - } - - try { - const result = await numberReputationService.checkReputation(body.phoneNumber); - return reply.send({ - reputation: { - isSpam: result.isSpam, - confidence: result.confidence, - spamType: result.spamType, - reportCount: result.reportCount, - }, - }); - } catch (error) { - ErrorHandler.send(reply, SpamErrorCode.REPUTATION_CHECK_FAILED, 'Reputation check failed', { - status: 422, - }); - } - }); - - // Analyze incoming call - fastify.post('/call/analyze', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - const body = request.body as { - phoneNumber: string; - duration?: number; - callTime: string; - isVoip?: boolean; - }; - - const phoneValidation = ErrorHandler.validateRequiredField(body.phoneNumber, 'phoneNumber'); - const callTimeValidation = ErrorHandler.validateRequiredField(body.callTime, 'callTime'); - - if (!phoneValidation.isValid && phoneValidation.error) { - ErrorHandler.send(reply, phoneValidation.error.code, phoneValidation.error.message, { - field: phoneValidation.error.field, - status: 400, - }); - return; - } - - if (!callTimeValidation.isValid && callTimeValidation.error) { - ErrorHandler.send(reply, callTimeValidation.error.code, callTimeValidation.error.message, { - field: callTimeValidation.error.field, - status: 400, - }); - return; - } - - try { - const result = await callAnalysisService.analyzeCall({ - phoneNumber: body.phoneNumber, - duration: body.duration, - callTime: new Date(body.callTime), - isVoip: body.isVoip, - }); - return reply.send({ - analysis: { - decision: result.decision, - confidence: result.confidence, - reasons: result.reasons, - }, - }); - } catch (error) { - ErrorHandler.send(reply, SpamErrorCode.ANALYSIS_FAILED, 'Call analysis failed', { - status: 422, - }); - } - }); - - // Record spam feedback - fastify.post('/feedback', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - const body = request.body as { - phoneNumber: string; - isSpam: boolean; - confidence?: number; - metadata?: Record; - }; - - const phoneValidation = ErrorHandler.validateRequiredField(body.phoneNumber, 'phoneNumber'); - if (!phoneValidation.isValid && phoneValidation.error) { - ErrorHandler.send(reply, phoneValidation.error.code, phoneValidation.error.message, { - field: phoneValidation.error.field, - status: 400, - }); - return; - } - - const isSpamValidation = ErrorHandler.validateBooleanField(body.isSpam, 'isSpam'); - if (!isSpamValidation.isValid && isSpamValidation.error) { - ErrorHandler.send(reply, isSpamValidation.error.code, isSpamValidation.error.message, { - field: isSpamValidation.error.field, - status: 400, - }); - return; - } - - try { - const feedback = await spamFeedbackService.recordFeedback( - userId, - body.phoneNumber, - body.isSpam, - body.confidence, - body.metadata - ); - return reply.code(201).send({ - feedback: { - id: feedback.id, - phoneNumber: feedback.phoneNumber, - isSpam: feedback.isSpam, - createdAt: feedback.createdAt, - }, - }); - } catch (error) { - ErrorHandler.send(reply, SpamErrorCode.FEEDBACK_RECORD_FAILED, 'Feedback recording failed', { - status: 422, - }); - } - }); - - // Get spam history - fastify.get('/history', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - const query = request.query as { - limit?: string; - isSpam?: string; - startDate?: string; - }; - - const results = await spamFeedbackService.getSpamHistory(userId, { - limit: query.limit ? parseInt(query.limit, 10) : undefined, - isSpam: query.isSpam !== undefined ? query.isSpam === 'true' : undefined, - startDate: query.startDate ? new Date(query.startDate) : undefined, - }); - - return reply.send({ - history: results.map((r) => ({ - id: r.id, - phoneNumber: r.phoneNumber, - isSpam: r.isSpam, - createdAt: r.createdAt, - })), - }); - }); - - // Get spam statistics - fastify.get('/statistics', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as FastifyRequest & { user?: { id: string } }; - const userId = authReq.user?.id; - - if (!userId) { - ErrorHandler.send(reply, SpamErrorCode.UNAUTHORIZED, 'User ID required', { status: 401 }); - return; - } - - try { - const stats = await spamFeedbackService.getStatistics(userId); - return reply.send({ statistics: stats }); - } catch (error) { - ErrorHandler.send(reply, SpamErrorCode.ANALYSIS_FAILED, 'Statistics retrieval failed', { - status: 422, - }); - } - }); -} diff --git a/packages/api/src/routes/subscription.routes.ts b/packages/api/src/routes/subscription.routes.ts deleted file mode 100644 index b387f22..0000000 --- a/packages/api/src/routes/subscription.routes.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { FastifyInstance } from 'fastify'; -import { BillingService } from '@shieldai/shared-billing/src/services/billing.service'; -import { SubscriptionService, customerService, webhookService } from '@shieldai/shared-billing/src/services/billing.services'; -import { SubscriptionTier, isValidReturnUrl } from '@shieldai/shared-billing/src/config/billing.config'; -import { AuthRequest } from './auth.middleware'; - -const billingService = BillingService.getInstance(); -const subscriptionService = new SubscriptionService(); - -export async function subscriptionRoutes(fastify: FastifyInstance) { - // Get current user's subscription - fastify.get( - '/subscription', - { - preHandler: async (request, reply) => { - await fastify.requireAuth(request as AuthRequest); - }, - }, - async (request, reply) => { - const authReq = request as AuthRequest; - - try { - const subscription = await billingService.getUserSubscription(authReq.user!.id); - - if (!subscription) { - return reply.status(404).send({ - error: 'No active subscription found', - message: 'Please create a subscription to access premium features', - }); - } - - return { - subscription: { - id: subscription.id, - status: subscription.status, - currentPeriodStart: new Date(subscription.current_period_start * 1000).toISOString(), - currentPeriodEnd: new Date(subscription.current_period_end * 1000).toISOString(), - cancelAtPeriodEnd: subscription.cancel_at_period_end, - created: new Date(subscription.created * 1000).toISOString(), - }, - customer: { - id: subscription.customer as string, - }, - }; - } catch (error) { - reply.status(500).send({ - error: 'Failed to fetch subscription', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Create a new subscription (for mobile app in-app purchases) - fastify.post( - '/subscription/create', - { - preHandler: async (request, reply) => { - await fastify.requireAuth(request as AuthRequest); - }, - }, - async (request, reply) => { - const authReq = request as AuthRequest; - const { tier, customerId } = request.body as { tier: SubscriptionTier; customerId: string }; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - if (!tier || !customerId) { - return reply.status(400).send({ - error: 'Missing required fields', - required: ['tier', 'customerId'], - }); - } - - try { - const result = await billingService.createSubscription( - authReq.user.id, - tier, - customerId - ); - - return { - subscription: { - id: result.subscription.id, - status: result.subscription.status, - currentPeriodStart: new Date(result.subscription.current_period_start * 1000).toISOString(), - currentPeriodEnd: new Date(result.subscription.current_period_end * 1000).toISOString(), - }, - customer: { - id: result.customer.id, - email: result.customer.email, - }, - }; - } catch (error) { - reply.status(500).send({ - error: 'Failed to create subscription', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Update subscription tier - fastify.put( - '/subscription/:subscriptionId/tier', - { - preHandler: async (request, reply) => { - await fastify.requireAuth(request as AuthRequest); - }, - }, - async (request, reply) => { - const authReq = request as AuthRequest; - const { subscriptionId } = request.params as { subscriptionId: string }; - const { tier } = request.body as { tier: SubscriptionTier }; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - if (!tier) { - return reply.status(400).send({ - error: 'Missing required field', - required: ['tier'], - }); - } - - try { - const updated = await billingService.updateSubscription( - subscriptionId, - authReq.user.id, - tier - ); - - return { - subscription: { - id: updated.id, - status: updated.status, - currentPeriodStart: new Date(updated.current_period_start * 1000).toISOString(), - currentPeriodEnd: new Date(updated.current_period_end * 1000).toISOString(), - }, - message: 'Subscription updated successfully', - }; - } catch (error) { - reply.status(500).send({ - error: 'Failed to update subscription', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Cancel subscription - fastify.delete( - '/subscription/:subscriptionId', - { - preHandler: async (request, reply) => { - await fastify.requireAuth(request as AuthRequest); - }, - }, - async (request, reply) => { - const authReq = request as AuthRequest; - const { subscriptionId } = request.params as { subscriptionId: string }; - const { cancelAtPeriodEnd } = request.body as { cancelAtPeriodEnd?: boolean }; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - try { - const cancelled = await billingService.cancelSubscription( - subscriptionId, - authReq.user.id, - cancelAtPeriodEnd ?? true - ); - - return { - subscription: { - id: cancelled.id, - status: cancelled.status, - cancelAtPeriodEnd: cancelled.cancel_at_period_end, - canceledAt: cancelled.canceled_at ? new Date(cancelled.canceled_at * 1000).toISOString() : null, - }, - message: cancelAtPeriodEnd - ? 'Subscription will cancel at the end of the billing period' - : 'Subscription cancelled immediately', - }; - } catch (error) { - reply.status(500).send({ - error: 'Failed to cancel subscription', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Create customer portal session - fastify.post( - '/customer/portal', - { - preHandler: async (request, reply) => { - await fastify.requireAuth(request as AuthRequest); - }, - }, - async (request, reply) => { - const authReq = request as AuthRequest; - const { customerId, returnUrl } = request.body as { customerId: string; returnUrl: string }; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - if (!customerId || !returnUrl) { - return reply.status(400).send({ - error: 'Missing required fields', - required: ['customerId', 'returnUrl'], - }); - } - - if (!isValidReturnUrl(returnUrl)) { - return reply.status(400).send({ - error: 'Invalid return URL', - message: 'returnUrl must be from an allowed origin', - }); - } - - try { - const portalSession = await billingService.createCustomerPortalSession( - customerId, - returnUrl - ); - - return { - url: portalSession.url, - expiresAt: new Date(portalSession.expires_at * 1000).toISOString(), - }; - } catch (error) { - reply.status(500).send({ - error: 'Failed to create portal session', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Create customer - fastify.post( - '/customer', - { - preHandler: async (request, reply) => { - await fastify.requireAuth(request as AuthRequest); - }, - }, - async (request, reply) => { - const authReq = request as AuthRequest; - const { email, name } = request.body as { email: string; name?: string }; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - if (!email) { - return reply.status(400).send({ - error: 'Email is required', - }); - } - - try { - const customer = await billingService.createCustomer(email, authReq.user.id); - - return { - customer: { - id: customer.id, - email: customer.email, - name: customer.name, - metadata: customer.metadata, - }, - }; - } catch (error) { - reply.status(500).send({ - error: 'Failed to create customer', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Get user tier - fastify.get( - '/user/tier', - { - preHandler: async (request, reply) => { - await fastify.requireAuth(request as AuthRequest); - }, - }, - async (request, reply) => { - const authReq = request as AuthRequest; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - try { - const tier = await billingService.getUserTier(authReq.user.id); - - if (!tier) { - return { - tier: 'free' as SubscriptionTier, - limits: await billingService.getTierLimits('free'), - }; - } - - const limits = await billingService.getTierLimits(tier); - - return { - tier, - limits, - }; - } catch (error) { - reply.status(500).send({ - error: 'Failed to fetch user tier', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Get invoice history - fastify.get( - '/invoices', - { - preHandler: async (request, reply) => { - await fastify.requireAuth(request as AuthRequest); - }, - }, - async (request, reply) => { - const authReq = request as AuthRequest; - const { customerId } = request.query as { customerId?: string }; - - if (!authReq.user?.id) { - return reply.status(401).send({ error: 'Authentication required' }); - } - - if (!customerId) { - return reply.status(400).send({ - error: 'customerId is required', - }); - } - - // Verify the customer belongs to the authenticated user (IDOR prevention) - try { - await billingService.verifyCustomerOwnership(customerId, authReq.user.id); - } catch { - return reply.status(403).send({ - error: 'Forbidden', - message: 'You do not have access to this customer', - }); - } - - try { - const invoices = await billingService.getInvoiceHistory(customerId); - - return { - invoices: invoices.data.map((invoice) => ({ - id: invoice.id, - amountDue: invoice.amount_due, - amountPaid: invoice.amount_paid, - status: invoice.status, - created: new Date(invoice.created * 1000).toISOString(), - hostedInvoiceUrl: invoice.hosted_invoice_url, - })), - hasMore: invoices.has_more, - }; - } catch (error) { - reply.status(500).send({ - error: 'Failed to fetch invoice history', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); - - // Webhook handler (public endpoint) - fastify.post( - '/webhooks/stripe', - { - // Skip authentication for webhooks - preHandler: async (request, reply) => { - // Don't require auth for webhooks - }, - }, - async (request, reply) => { - const sig = request.headers['stripe-signature'] as string; - - if (!sig) { - return reply.status(400).send({ - error: 'Missing Stripe signature', - }); - } - - try { - const event = await billingService.handleWebhook(sig, request.rawBody as Buffer); - - if (!event) { - return reply.status(200).send({ - received: true, - message: 'Event already processed', - }); - } - - // Handle different event types using webhook service - await webhookService.handleWebhook(event); - - return { received: true }; - } catch (error) { - console.error('Webhook error:', error); - return reply.status(400).send({ - error: 'Webhook handler failed', - message: error instanceof Error ? error.message : 'Unknown error', - }); - } - } - ); -} diff --git a/packages/api/src/routes/voiceprint.routes.ts b/packages/api/src/routes/voiceprint.routes.ts deleted file mode 100644 index f8df23a..0000000 --- a/packages/api/src/routes/voiceprint.routes.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import fastifyMultipart from '@fastify/multipart'; -import { - voiceEnrollmentService, - analysisService, - batchAnalysisService, - voicePrintEnv, -} from '../services/voiceprint'; - -interface AuthenticatedRequest extends FastifyRequest { - user?: { id: string; email: string; role: string }; - authType?: 'jwt' | 'api-key' | 'anonymous'; -} - -export async function voiceprintRoutes(fastify: FastifyInstance) { - // P1-2 fix: Require authentication on all VoicePrint routes - fastify.addHook('onRequest', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthenticatedRequest; - if (authReq.authType === 'anonymous' || !authReq.user?.id || authReq.user.id === 'anonymous') { - return reply.code(401).send({ error: 'Authentication required' }); - } - }); - - // P1-3 fix: Register multipart for audio file uploads - await fastify.register(fastifyMultipart, { - limits: { - fileSize: voicePrintEnv.ENROLLMENT_MAX_DURATION_SEC > 0 - ? 50 * 1024 * 1024 // 50MB max file size for audio - : 50 * 1024 * 1024, - }, - }); - // Enroll a new voice profile - fastify.post('/enroll', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthenticatedRequest; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - // P1-3 fix: Parse multipart form-data for audio upload - let name: string | undefined; - let audioBuffer: Buffer | undefined; - - for await (const part of request.files()) { - if (part.type === 'file') { - audioBuffer = await part.toBuffer(); - name = name || part.filename || 'voice_enrollment'; - } else if (part.fieldname === 'name') { - name = part.value; - } - } - - if (!audioBuffer || audioBuffer.length === 0) { - return reply.code(400).send({ error: 'audio file is required' }); - } - - try { - const enrollment = await voiceEnrollmentService.enroll( - userId, - name || 'voice_enrollment', - audioBuffer - ); - return reply.code(201).send({ - enrollment: { - id: enrollment.id, - name: enrollment.name, - isActive: enrollment.isActive, - createdAt: enrollment.createdAt, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Enrollment failed'; - return reply.code(422).send({ error: message }); - } - }); - - // List user's voice enrollments - fastify.get('/enrollments', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthenticatedRequest; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const isActive = request.query as { isActive?: string }; - const limit = request.query as { limit?: string }; - const offset = request.query as { offset?: string }; - - const enrollments = await voiceEnrollmentService.listEnrollments(userId, { - isActive: isActive.isActive !== undefined - ? isActive.isActive === 'true' - : undefined, - limit: limit.limit ? parseInt(limit.limit, 10) : undefined, - offset: offset.offset ? parseInt(offset.offset, 10) : undefined, - }); - - return reply.send({ - enrollments: enrollments.map((e) => ({ - id: e.id, - name: e.name, - isActive: e.isActive, - createdAt: e.createdAt, - })), - }); - }); - - // Remove an enrollment - fastify.delete('/enrollments/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthenticatedRequest; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const enrollmentId = (request.params as { id: string }).id; - - try { - const enrollment = await voiceEnrollmentService.removeEnrollment( - enrollmentId, - userId - ); - return reply.send({ - enrollment: { - id: enrollment.id, - name: enrollment.name, - isActive: enrollment.isActive, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Removal failed'; - return reply.code(404).send({ error: message }); - } - }); - - // Analyze a single audio file - fastify.post('/analyze', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthenticatedRequest; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - // P1-3 fix: Parse multipart form-data for audio upload - let audioBuffer: Buffer | undefined; - let enrollmentId: string | undefined; - let audioUrl: string | undefined; - - for await (const part of request.files()) { - if (part.type === 'file') { - audioBuffer = await part.toBuffer(); - } else if (part.fieldname === 'enrollmentId') { - enrollmentId = part.value; - } else if (part.fieldname === 'audioUrl') { - audioUrl = part.value; - } - } - - if (!audioBuffer || audioBuffer.length === 0) { - return reply.code(400).send({ error: 'audio file is required' }); - } - - try { - const result = await analysisService.analyze(userId, audioBuffer, { - enrollmentId, - audioUrl, - }); - return reply.code(201).send({ - analysis: { - id: result.id, - isSynthetic: result.isSynthetic, - confidence: result.confidence, - analysisResult: result.analysisResult, - createdAt: result.createdAt, - }, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Analysis failed'; - return reply.code(422).send({ error: message }); - } - }); - - // Get analysis result by ID - fastify.get('/results/:id', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthenticatedRequest; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const analysisId = (request.params as { id: string }).id; - const result = await analysisService.getResult(analysisId, userId); - - if (!result) { - return reply.code(404).send({ error: 'Analysis not found' }); - } - - return reply.send({ - analysis: { - id: result.id, - isSynthetic: result.isSynthetic, - confidence: result.confidence, - analysisResult: result.analysisResult, - createdAt: result.createdAt, - }, - }); - }); - - // Get analysis history - fastify.get('/history', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthenticatedRequest; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - const query = request.query as { - limit?: string; - offset?: string; - isSynthetic?: string; - }; - - const results = await analysisService.getHistory(userId, { - limit: query.limit ? parseInt(query.limit, 10) : undefined, - offset: query.offset ? parseInt(query.offset, 10) : undefined, - isSynthetic: query.isSynthetic !== undefined - ? query.isSynthetic === 'true' - : undefined, - }); - - return reply.send({ - analyses: results.map((r) => ({ - id: r.id, - isSynthetic: r.isSynthetic, - confidence: r.confidence, - createdAt: r.createdAt, - })), - }); - }); - - // Batch analyze multiple audio files - fastify.post('/batch', async (request: FastifyRequest, reply: FastifyReply) => { - const authReq = request as AuthenticatedRequest; - const userId = authReq.user?.id; - - if (!userId) { - return reply.code(401).send({ error: 'User ID required' }); - } - - // P1-3 fix: Parse multipart form-data for multiple audio uploads - const files: Array<{ name: string; buffer: Buffer; audioUrl?: string }> = []; - let enrollmentId: string | undefined; - - for await (const part of request.files()) { - if (part.type === 'file') { - const buffer = await part.toBuffer(); - files.push({ - name: part.filename || `file_${files.length}`, - buffer, - }); - } else if (part.fieldname === 'enrollmentId') { - enrollmentId = part.value; - } else if (part.fieldname === 'audioUrl') { - if (files.length > 0) { - files[files.length - 1].audioUrl = part.value; - } - } - } - - if (files.length === 0) { - return reply.code(400).send({ error: 'at least one audio file is required' }); - } - - try { - const result = await batchAnalysisService.analyzeBatch( - userId, - files, - { enrollmentId } - ); - - return reply.code(201).send({ - jobId: result.jobId, - results: result.results.map((r) => ({ - id: r.id, - isSynthetic: r.isSynthetic, - confidence: r.confidence, - })), - summary: result.summary, - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Batch analysis failed'; - return reply.code(422).send({ error: message }); - } - }); -} diff --git a/packages/api/src/routes/waitlist.routes.ts b/packages/api/src/routes/waitlist.routes.ts deleted file mode 100644 index 6a5d9bc..0000000 --- a/packages/api/src/routes/waitlist.routes.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify'; -import { prisma } from '@shieldai/db'; -import { EmailService } from '@shieldai/shared-notifications'; -import { Queue } from 'bullmq'; -import { Redis } from 'ioredis'; - -const redisUrl = process.env.REDIS_URL || 'redis://localhost:6379'; -const connection = new Redis(redisUrl); -const waitlistEmailQueue = new Queue('waitlist-emails', { connection }); - -interface WaitlistSignupBody { - email: string; - name?: string; - tier?: string; - utmSource?: string; - utmMedium?: string; - utmCampaign?: string; -} - -function getPosition(entryId: string): string { - const hash = entryId.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0); - return String(10000 + (hash % 90000)); -} - -const DAY_MS = 24 * 60 * 60 * 1000; - -export async function waitlistRoutes(fastify: FastifyInstance) { - fastify.post('/waitlist/signup', async (request: FastifyRequest, reply: FastifyReply) => { - const body = request.body as WaitlistSignupBody; - - if (!body.email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(body.email)) { - return reply.code(400).send({ error: 'Valid email is required' }); - } - - const email = body.email.toLowerCase().trim(); - - const existing = await prisma.waitlistEntry.findFirst({ - where: { email }, - }); - - if (existing) { - return reply.code(200).send({ - message: 'Already on the waitlist', - id: existing.id, - }); - } - - const validTiers = ['basic', 'plus', 'premium'] as const; - const tier = validTiers.includes(body.tier as typeof validTiers[number]) - ? (body.tier as string) - : undefined; - - const entry = await prisma.waitlistEntry.create({ - data: { - email, - name: body.name?.trim() || null, - source: 'landing_page', - tier: tier as any || null, - utmSource: body.utmSource || null, - utmMedium: body.utmMedium || null, - utmCampaign: body.utmCampaign || null, - }, - }); - - const name = body.name?.trim() || 'there'; - const position = getPosition(entry.id); - - try { - const emailService = EmailService.getInstance(); - const result = await emailService.sendWithTemplate(email, { - templateId: 'waitlist_confirmation', - variables: { name, position }, - }); - if (result.status === 'failed') { - request.log.warn({ error: result.error }, 'Failed to send waitlist confirmation email'); - } else { - request.log.info({ email }, 'Waitlist confirmation email sent'); - } - } catch (err) { - request.log.error({ err }, 'Error sending waitlist confirmation email'); - } - - try { - await Promise.all([ - waitlistEmailQueue.add( - 'send-waitlist-intro', - { email, name, entryId: entry.id, tier }, - { delay: 1 * DAY_MS, attempts: 3, backoff: { type: 'exponential', delay: 5000 } } - ), - waitlistEmailQueue.add( - 'send-waitlist-features', - { email, name, entryId: entry.id, tier }, - { delay: 3 * DAY_MS, attempts: 3, backoff: { type: 'exponential', delay: 5000 } } - ), - waitlistEmailQueue.add( - 'send-waitlist-launch-teaser', - { email, name, entryId: entry.id, tier }, - { delay: 7 * DAY_MS, attempts: 3, backoff: { type: 'exponential', delay: 5000 } } - ), - ]); - request.log.info({ email }, 'Welcome sequence scheduled'); - } catch (err) { - request.log.error({ err }, 'Failed to schedule welcome sequence emails'); - } - - return reply.code(201).send({ - message: 'Welcome to the ShieldAI waitlist', - id: entry.id, - }); - }); - - fastify.get('/waitlist/count', async (_request: FastifyRequest, reply: FastifyReply) => { - const count = await prisma.waitlistEntry.count(); - return reply.send({ count }); - }); -} diff --git a/packages/api/src/routes/watchlist.routes.ts b/packages/api/src/routes/watchlist.routes.ts deleted file mode 100644 index c7aec88..0000000 --- a/packages/api/src/routes/watchlist.routes.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { FastifyInstance } from "fastify"; -import { WatchListService } from "@shieldai/darkwatch"; -import { IdentifierType } from "@shieldai/types"; - -export function watchlistRoutes(fastify: FastifyInstance) { - const service = new WatchListService(); - - fastify.post("/", async (request, reply) => { - const body = request.body as { identifierType: string; identifierValue: string }; - const userId = (request.user as { id: string })?.id; - - if (!userId) { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const item = await service.addItem(userId, body.identifierType as IdentifierType, body.identifierValue); - return reply.code(201).send(item); - }); - - fastify.get("/", async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const items = await service.listItems(userId); - return reply.send(items); - }); - - fastify.delete("/:id", async (request, reply) => { - const userId = (request.user as { id: string })?.id; - - if (!userId) { - return reply.code(401).send({ error: "User not authenticated" }); - } - - const result = await service.removeItem(userId, request.params.id); - return reply.send({ count: result.count }); - }); -} diff --git a/packages/api/src/routes/webhook.routes.ts b/packages/api/src/routes/webhook.routes.ts deleted file mode 100644 index 0fe8112..0000000 --- a/packages/api/src/routes/webhook.routes.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { FastifyInstance } from "fastify"; -import { WebhookHandler } from "@shieldai/darkwatch"; - -export function webhookRoutes(fastify: FastifyInstance) { - const handler = new WebhookHandler(); - - fastify.post( - "/", - async (request, reply) => { - const body = request.body as { - eventType: string; - payload: Record; - source?: string; - }; - - const signature = - (request.headers["x-webhook-signature"] as string) || - (request.headers["x-hub-signature-256"] as string) || - undefined; - - try { - const result = await handler.processEvent( - body.eventType, - body.payload, - body.source, - signature - ); - - return reply.code(200).send({ - eventId: result.eventId, - scanTriggered: result.scanTriggered, - }); - } catch (err) { - console.error("[Webhook] Event processing error:", err); - return reply.code(400).send({ error: "Webhook processing failed" }); - } - } - ); - - fastify.get( - "/history", - async (request, reply) => { - const limit = parseInt((request.query as { limit?: string }).limit || "50"); - const offset = parseInt((request.query as { offset?: string }).offset || "0"); - - const events = await handler.getEventHistory(limit, offset); - return reply.send(events); - } - ); - - fastify.get( - "/user/:userId", - async (request, reply) => { - const params = request.params as { userId: string }; - const authedUser = (request.user as { id: string })?.id; - if (authedUser !== params.userId) { - return reply.code(403).send({ error: "Forbidden" }); - } - const limit = parseInt((request.query as { limit?: string }).limit || "50"); - const offset = parseInt((request.query as { offset?: string }).offset || "0"); - - const events = await handler.getUserEvents(params.userId, limit, offset); - return reply.send(events); - } - ); -} diff --git a/packages/api/src/seed.ts b/packages/api/src/seed.ts deleted file mode 100644 index 0343025..0000000 --- a/packages/api/src/seed.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { prisma } from '@shieldai/db'; - -const blogPosts = [ - { - slug: 'what-is-ai-voice-cloning', - title: 'What Is AI Voice Cloning and How to Protect Your Family', - excerpt: 'AI voice cloning technology is advancing rapidly. Learn how scammers use it to impersonate loved ones and how ShieldAI detects these attacks in real time.', - content: `

Understanding AI Voice Cloning

-

AI voice cloning uses deep learning models to analyze a small sample of someone's voice—sometimes just a few seconds from a social media video or phone call—and generate new speech that sounds identical to the original speaker.

- -

How Scammers Exploit It

-

The most common attack pattern involves a scammer calling a victim while using a cloned voice of a family member. The fake "family member" claims to be in distress—needing bail money, hospital fees, or help with a car accident. The emotional urgency makes victims less likely to question the call's authenticity.

- -

Warning Signs

-
    -
  • Unexpected calls from family members asking for money
  • -
  • Slight delays or unnatural pauses in speech
  • -
  • Background noise that doesn't match the claimed location
  • -
  • Requests to keep the call secret or avoid contacting other family members
  • -
- -

How ShieldAI Protects You

-

ShieldAI's VoicePrint technology creates audio fingerprints for each family member's voice. When an incoming call is detected, our AI analyzes the audio in real time and flags any call that doesn't match the verified voiceprint. You'll receive an instant alert if a voice clone is suspected.

`, - authorName: 'ShieldAI Team', - tags: ['voice cloning', 'AI scams', 'family protection'], - published: true, - }, - { - slug: 'dark-web-monitoring-guide', - title: 'Dark Web Monitoring: What Gets Exposed and How to Stay Safe', - excerpt: 'Your personal data is traded on dark web marketplaces every day. Here is what criminals buy, how they use it, and how ShieldAI monitors for your exposure.', - content: `

What Is the Dark Web?

-

The dark web is a hidden part of the internet accessible only through specialized browsers like Tor. While it has legitimate uses for privacy and journalism, it is also the primary marketplace for stolen data, including emails, passwords, phone numbers, and Social Security numbers.

- -

What Data Gets Exposed

-
    -
  • Email addresses — used for phishing and credential stuffing attacks
  • -
  • Phone numbers — sold to robocallers and used for SIM swapping
  • -
  • Passwords — sold in bulk for account takeover attempts
  • -
  • Social Security Numbers — used for identity theft and tax fraud
  • -
  • Home addresses — used for physical threats and doxxing
  • -
- -

How ShieldAI Monitors for You

-

ShieldAI continuously scans dark web marketplaces, forums, and known data leak repositories. When your monitored data appears in a new leak, we send you an immediate alert with details about what was exposed and recommended next steps.

- -

What to Do If Your Data Is Leaked

-
    -
  1. Change passwords immediately — use unique passwords for each service
  2. -
  3. Enable two-factor authentication everywhere
  4. -
  5. Freeze your credit if SSN was exposed
  6. -
  7. Monitor bank and credit card statements for unusual activity
  8. -
  9. Run a ShieldAI dark web scan to check for additional exposures
  10. -
`, - authorName: 'ShieldAI Team', - tags: ['dark web', 'data breach', 'identity theft'], - published: true, - }, - { - slug: 'spam-call-statistics-2025', - title: 'Spam Call Statistics 2025: The Rise of AI-Powered Phone Scams', - excerpt: 'Spam calls are at an all-time high, and AI is making them harder to detect. Here are the latest numbers and what you can do to protect yourself.', - content: `

The Scale of the Problem

-

In 2025, Americans received an estimated 55 billion spam calls — an average of 15 calls per person per month. AI-powered scam calls now account for 40% of all phone fraud attempts, up from just 12% in 2023.

- -

Key Statistics

-
    -
  • 1 in 3 Americans report losing money to phone scams
  • -
  • Average loss per victim: $1,200
  • -
  • 68% of scam calls now use AI-generated voices
  • -
  • Elderly individuals (65+) are 3x more likely to fall victim
  • -
  • Most common scam: fake tech support (32% of all reports)
  • -
- -

Why Traditional Blocking Falls Short

-

Traditional spam blockers rely on known phone number databases. But AI-powered scammers constantly rotate numbers, spoof caller IDs, and use voice cloning to bypass voice-based verification. ShieldAI's machine learning approach classifies calls based on behavioral patterns, not just number reputation — catching new scams that traditional methods miss.

`, - authorName: 'ShieldAI Team', - tags: ['spam calls', 'statistics', 'AI scams'], - published: true, - }, -]; - -async function seed() { - console.log('Seeding blog posts...'); - - for (const post of blogPosts) { - const existing = await prisma.blogPost.findUnique({ where: { slug: post.slug } }); - if (existing) { - console.log(` Skipping "${post.slug}" — already exists`); - continue; - } - - await prisma.blogPost.create({ - data: { - ...post, - publishedAt: new Date(), - }, - }); - console.log(` Created "${post.slug}"`); - } - - console.log('Seed complete!'); -} - -seed() - .catch((e) => { - console.error('Seed failed:', e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts deleted file mode 100644 index 05030ff..0000000 --- a/packages/api/src/server.ts +++ /dev/null @@ -1,120 +0,0 @@ -// dd-trace must be initialized before any other module is loaded for auto-instrumentation -import '@shieldai/monitoring/datadog-init'; -import Fastify from "fastify"; -import cors from "@fastify/cors"; -import helmet from "@fastify/helmet"; -import sensible from "@fastify/sensible"; -import rawBody from "fastify-raw-body"; -import { extractOrGenerateRequestId } from "@shieldai/types"; -import { authMiddleware } from "./middleware/auth.middleware"; -import { errorHandlingMiddleware } from "./middleware/error-handling.middleware"; -import { loggingMiddleware } from "./middleware/logging.middleware"; -import { monitoringMiddleware } from "./middleware/monitoring.middleware"; -import { darkwatchRoutes } from "./routes/darkwatch.routes"; -import { voiceprintRoutes } from "./routes/voiceprint.routes"; -import { correlationRoutes } from "./routes/correlation.routes"; -import { extensionRoutes } from "./routes/extension.routes"; -import { waitlistRoutes } from "./routes/waitlist.routes"; -import { blogRoutes } from "./routes/blog.routes"; -import { blogAdminRoutes } from "./routes/blog-admin.routes"; -import { routes } from "./routes"; -import { captureSentryError } from "@shieldai/monitoring"; -import { getCorsOrigins } from "./config/api.config"; -import fastifySwagger from "@fastify/swagger"; -import fastifySwaggerUi from "@fastify/swagger-ui"; -import * as fs from "fs"; -import * as path from "path"; - -const app = Fastify({ - logger: { - level: process.env.LOG_LEVEL || "info", - }, -}); - -async function bootstrap() { - const corsOrigins = getCorsOrigins(); - await app.register(cors, { origin: corsOrigins }); - await app.register(helmet); - await app.register(sensible); - await app.register(rawBody, { runFirst: true }); - - // Register auth middleware to populate request.user - await app.register(authMiddleware); - - // Register logging middleware (request/response logging) - await app.register(loggingMiddleware); - - // Register monitoring middleware (CloudWatch metrics) - await app.register(monitoringMiddleware); - - // Register error handling middleware (Sentry integration) - await app.register(errorHandlingMiddleware); - - app.addHook("onRequest", async (request, _reply) => { - const requestId = extractOrGenerateRequestId(request.headers); - request.id = requestId; - const pinoLog = request.log as typeof request.log & { bindings?: Record; bindActive?: () => void }; - pinoLog.bindings = { requestId }; - pinoLog.bindActive?.(); - request.headers["x-request-id"] = requestId; - }); - - await app.register(routes); - - app.get("/health", async () => ({ status: "ok", timestamp: new Date().toISOString() })); - - // Swagger/OpenAPI documentation - const openapiSpec = JSON.parse( - fs.readFileSync(path.join(__dirname, "openapi", "spec.json"), "utf-8"), - ) as Record; - - const swaggerDefinition: Record = { - openapi: "3.0.3", - info: { - title: "ShieldAI API", - description: - "ShieldAI API documentation — reverse-engineer endpoints and run contract tests", - version: "1.0.0", - }, - servers: openapiSpec.servers, - paths: openapiSpec.paths, - components: openapiSpec.components, - security: openapiSpec.security, - tags: openapiSpec.tags, - }; - - await app.register(fastifySwagger, { - openapi: swaggerDefinition, - }); - - await app.register(fastifySwaggerUi, { - routePrefix: "/docs", - uiConfig: { - docExpansion: "list", - }, - staticCSP: true, - theme: { - js: [ - { - filename: "custom.js", - content: ` - window.addEventListener('DOMContentLoaded', () => { - document.querySelector('link[rel="icon"]')?.remove(); - }); - `, - }, - ], - }, - }); - - try { - await app.listen({ port: parseInt(process.env.PORT || "3000", 10), host: "0.0.0.0" }); - app.log.info(`Server listening on port ${process.env.PORT || 3000}`); - } catch (err) { - app.log.error(err); - captureSentryError(err as Error, { context: "server_startup" }); - process.exit(1); - } -} - -bootstrap(); diff --git a/packages/api/src/services/darkwatch/alert.pipeline.ts b/packages/api/src/services/darkwatch/alert.pipeline.ts deleted file mode 100644 index a3d2f1a..0000000 --- a/packages/api/src/services/darkwatch/alert.pipeline.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { prisma, AlertType, AlertSeverity } from '@shieldai/db'; -import { - NotificationService, - NotificationPriority, - loadNotificationConfig, -} from '@shieldsai/shared-notifications'; - -const ALERT_DEDUP_WINDOW_MS = 24 * 60 * 60 * 1000; - -export class AlertPipeline { - private notificationService: NotificationService; - - constructor() { - this.notificationService = new NotificationService(loadNotificationConfig()); - } - - async processNewExposures(exposureIds: string[]) { - const exposures = await prisma.exposure.findMany({ - where: { id: { in: exposureIds }, isFirstTime: true }, - include: { - subscription: { - select: { - id: true, - userId: true, - tier: true, - }, - }, - watchlistItem: true, - }, - }); - - const alertsCreated: Awaited>[] = []; - - for (const exposure of exposures) { - const dedupKey = `exposure:${exposure.subscriptionId}:${exposure.source}:${exposure.identifierHash}`; - - const recentAlert = await prisma.alert.findFirst({ - where: { - subscriptionId: exposure.subscriptionId, - type: AlertType.exposure_detected, - createdAt: { - gte: new Date(Date.now() - ALERT_DEDUP_WINDOW_MS), - }, - }, - orderBy: { createdAt: 'desc' }, - }); - - if (recentAlert) { - continue; - } - - const alert = await prisma.alert.create({ - data: { - subscriptionId: exposure.subscriptionId, - userId: exposure.subscription.userId, - exposureId: exposure.id, - type: AlertType.exposure_detected, - title: this.buildTitle(exposure), - message: this.buildMessage(exposure), - severity: this.mapSeverity(exposure.severity), - channel: this.getChannelsForTier(exposure.subscription.tier), - }, - }); - - alertsCreated.push(alert); - - await this.dispatchNotification(alert, exposure); - } - - return alertsCreated; - } - - async dispatchScanCompleteAlert( - subscriptionId: string, - userId: string, - exposuresFound: number - ) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { tier: true }, - }); - - if (!subscription) return; - - const alert = await prisma.alert.create({ - data: { - subscriptionId, - userId, - type: AlertType.scan_complete, - title: 'DarkWatch Scan Complete', - message: `Scan found ${exposuresFound} new exposure${exposuresFound === 1 ? '' : 's'}.`, - severity: exposuresFound > 0 ? 'warning' : 'info', - channel: this.getChannelsForTier(subscription.tier), - }, - }); - - await this.dispatchNotification(alert, { - source: 'hibp', - severity: 'info', - identifier: '', - dataType: 'email', - } as any); - - return alert; - } - - private async dispatchNotification( - alert: { - userId: string; - channel: string[]; - title: string; - message: string; - severity: AlertSeverity; - }, - exposure: { source: string; severity: string; identifier: string; dataType: string } - ) { - try { - if (!this.notificationService.isFullyConfigured()) return; - - await this.notificationService.sendMultiChannelNotification( - { - userId: alert.userId, - }, - alert.channel as any, - alert.title, - `

${alert.message}

-

Source: ${exposure.source}

-

Severity: ${exposure.severity}

-

Type: ${exposure.dataType}

`, - alert.severity === 'critical' - ? NotificationPriority.HIGH - : NotificationPriority.NORMAL - ); - } catch (error) { - console.error('[AlertPipeline] Notification dispatch error:', error); - } - } - - private buildTitle(exposure: { - source: string; - dataType: string; - severity: string; - }): string { - return `${exposure.severity.toUpperCase()}: ${exposure.dataType} exposure on ${exposure.source}`; - } - - private buildMessage(exposure: { - identifier: string; - source: string; - severity: string; - dataType: string; - }): string { - const masked = exposure.identifier.includes('@') - ? exposure.identifier.replace(/(?<=.{2}).*(?=@)/, '***') - : exposure.identifier.slice(0, 3) + '***'; - - return `Your ${exposure.dataType} (${masked}) was found in a ${exposure.source} breach with ${exposure.severity} severity.`; - } - - private mapSeverity(severity: string): AlertSeverity { - return severity as AlertSeverity; - } - - private getChannelsForTier(tier: string): string[] { - const channelMap: Record = { - basic: ['email'], - plus: ['email', 'push'], - premium: ['email', 'push', 'sms'], - }; - return channelMap[tier] || ['email']; - } -} - -export const alertPipeline = new AlertPipeline(); diff --git a/packages/api/src/services/darkwatch/index.ts b/packages/api/src/services/darkwatch/index.ts deleted file mode 100644 index f18ad02..0000000 --- a/packages/api/src/services/darkwatch/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { watchlistService } from './watchlist.service'; -export { scanService } from './scan.service'; -export { schedulerService } from './scheduler.service'; -export { webhookService } from './webhook.service'; -export { alertPipeline } from './alert.pipeline'; diff --git a/packages/api/src/services/darkwatch/scan.service.ts b/packages/api/src/services/darkwatch/scan.service.ts deleted file mode 100644 index 61a3401..0000000 --- a/packages/api/src/services/darkwatch/scan.service.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { prisma, ExposureSource, ExposureSeverity, WatchlistType } from '@shieldai/db'; -import { createHash } from 'crypto'; - -function hashIdentifier(identifier: string): string { - return createHash('sha256').update(identifier.toLowerCase().trim()).digest('hex'); -} - -function determineSeverity( - source: ExposureSource, - dataType: WatchlistType -): ExposureSeverity { - const criticalSources = [ExposureSource.darkWebForum, ExposureSource.honeypot]; - const warningSources = [ExposureSource.hibp, ExposureSource.shodan]; - const criticalTypes = [WatchlistType.ssn]; - - if (criticalTypes.includes(dataType)) return ExposureSeverity.critical; - if (criticalSources.includes(source)) return ExposureSeverity.critical; - if (warningSources.includes(source)) return ExposureSeverity.warning; - return ExposureSeverity.info; -} - -export class ScanService { - async checkHIBP(email: string): Promise<{ exposed: boolean; sources: string[] }> { - try { - const response = await fetch( - `https://hibp.com/api/v2/${encodeURIComponent(email)}`, - { - headers: { - 'hibp-api-key': process.env.HIBP_API_KEY || '', - Accept: 'application/json', - }, - signal: AbortSignal.timeout(15000), - } - ); - - if (response.status === 404) { - return { exposed: false, sources: [] }; - } - - if (!response.ok) { - console.error(`[ScanService:HIBP] Status ${response.status} for ${email}`); - return { exposed: false, sources: [] }; - } - - const data = await response.json(); - const sources = Array.isArray(data) - ? data.map((p: { Name: string }) => p.Name) - : []; - - return { exposed: sources.length > 0, sources }; - } catch (error) { - console.error('[ScanService:HIBP] Error:', error); - return { exposed: false, sources: [] }; - } - } - - async checkShodan(domain: string): Promise<{ exposed: boolean; ports: string[]; ips: string[] }> { - try { - const response = await fetch( - `https://api.shodan.io/shodan/host/${encodeURIComponent(domain)}`, - { - headers: { - Authorization: `Bearer ${process.env.SHODAN_API_KEY || ''}`, - }, - signal: AbortSignal.timeout(15000), - } - ); - - if (response.status === 404) { - return { exposed: false, ports: [], ips: [] }; - } - - if (!response.ok) { - console.error(`[ScanService:Shodan] Status ${response.status} for ${domain}`); - return { exposed: false, ports: [], ips: [] }; - } - - const data = await response.json(); - return { - exposed: !!data.ip_str, - ports: data.ports?.map(String) || [], - ips: [data.ip_str || ''], - }; - } catch (error) { - console.error('[ScanService:Shodan] Error:', error); - return { exposed: false, ports: [], ips: [] }; - } - } - - async processSubscriptionScan( - subscriptionId: string, - watchlistItems: Awaited> - ): Promise<{ exposuresCreated: number; exposuresUpdated: number }> { - let exposuresCreated = 0; - let exposuresUpdated = 0; - - for (const item of watchlistItems) { - const identifier = item.value; - const identifierHash = hashIdentifier(identifier); - - switch (item.type) { - case WatchlistType.email: { - const hibpResult = await this.checkHIBP(identifier); - if (hibpResult.exposed) { - for (const source of hibpResult.sources) { - const existing = await prisma.exposure.findFirst({ - where: { - subscriptionId, - source: ExposureSource.hibp, - identifierHash, - metadata: { path: ['dbName'], equals: source }, - }, - }); - - if (existing) { - await prisma.exposure.update({ - where: { id: existing.id }, - data: { detectedAt: new Date() }, - }); - exposuresUpdated++; - } else { - await prisma.exposure.create({ - data: { - subscriptionId, - watchlistItemId: item.id, - source: ExposureSource.hibp, - dataType: item.type, - identifier, - identifierHash, - severity: determineSeverity(ExposureSource.hibp, item.type), - isFirstTime: true, - metadata: { dbName: source }, - detectedAt: new Date(), - }, - }); - exposuresCreated++; - } - } - } - break; - } - - case WatchlistType.domain: { - const shodanResult = await this.checkShodan(identifier); - if (shodanResult.exposed) { - const existing = await prisma.exposure.findFirst({ - where: { - subscriptionId, - source: ExposureSource.shodan, - identifierHash, - }, - }); - - if (existing) { - await prisma.exposure.update({ - where: { id: existing.id }, - data: { - detectedAt: new Date(), - metadata: { ports: shodanResult.ports, ips: shodanResult.ips }, - }, - }); - exposuresUpdated++; - } else { - await prisma.exposure.create({ - data: { - subscriptionId, - watchlistItemId: item.id, - source: ExposureSource.shodan, - dataType: item.type, - identifier, - identifierHash, - severity: determineSeverity(ExposureSource.shodan, item.type), - isFirstTime: true, - metadata: { ports: shodanResult.ports, ips: shodanResult.ips }, - detectedAt: new Date(), - }, - }); - exposuresCreated++; - } - } - break; - } - - default: { - const existing = await prisma.exposure.findFirst({ - where: { subscriptionId, watchlistItemId: item.id, identifierHash }, - }); - - if (!existing) { - await prisma.exposure.create({ - data: { - subscriptionId, - watchlistItemId: item.id, - source: ExposureSource.darkWebForum, - dataType: item.type, - identifier, - identifierHash, - severity: determineSeverity(ExposureSource.darkWebForum, item.type), - isFirstTime: true, - detectedAt: new Date(), - }, - }); - exposuresCreated++; - } - break; - } - } - } - - return { exposuresCreated, exposuresUpdated }; - } - - async getWatchlistItems(subscriptionId: string) { - return prisma.watchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - }); - } -} - -export const scanService = new ScanService(); diff --git a/packages/api/src/services/darkwatch/scheduler.service.ts b/packages/api/src/services/darkwatch/scheduler.service.ts deleted file mode 100644 index fefd4b8..0000000 --- a/packages/api/src/services/darkwatch/scheduler.service.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { prisma, SubscriptionTier, SubscriptionStatus } from '@shieldai/db'; -import { tierConfig } from '@shieldsai/shared-billing'; -import { darkwatchScanQueue } from '@shieldsai/jobs'; -import { randomUUID } from 'crypto'; - -const CRON_EXPRESSIONS = { - daily: '0 0 * * *', - hourly: '0 * * * *', - realtime: null, -}; - -export class SchedulerService { - async scheduleSubscriptionScans() { - const activeSubscriptions = await prisma.subscription.findMany({ - where: { - tier: { in: [SubscriptionTier.basic, SubscriptionTier.plus, SubscriptionTier.premium] }, - status: SubscriptionStatus.active, - }, - select: { - id: true, - tier: true, - userId: true, - }, - }); - - const jobsEnqueued = []; - - for (const subscription of activeSubscriptions) { - const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency; - const cron = CRON_EXPRESSIONS[frequency]; - - if (!cron) { - continue; - } - - const jobKey = `scheduled-scan:${subscription.id}`; - - try { - await darkwatchScanQueue.add( - 'scheduled-scan', - { - subscriptionId: subscription.id, - tier: subscription.tier, - scanType: 'scheduled', - }, - { - jobId: jobKey, - repeat: { - every: frequency === 'daily' - ? 24 * 60 * 60 * 1000 - : 60 * 60 * 1000, - }, - priority: subscription.tier === SubscriptionTier.premium ? 1 : 3, - } - ); - - jobsEnqueued.push({ - subscriptionId: subscription.id, - tier: subscription.tier, - frequency, - }); - } catch (error) { - if ((error as Error).message?.includes('Duplicate')) { - continue; - } - console.error( - `[SchedulerService] Failed to schedule scan for ${subscription.id}:`, - error - ); - } - } - - return jobsEnqueued; - } - - async enqueueOnDemandScan(subscriptionId: string) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - throw new Error(`Subscription ${subscriptionId} not found`); - } - - return darkwatchScanQueue.add( - 'on-demand-scan', - { - subscriptionId, - tier: subscription.tier, - scanType: 'on-demand', - }, - { - priority: 1, - jobId: `on-demand-scan:${subscriptionId}:${randomUUID()}`, - } - ); - } - - async enqueueRealtimeTrigger(subscriptionId: string, sourceData: Record) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { id: true, tier: true }, - }); - - if (!subscription || subscription.tier !== SubscriptionTier.premium) { - throw new Error('Realtime triggers require Premium tier'); - } - - return darkwatchScanQueue.add( - 'realtime-trigger', - { - subscriptionId, - tier: subscription.tier, - scanType: 'realtime', - sourceData, - }, - { - priority: 0, - jobId: `realtime-trigger:${subscriptionId}:${randomUUID()}`, - } - ); - } - - async rescheduleAll() { - const repeatableJobs = await darkwatchScanQueue.getRepeatableJobs(); - - for (const job of repeatableJobs) { - await darkwatchScanQueue.removeRepeatableByKey(job.key); - } - - return this.scheduleSubscriptionScans(); - } - - async getScanSchedule(subscriptionId: string) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { tier: true }, - }); - - if (!subscription) return null; - - const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency; - - return { - subscriptionId, - tier: subscription.tier, - frequency, - cron: CRON_EXPRESSIONS[frequency], - nextRun: frequency === 'realtime' ? 'event-driven' : CRON_EXPRESSIONS[frequency], - }; - } -} - -export const schedulerService = new SchedulerService(); diff --git a/packages/api/src/services/darkwatch/watchlist.service.ts b/packages/api/src/services/darkwatch/watchlist.service.ts deleted file mode 100644 index 5fd8e89..0000000 --- a/packages/api/src/services/darkwatch/watchlist.service.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { prisma, WatchlistType } from '@shieldai/db'; -import { createHash } from 'crypto'; - -export function normalizeValue(type: WatchlistType, value: string): string { - const trimmed = value.trim().toLowerCase(); - switch (type) { - case WatchlistType.email: - return trimmed.replace(/\s+/g, ''); - case WatchlistType.phoneNumber: - return trimmed.replace(/[\s\-\(\)]/g, ''); - case WatchlistType.ssn: - return trimmed.replace(/-/g, ''); - case WatchlistType.address: - return trimmed; - case WatchlistType.domain: - return trimmed.replace(/^https?:\/\//, '').replace(/\/.*$/, ''); - default: - return trimmed; - } -} - -export function hashValue(value: string): string { - return createHash('sha256').update(value).digest('hex'); -} - -export class WatchlistService { - async addItem( - subscriptionId: string, - type: WatchlistType, - value: string, - maxItems: number - ) { - const normalized = normalizeValue(type, value); - const itemHash = hashValue(normalized); - - const currentCount = await prisma.watchlistItem.count({ - where: { subscriptionId, isActive: true }, - }); - - if (currentCount >= maxItems) { - throw new Error( - `Watchlist limit reached (${maxItems} items). Upgrade your plan to add more.` - ); - } - - const existing = await prisma.watchlistItem.findFirst({ - where: { subscriptionId, type, hash: itemHash }, - }); - - if (existing) { - if (!existing.isActive) { - return prisma.watchlistItem.update({ - where: { id: existing.id }, - data: { isActive: true }, - }); - } - return existing; - } - - return prisma.watchlistItem.create({ - data: { - subscriptionId, - type, - value: normalized, - hash: itemHash, - }, - }); - } - - async getItems(subscriptionId: string) { - return prisma.watchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - orderBy: { createdAt: 'desc' }, - }); - } - - async removeItem(id: string, subscriptionId: string) { - return prisma.watchlistItem.update({ - where: { id }, - data: { isActive: false }, - }); - } - - async getActiveItemsForScan(subscriptionId: string) { - return prisma.watchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - }); - } - - async getItemCount(subscriptionId: string) { - return prisma.watchlistItem.count({ - where: { subscriptionId, isActive: true }, - }); - } -} - -export const watchlistService = new WatchlistService(); diff --git a/packages/api/src/services/darkwatch/webhook.service.ts b/packages/api/src/services/darkwatch/webhook.service.ts deleted file mode 100644 index 419126c..0000000 --- a/packages/api/src/services/darkwatch/webhook.service.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { prisma, ExposureSource, ExposureSeverity, WatchlistType, AlertType, AlertSeverity } from '@shieldai/db'; -import { createHash } from 'crypto'; -import { mixpanelService, EventType } from '@shieldsai/shared-analytics'; - -function hashIdentifier(identifier: string): string { - return createHash('sha256').update(identifier.toLowerCase().trim()).digest('hex'); -} - -function determineSeverity( - source: ExposureSource, - dataType: WatchlistType -): ExposureSeverity { - const criticalSources = [ExposureSource.darkWebForum, ExposureSource.honeypot]; - const warningSources = [ExposureSource.hibp, ExposureSource.shodan]; - const criticalTypes = [WatchlistType.ssn]; - - if (criticalTypes.includes(dataType)) return ExposureSeverity.critical; - if (criticalSources.includes(source)) return ExposureSeverity.critical; - if (warningSources.includes(source)) return ExposureSeverity.warning; - return ExposureSeverity.info; -} - -export interface WebhookPayload { - source: string; - identifier: string; - identifierType: string; - metadata?: Record; - timestamp?: string; -} - -export class WebhookService { - async processExternalWebhook(payload: WebhookPayload): Promise<{ - exposuresCreated: number; - alertsCreated: number; - }> { - const source = this.mapSource(payload.source); - const dataType = this.mapDataType(payload.identifierType); - const identifier = payload.identifier.toLowerCase().trim(); - const identifierHash = hashIdentifier(identifier); - const severity = determineSeverity(source, dataType); - - const matchingItems = await prisma.watchlistItem.findMany({ - where: { - isActive: true, - OR: [ - { hash: identifierHash, type: dataType }, - { value: identifier, type: dataType }, - ], - }, - include: { - subscription: { - select: { - id: true, - tier: true, - userId: true, - }, - }, - }, - }); - - let exposuresCreated = 0; - let alertsCreated = 0; - - for (const item of matchingItems) { - const existing = await prisma.exposure.findFirst({ - where: { - subscriptionId: item.subscriptionId, - source, - identifierHash, - }, - }); - - if (existing) { - await prisma.exposure.update({ - where: { id: existing.id }, - data: { detectedAt: new Date() }, - }); - continue; - } - - const exposure = await prisma.exposure.create({ - data: { - subscriptionId: item.subscriptionId, - watchlistItemId: item.id, - source, - dataType, - identifier, - identifierHash, - severity, - isFirstTime: true, - metadata: payload.metadata || {}, - detectedAt: new Date(), - }, - }); - - exposuresCreated++; - - const alertChannels = this.getAlertChannelsForTier(item.subscription.tier); - - await prisma.alert.create({ - data: { - subscriptionId: item.subscriptionId, - userId: item.subscription.userId, - exposureId: exposure.id, - type: AlertType.exposure_detected, - title: `New Exposure Detected: ${this.getSourceLabel(source)}`, - message: this.buildAlertMessage(identifier, source, severity), - severity: this.mapAlertSeverity(severity), - channel: alertChannels, - }, - }); - - alertsCreated++; - - await mixpanelService.track(EventType.EXPOSURE_DETECTED, { - userId: item.subscription.userId, - exposureType: dataType, - severity, - source, - subscriptionTier: item.subscription.tier, - }); - } - - return { exposuresCreated, alertsCreated }; - } - - async verifyWebhookSignature( - body: string, - signature: string, - timestamp: string - ): Promise { - const webhookSecret = process.env.DARKWATCH_WEBHOOK_SECRET; - if (!webhookSecret) { - console.warn('[WebhookService] DARKWATCH_WEBHOOK_SECRET not set — signature verification skipped'); - return false; - } - - const expected = createHash('sha256') - .update(`${timestamp}:${body}`) - .digest('hex'); - - return expected === signature; - } - - private mapSource(source: string): ExposureSource { - const sourceMap: Record = { - hibp: ExposureSource.hibp, - 'haveibeenpwned': ExposureSource.hibp, - securitytrails: ExposureSource.securityTrails, - censys: ExposureSource.censys, - 'darkweb-forum': ExposureSource.darkWebForum, - 'darkweb': ExposureSource.darkWebForum, - shodan: ExposureSource.shodan, - honeypot: ExposureSource.honeypot, - }; - - const normalized = source.toLowerCase().replace(/\s+/g, ''); - const mapped = sourceMap[normalized]; - if (!mapped) { - console.warn(`[WebhookService] Unknown source "${source}", falling back to darkWebForum`); - } - return mapped || ExposureSource.darkWebForum; - } - - private mapDataType(type: string): WatchlistType { - const typeMap: Record = { - email: WatchlistType.email, - phone: WatchlistType.phoneNumber, - phonenumber: WatchlistType.phoneNumber, - ssn: WatchlistType.ssn, - address: WatchlistType.address, - domain: WatchlistType.domain, - }; - - const normalized = type.toLowerCase().trim(); - return typeMap[normalized] || WatchlistType.email; - } - - private getAlertChannelsForTier(tier: string): string[] { - const channelMap: Record = { - basic: ['email'], - plus: ['email', 'push'], - premium: ['email', 'push', 'sms'], - }; - return channelMap[tier] || ['email']; - } - - private mapAlertSeverity(severity: ExposureSeverity): AlertSeverity { - return severity as AlertSeverity; - } - - private getSourceLabel(source: ExposureSource): string { - const labels: Record = { - [ExposureSource.hibp]: 'Have I Been Pwned', - [ExposureSource.securityTrails]: 'SecurityTrails', - [ExposureSource.censys]: 'Censys', - [ExposureSource.darkWebForum]: 'Dark Web Forum', - [ExposureSource.shodan]: 'Shodan', - [ExposureSource.honeypot]: 'Honeypot', - }; - return labels[source] || source; - } - - private buildAlertMessage( - identifier: string, - source: ExposureSource, - severity: ExposureSeverity - ): string { - const masked = this.maskIdentifier(identifier); - return `${severity.toUpperCase()}: "${masked}" found in ${this.getSourceLabel(source)}.`; - } - - private maskIdentifier(identifier: string): string { - if (identifier.includes('@')) { - const [user, domain] = identifier.split('@'); - const maskedUser = user.slice(0, 2) + '***' + user.slice(-1); - return `${maskedUser}@${domain}`; - } - if (identifier.length > 8) { - return identifier.slice(0, 3) + '***' + identifier.slice(-2); - } - return identifier; - } -} - -export const webhookService = new WebhookService(); diff --git a/packages/api/src/services/spamshield/feature-flags.ts b/packages/api/src/services/spamshield/feature-flags.ts deleted file mode 100644 index 5c72e6a..0000000 --- a/packages/api/src/services/spamshield/feature-flags.ts +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Feature Flag Management System - * Centralized feature flag handling with type safety and runtime updates - */ - -import type { z } from 'zod'; - -/** - * Type for feature flag values - */ -export type FeatureFlagValue = boolean | string | number; - -/** - * Interface for a feature flag definition - */ -export interface FeatureFlag { - key: string; - defaultValue: T; - description?: string; - allowedValues?: T[]; // For enum-like flags - category?: string; -} - -/** - * Feature flag registry - stores all defined flags - */ -export interface FeatureFlagRegistry { - [key: string]: FeatureFlag; -} - -/** - * Feature flag resolver - handles flag resolution logic - */ -export class FeatureFlagResolver { - private flags: FeatureFlagRegistry; - private resolvedCache: Map = new Map(); - - constructor(flags: FeatureFlagRegistry) { - this.flags = flags; - } - - /** - * Resolve a feature flag value - * Priority: Environment > Cache > Default - */ - resolve(key: string, defaultValue: T): T { - // Check cache first - if (this.resolvedCache.has(key)) { - return this.resolvedCache.get(key)! as T; - } - - // Check environment variable (allows runtime updates) - const envValue = process.env[`FLAG_${key.toUpperCase()}`]; - if (envValue !== undefined) { - // Try to parse as JSON first, then as boolean, then as string - let parsed: FeatureFlagValue; - try { - parsed = JSON.parse(envValue); - } catch { - parsed = envValue.toLowerCase() === 'true' ? true : - envValue.toLowerCase() === 'false' ? false : - envValue; - } - - // Validate against allowed values if defined - const flag = this.flags[key]; - if (flag && flag.allowedValues && !flag.allowedValues.includes(parsed)) { - console.warn(`Invalid value for flag ${key}: ${parsed}. Using default.`); - parsed = defaultValue as FeatureFlagValue; - } - - this.resolvedCache.set(key, parsed); - return parsed as T; - } - - // Use cached value if available - if (this.resolvedCache.has(key)) { - return this.resolvedCache.get(key)! as T; - } - - // Return default - this.resolvedCache.set(key, defaultValue as FeatureFlagValue); - return defaultValue as T; - } - - /** - * Check if a flag is enabled (boolean check) - */ - isEnabled(key: string, defaultValue: T): T { - return this.resolve(key, defaultValue) as T; - } - - /** - * Get flag definition - */ - getDefinition(key: string): FeatureFlag | undefined { - return this.flags[key]; - } - - /** - * List all registered flags - */ - getAllFlags(): FeatureFlagRegistry { - return { ...this.flags }; - } - - /** - * Clear the resolution cache (useful for testing) - */ - clearCache(): void { - this.resolvedCache.clear(); - } -} - -/** - * Feature flag configuration with pre-defined flags - */ -export const featureFlags: FeatureFlagRegistry = { - // SpamShield Feature Flags - 'spamshield.enable.number.reputation': { - key: 'spamshield_enable_number_reputation', - defaultValue: true, - description: 'Enable number reputation checking (Hiya API integration)', - category: 'spamshield', - }, - 'spamshield.enable.content.classification': { - key: 'spamshield_enable_content_classification', - defaultValue: true, - description: 'Enable SMS content classification (BERT model)', - category: 'spamshield', - }, - 'spamshield.enable.behavioral.analysis': { - key: 'spamshield_enable_behavioral_analysis', - defaultValue: true, - description: 'Enable call behavioral analysis', - category: 'spamshield', - }, - 'spamshield.enable.community.intelligence': { - key: 'spamshield_enable_community_intelligence', - defaultValue: true, - description: 'Enable community intelligence sharing', - category: 'spamshield', - }, - 'spamshield.enable.real.time.blocking': { - key: 'spamshield_enable_real_time_blocking', - defaultValue: true, - description: 'Enable real-time spam blocking', - category: 'spamshield', - }, - 'spamshield.enable.multiple.sources': { - key: 'spamshield_enable_multiple_sources', - defaultValue: false, - description: 'Enable multiple reputation source aggregation (Truecaller, etc.)', - category: 'spamshield', - }, - 'spamshield.enable.ml.classifier': { - key: 'spamshield_enable_ml_classifier', - defaultValue: false, - description: 'Enable ML-based spam classification', - category: 'spamshield', - }, - - // VoicePrint Feature Flags - 'voiceprint.enable.ml.service': { - key: 'voiceprint_enable_ml_service', - defaultValue: false, - description: 'Enable ML service integration for voice analysis', - category: 'voiceprint', - }, - 'voiceprint.enable.faiss.index': { - key: 'voiceprint_enable_faiss_index', - defaultValue: true, - description: 'Enable FAISS index for voice matching', - category: 'voiceprint', - }, - 'voiceprint.enable.batch.analysis': { - key: 'voiceprint_enable_batch_analysis', - defaultValue: true, - description: 'Enable batch voice analysis', - category: 'voiceprint', - }, - 'voiceprint.enable.realtime.analysis': { - key: 'voiceprint_enable_realtime_analysis', - defaultValue: false, - description: 'Enable real-time voice analysis', - category: 'voiceprint', - }, - 'voiceprint.enable.mock.model': { - key: 'voiceprint_enable_mock_model', - defaultValue: true, - description: 'Enable mock model for development', - category: 'voiceprint', - }, - - // General Platform Flags - 'platform.enable.audit.logs': { - key: 'platform_enable_audit_logs', - defaultValue: true, - description: 'Enable comprehensive audit logging', - category: 'platform', - }, - 'platform.enable.kpi.tracking': { - key: 'platform_enable_kpi_tracking', - defaultValue: true, - description: 'Enable KPI snapshot tracking', - category: 'platform', - }, -}; - -/** - * Create a resolver instance with the default flags - */ -export const featureFlagResolver = new FeatureFlagResolver(featureFlags); - -/** - * Convenience function for quick flag checks - */ -export function isFeatureEnabled(key: string, defaultValue: T): T { - return featureFlagResolver.isEnabled(key, defaultValue); -} - -/** - * Check if a flag is enabled with type safety - */ -export function checkFlag(key: string, defaultValue: T): T { - return featureFlagResolver.resolve(key, defaultValue); -} diff --git a/packages/api/src/services/spamshield/index.ts b/packages/api/src/services/spamshield/index.ts deleted file mode 100644 index 917e237..0000000 --- a/packages/api/src/services/spamshield/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -// Config -export { - spamShieldEnv, - SpamLayer, - SpamDecision, - ConfidenceLevel, - spamFeatureFlags, - spamRateLimits, - checkFlag, - isFeatureEnabled, -} from './spamshield.config'; - -// Feature flags -export * from './feature-flags'; - -// Services -export { - NumberReputationService, - SMSClassifierService, - CallAnalysisService, - SpamFeedbackService, - numberReputationService, - smsClassifierService, - callAnalysisService, - spamFeedbackService, -} from './spamshield.service'; diff --git a/packages/api/src/services/spamshield/spamshield.audit-logger.ts b/packages/api/src/services/spamshield/spamshield.audit-logger.ts deleted file mode 100644 index dd62ee4..0000000 --- a/packages/api/src/services/spamshield/spamshield.audit-logger.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { createHash } from 'crypto'; - -export type AuditClassificationType = 'sms' | 'call'; - -export interface AuditClassificationEntry { - id: string; - timestamp: string; - type: AuditClassificationType; - phoneNumberHash: string; - decision: 'spam' | 'ham' | 'block' | 'flag' | 'allow'; - confidence: number; - reasons: string[]; - featureFlags: Record; - metadata?: Record; -} - -const MAX_AUDIT_LOG_SIZE = 10_000; - -class AuditLogger { - private entries: AuditClassificationEntry[] = []; - - logClassification(entry: Omit): AuditClassificationEntry { - const record: AuditClassificationEntry = { - id: `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - timestamp: new Date().toISOString(), - ...entry, - }; - - this.entries.push(record); - - if (this.entries.length > MAX_AUDIT_LOG_SIZE) { - this.entries.shift(); - } - - console.log( - `[SpamShield:Audit] type=${record.type} decision=${record.decision} ` + - `confidence=${record.confidence.toFixed(3)} reasons=${record.reasons.join(',') || 'none'} ` + - `phoneHash=${record.phoneNumberHash}` - ); - - return record; - } - - getEntries( - filters?: { - type?: AuditClassificationType; - decision?: string; - startDate?: Date; - endDate?: Date; - limit?: number; - } - ): AuditClassificationEntry[] { - let results = this.entries; - - if (filters?.type) { - results = results.filter(e => e.type === filters.type); - } - - if (filters?.decision) { - results = results.filter(e => e.decision === filters.decision); - } - - if (filters?.startDate) { - results = results.filter(e => new Date(e.timestamp) >= filters.startDate!); - } - - if (filters?.endDate) { - results = results.filter(e => new Date(e.timestamp) <= filters.endDate!); - } - - if (filters?.limit) { - results = results.slice(-filters.limit); - } - - return results; - } - - getSummary(): { - totalEntries: number; - spamCount: number; - hamCount: number; - blockCount: number; - flagCount: number; - allowCount: number; - avgConfidence: number; - } { - const spamCount = this.entries.filter(e => e.decision === 'spam' || e.decision === 'block').length; - const hamCount = this.entries.filter(e => e.decision === 'ham' || e.decision === 'allow').length; - const blockCount = this.entries.filter(e => e.decision === 'block').length; - const flagCount = this.entries.filter(e => e.decision === 'flag').length; - const allowCount = this.entries.filter(e => e.decision === 'allow').length; - const avgConfidence = - this.entries.length > 0 - ? this.entries.reduce((s, e) => s + e.confidence, 0) / this.entries.length - : 0; - - return { - totalEntries: this.entries.length, - spamCount, - hamCount, - blockCount, - flagCount, - allowCount, - avgConfidence: Math.round(avgConfidence * 1000) / 1000, - }; - } - - clear(): void { - this.entries = []; - } -} - -export const spamAuditLogger = new AuditLogger(); - -export function hashPhoneNumber(phoneNumber: string): string { - const hash = createHash('sha256').update(phoneNumber.trim()).digest('hex'); - return `sha256_${hash}`; -} diff --git a/packages/api/src/services/spamshield/spamshield.config.ts b/packages/api/src/services/spamshield/spamshield.config.ts deleted file mode 100644 index af56605..0000000 --- a/packages/api/src/services/spamshield/spamshield.config.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { z } from 'zod'; -import { checkFlag } from './feature-flags'; - -// Environment variables for SpamShield -const envSchema = z.object({ - HIYA_API_KEY: z.string(), - HIYA_API_URL: z.string().default('https://api.hiya.com/v1'), - TRUECALLER_API_KEY: z.string().optional(), - BERT_MODEL_PATH: z.string().default('./models/spam-classifier'), - SPAM_THRESHOLD_AUTO_BLOCK: z.string().transform(Number).default(0.85), - SPAM_THRESHOLD_FLAG: z.string().transform(Number).default(0.6), - CALL_ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default(200), -}); - -export const spamShieldEnv = envSchema.parse({ - HIYA_API_KEY: process.env.HIYA_API_KEY, - HIYA_API_URL: process.env.HIYA_API_URL, - TRUECALLER_API_KEY: process.env.TRUECALLER_API_KEY, - BERT_MODEL_PATH: process.env.BERT_MODEL_PATH, - SPAM_THRESHOLD_AUTO_BLOCK: process.env.SPAM_THRESHOLD_AUTO_BLOCK, - SPAM_THRESHOLD_FLAG: process.env.SPAM_THRESHOLD_FLAG, - CALL_ANALYSIS_TIMEOUT_MS: process.env.CALL_ANALYSIS_TIMEOUT_MS, -}); - -// Spam detection layers -export enum SpamLayer { - NUMBER_REPUTATION = 'number_reputation', - CONTENT_CLASSIFICATION = 'content_classification', - BEHAVIORAL_ANALYSIS = 'behavioral_analysis', - COMMUNITY_INTELLIGENCE = 'community_intelligence', -} - -// Spam decision types -export enum SpamDecision { - ALLOW = 'allow', - FLAG = 'flag', - BLOCK = 'block', - CHALLENGE = 'challenge', -} - -// Confidence levels -export enum ConfidenceLevel { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - VERY_HIGH = 'very_high', -} - -// Feature flags for spam detection -// Use the centralized feature flag system from feature-flags.ts -// These are aliases for quick access -export const spamFeatureFlags = { - enableNumberReputation: checkFlag('spamshield.enable.number.reputation', true), - enableContentClassification: checkFlag('spamshield.enable.content.classification', true), - enableBehavioralAnalysis: checkFlag('spamshield.enable.behavioral.analysis', true), - enableCommunityIntelligence: checkFlag('spamshield.enable.community.intelligence', true), - enableRealTimeBlocking: checkFlag('spamshield.enable.real.time.blocking', true), - enableMultipleSources: checkFlag('spamshield.enable.multiple.sources', false), - enableMLClassifier: checkFlag('spamshield.enable.ml.classifier', false), -}; - -// Rate limits for spam analysis -export const spamRateLimits = { - basic: { - analysesPerMinute: 10, - analysesPerDay: 100, - }, - plus: { - analysesPerMinute: 50, - analysesPerDay: 1000, - }, - premium: { - analysesPerMinute: 200, - analysesPerDay: 10000, - }, -}; - -// Default confidence scores for spam detection layers -export const defaultScores = { - // Number reputation service defaults - defaultReputationConfidence: 0.0, - defaultReputationLowConfidence: 0.1, - - // SMS classifier defaults - defaultBaseConfidence: 0.5, - defaultMaxConfidence: 1.0, - - // Feature weights for SMS classification - featureWeights: { - urlPresent: 0.1, - highEmojiDensity: 0.15, - urgencyKeyword: 0.2, - excessiveCaps: 0.15, - }, - - // Call analysis defaults - defaultSpamScore: 0.0, - highReputationThreshold: 0.7, - reputationWeightInCombinedScore: 0.4, - shortDurationScore: 0.2, - voipScore: 0.15, - unusualHoursScore: 0.1, - - // Source combination weights - hiyaWeightInCombinedScore: 0.7, - truecallerWeightInCombinedScore: 0.3, -}; - -// Metadata size limits for SpamFeedback -export const metadataLimits = { - // Maximum size for metadata JSON in bytes - maxMetadataSizeBytes: 4096, - - // Maximum number of keys in metadata object - maxMetadataKeys: 20, - - // Maximum size for individual metadata value in bytes - maxMetadataValueSizeBytes: 512, -}; - -// Standard error codes for spamshield API -export enum SpamErrorCode { - // Client errors (4xx) - INVALID_REQUEST = 'INVALID_REQUEST', - MISSING_REQUIRED_FIELD = 'MISSING_REQUIRED_FIELD', - UNAUTHORIZED = 'UNAUTHORIZED', - NOT_FOUND = 'NOT_FOUND', - VALIDATION_ERROR = 'VALIDATION_ERROR', - - // Server errors (5xx) - CLASSIFICATION_FAILED = 'CLASSIFICATION_FAILED', - REPUTATION_CHECK_FAILED = 'REPUTATION_CHECK_FAILED', - ANALYSIS_FAILED = 'ANALYSIS_FAILED', - FEEDBACK_RECORD_FAILED = 'FEEDBACK_RECORD_FAILED', - DATABASE_ERROR = 'DATABASE_ERROR', - RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED', - SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE', -} - -// Standard error response type -export interface SpamErrorResponse { - error: { - code: SpamErrorCode; - message: string; - field?: string; - timestamp: string; - requestId?: string; - }; -} - -// HTTP status code constants -export const HttpStatus = { - OK: 200, - CREATED: 201, - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - UNPROCESSABLE_ENTITY: 422, - TOO_MANY_REQUESTS: 429, - INTERNAL_SERVER_ERROR: 500, - SERVICE_UNAVAILABLE: 503, -}; diff --git a/packages/api/src/services/spamshield/spamshield.error-handler.ts b/packages/api/src/services/spamshield/spamshield.error-handler.ts deleted file mode 100644 index 41b382d..0000000 --- a/packages/api/src/services/spamshield/spamshield.error-handler.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { FastifyReply } from 'fastify'; -import { SpamErrorCode, HttpStatus, SpamErrorResponse } from './spamshield.config'; - -export { SpamErrorCode, HttpStatus }; -export type { SpamErrorResponse }; - -/** - * Standardized error response builder for SpamShield API - */ -export class ErrorHandler { - /** - * Create a standard error response - */ - static create( - code: SpamErrorCode, - message: string, - options?: { - field?: string; - requestId?: string; - additionalData?: Record; - } - ): SpamErrorResponse { - return { - error: { - code, - message, - ...(options?.field && { field: options.field }), - timestamp: new Date().toISOString(), - ...(options?.requestId && { requestId: options.requestId }), - }, - }; - } - - /** - * Send a standard error response with appropriate HTTP status code - */ - static send( - reply: FastifyReply, - code: SpamErrorCode, - message: string, - options?: { - field?: string; - status?: number; - requestId?: string; - } - ): void { - const status = options?.status ?? this.getStatusForCode(code); - const errorResponse = this.create(code, message, { - field: options?.field, - requestId: options?.requestId, - }); - reply.code(status).send(errorResponse); - } - - /** - * Map error codes to HTTP status codes - */ - private static getStatusForCode(code: SpamErrorCode): number { - const statusMap: Record = { - // Client errors - [SpamErrorCode.INVALID_REQUEST]: HttpStatus.BAD_REQUEST, - [SpamErrorCode.MISSING_REQUIRED_FIELD]: HttpStatus.BAD_REQUEST, - [SpamErrorCode.UNAUTHORIZED]: HttpStatus.UNAUTHORIZED, - [SpamErrorCode.NOT_FOUND]: HttpStatus.NOT_FOUND, - [SpamErrorCode.VALIDATION_ERROR]: HttpStatus.BAD_REQUEST, - - // Server errors - [SpamErrorCode.CLASSIFICATION_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.REPUTATION_CHECK_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.ANALYSIS_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.FEEDBACK_RECORD_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.DATABASE_ERROR]: HttpStatus.INTERNAL_SERVER_ERROR, - [SpamErrorCode.RATE_LIMIT_EXCEEDED]: HttpStatus.TOO_MANY_REQUESTS, - [SpamErrorCode.SERVICE_UNAVAILABLE]: HttpStatus.SERVICE_UNAVAILABLE, - }; - return statusMap[code] ?? HttpStatus.INTERNAL_SERVER_ERROR; - } - - /** - * Validate required string field - */ - static validateRequiredField( - value: unknown, - fieldName: string - ): { isValid: boolean; error?: { code: SpamErrorCode; message: string; field: string } } { - if (!value || typeof value !== 'string' || value.trim() === '') { - return { - isValid: false, - error: { - code: SpamErrorCode.MISSING_REQUIRED_FIELD, - message: `${fieldName} is required`, - field: fieldName, - }, - }; - } - return { isValid: true }; - } - - /** - * Validate boolean field - */ - static validateBooleanField( - value: unknown, - fieldName: string - ): { isValid: boolean; error?: { code: SpamErrorCode; message: string; field: string } } { - if (value === undefined || value === null || typeof value !== 'boolean') { - return { - isValid: false, - error: { - code: SpamErrorCode.VALIDATION_ERROR, - message: `${fieldName} must be a boolean`, - field: fieldName, - }, - }; - } - return { isValid: true }; - } -} diff --git a/packages/api/src/services/spamshield/spamshield.service.ts b/packages/api/src/services/spamshield/spamshield.service.ts deleted file mode 100644 index ce59e67..0000000 --- a/packages/api/src/services/spamshield/spamshield.service.ts +++ /dev/null @@ -1,481 +0,0 @@ -import { prisma, SpamFeedback } from '@shieldai/db'; -import { spamShieldEnv, SpamDecision, spamFeatureFlags, defaultScores, metadataLimits } from './spamshield.config'; -import { createHash } from 'crypto'; -import { spamAuditLogger, hashPhoneNumber } from './spamshield.audit-logger'; - -// Number reputation service (Hiya API integration) -export class NumberReputationService { - /** - * Check number reputation using Hiya API - */ - async checkReputation(phoneNumber: string): Promise<{ - isSpam: boolean; - confidence: number; - spamType?: string; - reportCount: number; - }> { - try { - // Only enable if feature flag is set - if (!spamFeatureFlags.enableNumberReputation) { - return { - isSpam: false, - confidence: 0.0, - reportCount: 0, - }; - } - - // TODO: Integrate with Hiya API - // const response = await fetch(`${spamShieldEnv.HIYA_API_URL}/lookup`, { - // headers: { 'X-API-Key': spamShieldEnv.HIYA_API_KEY }, - // method: 'POST', - // body: JSON.stringify({ phone: phoneNumber }), - // }); - - // Simulated response for now - return { - isSpam: false, - confidence: defaultScores.defaultReputationLowConfidence, - spamType: undefined, - reportCount: 0, - }; - } catch (error) { - console.error('Error checking number reputation:', error); - return { - isSpam: false, - confidence: defaultScores.defaultReputationConfidence, - reportCount: 0, - }; - } - } - - /** - * Check number against multiple reputation sources - */ - async checkMultiSource(phoneNumber: string): Promise<{ - hiya: { isSpam: boolean; confidence: number }; - truecaller: { isSpam: boolean; confidence: number } | null; - combinedScore: number; - }> { - // Only enable if feature flag is set - if (!spamFeatureFlags.enableMultipleSources) { - return { - hiya: { isSpam: false, confidence: defaultScores.defaultReputationConfidence }, - truecaller: null, - combinedScore: defaultScores.defaultSpamScore, - }; - } - - const hiyaResult = await this.checkReputation(phoneNumber); - - let truecallerResult: { isSpam: boolean; confidence: number } | null = null; - if (spamShieldEnv.TRUECALLER_API_KEY) { - // TODO: Integrate Truecaller - truecallerResult = { - isSpam: false, - confidence: defaultScores.defaultReputationConfidence, - }; - } - - // Weighted average: Hiya 70%, Truecaller 30% - const combinedScore = hiyaResult.confidence * defaultScores.hiyaWeightInCombinedScore + - (truecallerResult?.confidence ?? defaultScores.defaultReputationConfidence) * defaultScores.truecallerWeightInCombinedScore; - - return { - hiya: { isSpam: hiyaResult.isSpam, confidence: hiyaResult.confidence }, - truecaller: truecallerResult, - combinedScore, - }; - } -} - -// SMS content classifier (BERT-based) -export class SMSClassifierService { - private model: any = null; // BERT model placeholder - private _initPromise: Promise | null = null; - - /** - * Initialize the BERT model (thread-safe via promise deduplication) - */ - async initialize(): Promise { - // TODO: Load BERT model from path - // this.model = await loadBERTModel(spamShieldEnv.BERT_MODEL_PATH); - console.log('SMS classifier initialized'); - } - - /** - * Ensures model is initialized before use. Concurrent callers - * await the same initialization promise to avoid race conditions. - */ - private async ensureInitialized(): Promise { - if (this._initPromise) { - return this._initPromise; - } - this._initPromise = (async () => { - if (this.model) { - return; - } - await this.initialize(); - })(); - return this._initPromise; - } - - /** - * Classify SMS text as spam or ham - */ - async classify( - smsText: string, - phoneNumber?: string - ): Promise<{ - isSpam: boolean; - confidence: number; - spamFeatures: string[]; - }> { - // Only enable if feature flag is set - if (!spamFeatureFlags.enableMLClassifier) { - // Return basic feature-based classification - const features = this.extractFeatures(smsText); - const confidence = this.calculateConfidence(features); - const isSpam = confidence >= spamShieldEnv.SPAM_THRESHOLD_AUTO_BLOCK; - - spamAuditLogger.logClassification({ - type: 'sms', - phoneNumberHash: phoneNumber ? hashPhoneNumber(phoneNumber) : 'unknown', - decision: isSpam ? 'spam' : 'ham', - confidence, - reasons: features, - featureFlags: { enableMLClassifier: spamFeatureFlags.enableMLClassifier }, - }); - - return { - isSpam, - confidence, - spamFeatures: features, - }; - } - - await this.ensureInitialized(); - - // Extract features - const features = this.extractFeatures(smsText); - - // TODO: Run through BERT model - // const prediction = await this.model.predict(smsText); - - // Simulated prediction - const confidence = this.calculateConfidence(features); - const isSpam = confidence >= spamShieldEnv.SPAM_THRESHOLD_AUTO_BLOCK; - - spamAuditLogger.logClassification({ - type: 'sms', - phoneNumberHash: phoneNumber ? hashPhoneNumber(phoneNumber) : 'unknown', - decision: isSpam ? 'spam' : 'ham', - confidence, - reasons: features, - featureFlags: { enableMLClassifier: spamFeatureFlags.enableMLClassifier }, - }); - - return { - isSpam, - confidence, - spamFeatures: features, - }; - } - - private extractFeatures(text: string): string[] { - const features: string[] = []; - const lowerText = text.toLowerCase(); - - // URL presence - if (/(http|www)\./i.test(text)) { - features.push('url_present'); - } - - // Emoji density - const emojiCount = (text.match(/[\p{Emoji}]/gu) || []).length; - if (emojiCount / text.length > 0.1) { - features.push('high_emoji_density'); - } - - // Urgency keywords - const urgencyWords = ['now', 'urgent', 'limited', 'act fast', 'today']; - if (urgencyWords.some(word => lowerText.includes(word))) { - features.push('urgency_keyword'); - } - - // Excessive capitalization - if (/[A-Z]{3,}/.test(text)) { - features.push('excessive_caps'); - } - - return features; - } - - private calculateConfidence(features: string[]): number { - const baseConfidence = defaultScores.defaultBaseConfidence; - const featureWeights: Record = { - url_present: defaultScores.featureWeights.urlPresent, - high_emoji_density: defaultScores.featureWeights.highEmojiDensity, - urgency_keyword: defaultScores.featureWeights.urgencyKeyword, - excessive_caps: defaultScores.featureWeights.excessiveCaps, - }; - - return Math.min(defaultScores.defaultMaxConfidence, baseConfidence + - features.reduce((sum, f) => sum + (featureWeights[f] || 0), 0)); - } -} - -// Call analysis service -export class CallAnalysisService { - /** - * Analyze incoming call for spam indicators - */ - async analyzeCall(callData: { - phoneNumber: string; - duration?: number; - callTime: Date; - isVoip?: boolean; - }): Promise<{ - decision: SpamDecision; - confidence: number; - reasons: string[]; - }> { - const reasons: string[] = []; - let spamScore = defaultScores.defaultSpamScore; - - // Number reputation check - only if feature flag enabled - if (spamFeatureFlags.enableBehavioralAnalysis) { - const reputationService = new NumberReputationService(); - const reputation = await reputationService.checkMultiSource(callData.phoneNumber); - - if (reputation.combinedScore > defaultScores.highReputationThreshold) { - spamScore += reputation.combinedScore * defaultScores.reputationWeightInCombinedScore; - reasons.push('high_spam_reputation'); - } - } - - // Behavioral analysis - only if feature flag enabled - if (spamFeatureFlags.enableBehavioralAnalysis) { - if (callData.duration && callData.duration < 10) { - spamScore += defaultScores.shortDurationScore; - reasons.push('short_duration'); - } - - if (callData.isVoip) { - spamScore += defaultScores.voipScore; - reasons.push('voip_number'); - } - - // Time-of-day anomaly (simplified) - const hour = callData.callTime.getHours(); - if (hour < 6 || hour > 22) { - spamScore += defaultScores.unusualHoursScore; - reasons.push('unusual_hours'); - } - } - - // Determine decision - let decision: SpamDecision; - if (spamScore >= spamShieldEnv.SPAM_THRESHOLD_AUTO_BLOCK) { - decision = SpamDecision.BLOCK; - } else if (spamScore >= spamShieldEnv.SPAM_THRESHOLD_FLAG) { - decision = SpamDecision.FLAG; - } else { - decision = SpamDecision.ALLOW; - } - - spamAuditLogger.logClassification({ - type: 'call', - phoneNumberHash: hashPhoneNumber(callData.phoneNumber), - decision: decision.toLowerCase() as 'block' | 'flag' | 'allow', - confidence: spamScore, - reasons, - featureFlags: { - enableBehavioralAnalysis: spamFeatureFlags.enableBehavioralAnalysis, - enableNumberReputation: spamFeatureFlags.enableNumberReputation, - }, - metadata: { - duration: callData.duration, - isVoip: callData.isVoip, - callTime: callData.callTime.toISOString(), - }, - }); - - return { - decision, - confidence: spamScore, - reasons, - }; - } -} - -// User feedback service -export class SpamFeedbackService { - /** - * Validate metadata size against defined limits - */ - private validateMetadata(metadata?: Record): { - isValid: boolean; - trimmedMetadata?: Record; - reasons?: string[]; - } { - if (!metadata) { - return { isValid: true }; - } - - const reasons: string[] = []; - let trimmedMetadata: Record = metadata; - - // Check number of keys - const keyCount = Object.keys(metadata).length; - if (keyCount > metadataLimits.maxMetadataKeys) { - reasons.push(`Metadata has ${keyCount} keys, exceeding limit of ${metadataLimits.maxMetadataKeys}`); - trimmedMetadata = Object.entries(metadata).slice(0, metadataLimits.maxMetadataKeys); - } - - // Check total JSON size - const jsonSize = JSON.stringify(metadata).length; - if (jsonSize > metadataLimits.maxMetadataSizeBytes) { - reasons.push(`Metadata size ${jsonSize} bytes exceeds limit of ${metadataLimits.maxMetadataSizeBytes} bytes`); - - // Truncate long values - trimmedMetadata = Object.fromEntries( - Object.entries(metadata).map(([key, value]) => { - const valueStr = String(value); - if (valueStr.length > metadataLimits.maxMetadataValueSizeBytes) { - return [key, valueStr.slice(0, metadataLimits.maxMetadataValueSizeBytes)]; - } - return [key, value]; - }) - ); - } - - return { - isValid: reasons.length === 0, - trimmedMetadata, - reasons: reasons.length > 0 ? reasons : undefined, - }; - } - - /** - * Record user feedback on spam detection - */ - async recordFeedback( - userId: string, - phoneNumber: string, - isSpam: boolean, - confidence?: number, - metadata?: Record - ): Promise { - // Defensive null checks for required fields - if (!userId || typeof userId !== 'string' || userId.trim().length === 0) { - throw new Error('Feedback: userId is required'); - } - - if (!phoneNumber || typeof phoneNumber !== 'string' || phoneNumber.trim().length === 0) { - throw new Error('Feedback: phoneNumber is required'); - } - - if (typeof isSpam !== 'boolean') { - throw new Error('Feedback: isSpam must be a boolean'); - } - - // Validate confidence range if provided - const validatedConfidence = confidence !== undefined && confidence !== null - ? (Number.isFinite(confidence) && confidence >= 0 && confidence <= 1 ? confidence : undefined) - : undefined; - - // Treat null metadata as undefined - const effectiveMetadata = metadata !== null ? metadata : undefined; - const validation = this.validateMetadata(effectiveMetadata); - const validatedMetadata = validation.trimmedMetadata; - - // Only enable if feature flag is set - if (!spamFeatureFlags.enableCommunityIntelligence) { - // Return a mock feedback for development - return { - id: `mock_${Date.now()}`, - userId, - phoneNumber, - phoneNumberHash: this.hashPhoneNumber(phoneNumber), - isSpam, - confidence: validatedConfidence, - feedbackType: 'user_confirmation' as const, - metadata: validatedMetadata, - createdAt: new Date(), - updatedAt: new Date(), - }; - } - - const phoneNumberHash = this.hashPhoneNumber(phoneNumber); - - const feedback = await prisma.spamFeedback.create({ - data: { - userId, - phoneNumber, - phoneNumberHash, - isSpam, - confidence: validatedConfidence, - feedbackType: 'user_confirmation', - metadata: validatedMetadata, - }, - }); - - return feedback; - } - - /** - * Get spam history for a user - */ - async getSpamHistory( - userId: string, - options?: { - limit?: number; - isSpam?: boolean; - startDate?: Date; - } - ): Promise { - return prisma.spamFeedback.findMany({ - where: { - userId, - ...(options?.isSpam !== undefined && { isSpam: options.isSpam }), - ...(options?.startDate && { createdAt: { gte: options.startDate } }), - }, - orderBy: { createdAt: 'desc' }, - take: options?.limit ?? 100, - }); - } - - /** - * Get statistics for a user - */ - async getStatistics(userId: string): Promise<{ - totalAnalyses: number; - spamCount: number; - hamCount: number; - spamPercentage: number; - }> { - const [total, spam] = await Promise.all([ - prisma.spamFeedback.count({ where: { userId } }), - prisma.spamFeedback.count({ where: { userId, isSpam: true } }), - ]); - - return { - totalAnalyses: total, - spamCount: spam, - hamCount: total - spam, - spamPercentage: total > 0 ? (spam / total) * 100 : 0, - }; - } - - private hashPhoneNumber(phoneNumber: string): string { - // SHA-256 hash for phone number fingerprinting - const hash = createHash('sha256').update(phoneNumber).digest('hex'); - return `sha256_${hash}`; - } -} - -// Export instances -export const numberReputationService = new NumberReputationService(); -export const smsClassifierService = new SMSClassifierService(); -export const callAnalysisService = new CallAnalysisService(); -export const spamFeedbackService = new SpamFeedbackService(); diff --git a/packages/api/src/services/voiceprint/embedding.service.ts b/packages/api/src/services/voiceprint/embedding.service.ts deleted file mode 100644 index 13f0de4..0000000 --- a/packages/api/src/services/voiceprint/embedding.service.ts +++ /dev/null @@ -1,192 +0,0 @@ -import { spawn } from "child_process"; -import { logger } from './logger'; -import { voicePrintEnv } from './voiceprint.config'; - -const EMBEDDING_DIM = 192; -const MODEL_VERSION = "ecapa-tdnn-0.1.0-mock"; - -export class EmbeddingService { - private mlServiceUrl: string; - private initialized = false; - - constructor() { - this.mlServiceUrl = process.env.VOICEPRINT_ML_URL || "http://localhost:8001"; - } - - async initialize(): Promise { - if (this.initialized) return; - this.initialized = true; - logger.info('Embedding service initialized', { mlUrl: this.mlServiceUrl, modelVersion: MODEL_VERSION }); - } - - async extract(audioBuffer: Buffer): Promise { - await this.initialize(); - - const mlAvailable = await this.checkMLService(); - if (mlAvailable) { - logger.info('Using ML service for embedding', { mlUrl: this.mlServiceUrl }); - return this.extractViaML(audioBuffer); - } - - logger.info('Using mock embedding generation', { audioBufferLength: audioBuffer.length }); - return this.generateMockFromBuffer(audioBuffer); - } - - async analyze(audioBuffer: Buffer): Promise<{ - confidence: number; - detectionType: string; - features: Record; - embedding: number[]; - }> { - const embedding = await this.extract(audioBuffer); - const confidence = this.estimateSyntheticConfidence(audioBuffer, embedding); - const detectionType = confidence >= voicePrintEnv.SYNTHETIC_THRESHOLD ? 'synthetic_voice' : 'natural'; - const features = this.extractAnalysisFeatures(audioBuffer, embedding); - - return { confidence, detectionType, features, embedding }; - } - - getModelVersion(): string { - return MODEL_VERSION; - } - - private async extractViaML(audioBuffer: Buffer): Promise { - return new Promise((resolve, reject) => { - const jsonInput = audioBuffer.toString("base64"); - const proc = spawn("python3", [ - "-c", - ` -import urllib.request, json, sys -req = urllib.request.Request( - "${this.mlServiceUrl}/embedding", - data=json.dumps({"audio": "${jsonInput.substring(0, 5000)}"}).encode(), - headers={"Content-Type": "application/json"} -) -try: - with urllib.request.urlopen(req, timeout=60) as resp: - data = json.loads(resp.read()) - sys.stdout.write(json.dumps({"ok": True, "vector": data.get("embedding", []), "dim": data.get("dimension", ${EMBEDDING_DIM})})) -except Exception as e: - sys.stdout.write(json.dumps({"ok": False, "error": str(e)})) -`, - ]); - - let output = ""; - proc.stdout.on("data", (chunk) => { output += chunk.toString(); }); - proc.on("close", (code) => { - try { - const result = JSON.parse(output); - if (result.ok && result.vector && result.vector.length === EMBEDDING_DIM) { - resolve(result.vector); - } else { - resolve(this.generateMockFromBuffer(audioBuffer)); - } - } catch { - resolve(this.generateMockFromBuffer(audioBuffer)); - } - }); - }); - } - - private generateMockFromBuffer(audioBuffer: Buffer): number[] { - let hash = 0; - const sampleSize = Math.min(audioBuffer.length, 1024); - for (let i = 0; i < sampleSize; i += 4) { - hash = ((hash << 5) - hash + audioBuffer.readInt32LE(i)) | 0; - } - const seed = Math.abs(hash); - - const rng = this.createRNG(seed); - const vector: number[] = []; - - // Box-Muller transform for Gaussian distribution - for (let i = 0; i < EMBEDDING_DIM; i += 2) { - const u1 = rng(); - const u2 = rng(); - const mag = Math.sqrt(-2 * Math.log(u1)); - const z0 = mag * Math.cos(2 * Math.PI * u2); - const z1 = mag * Math.sin(2 * Math.PI * u2); - vector.push(parseFloat(z0.toFixed(6))); - if (i + 1 < EMBEDDING_DIM) { - vector.push(parseFloat(z1.toFixed(6))); - } - } - - // L2 normalize - const norm = Math.sqrt(vector.reduce((s, v) => s + v * v, 0)); - return vector.map((v) => parseFloat((v / norm).toFixed(6))); - } - - private estimateSyntheticConfidence(buffer: Buffer, embedding: number[]): number { - const meanAmplitude = buffer.reduce((s, v) => s + v, 0) / buffer.length / 255; - const meanEmbedding = embedding.reduce((s, v) => s + v, 0) / embedding.length; - const embeddingStdDev = Math.sqrt(embedding.reduce((s, v) => s + (v - meanEmbedding) ** 2, 0) / embedding.length); - - const amplitudeScore = Math.abs(meanAmplitude - 0.5) * 2; - const embeddingScore = 1.0 - Math.min(1.0, embeddingStdDev * 2); - const varianceScore = Math.min(1.0, buffer.length / 10000); - - return Math.min(1.0, amplitudeScore * 0.3 + embeddingScore * 0.4 + varianceScore * 0.3); - } - - private extractAnalysisFeatures(buffer: Buffer, embedding: number[]): Record { - const meanAmplitude = buffer.reduce((s, v) => s + v, 0) / buffer.length / 255; - const zeroCrossings = buffer.reduce((count, v, i, arr) => { - return i > 0 && ((v - 128) * (arr[i - 1] - 128) < 0) ? count + 1 : count; - }, 0); - - return { - mean_amplitude: meanAmplitude, - zero_crossing_rate: zeroCrossings / buffer.length, - embedding_energy: embedding.reduce((s, v) => s + v * v, 0), - embedding_entropy: this.calculateEntropy(embedding), - }; - } - - private calculateEntropy(values: number[]): number { - const bins = 20; - const histogram = new Array(bins).fill(0); - const min = Math.min(...values); - const max = Math.max(...values); - const range = max - min || 1; - - for (const v of values) { - const bin = Math.min(bins - 1, Math.floor(((v - min) / range) * bins)); - histogram[bin]++; - } - - let entropy = 0; - const total = values.length; - for (const count of histogram) { - if (count > 0) { - const p = count / total; - entropy -= p * Math.log2(p); - } - } - return entropy; - } - - private async checkMLService(): Promise { - return new Promise((resolve) => { - const proc = spawn("python3", [ - "-c", - ` -import urllib.request, sys -try: - urllib.request.urlopen("${this.mlServiceUrl}/health", timeout=2) - sys.exit(0) -except: - sys.exit(1) -`, - ]); - proc.on("close", (code) => resolve(code === 0)); - }); - } - - private createRNG(seed: number): () => number { - return () => { - seed = (seed * 1664525 + 1013904223) & 0xffffffff; - return (seed >>> 0) / 0xffffffff; - }; - } -} diff --git a/packages/api/src/services/voiceprint/faiss.index.ts b/packages/api/src/services/voiceprint/faiss.index.ts deleted file mode 100644 index 40d85fd..0000000 --- a/packages/api/src/services/voiceprint/faiss.index.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { logger } from './logger'; -import { voicePrintEnv } from './voiceprint.config'; - -export class FAISSIndex { - private store: Map = new Map(); - private readonly indexPath: string; - private initialized = false; - - constructor(path?: string) { - this.indexPath = path ?? voicePrintEnv.FAISS_INDEX_PATH; - } - - async initialize(): Promise { - if (this.initialized) return; - await this.loadFromDatabase(); - this.initialized = true; - logger.info('FAISS index initialized', { indexPath: this.indexPath, enrollmentCount: this.store.size }); - } - - async add(enrollmentId: string, embedding: number[]): Promise { - await this.initialize(); - const normalized = [...embedding]; - this.normalizeInPlace(normalized); - this.store.set(enrollmentId, normalized); - logger.info('Added enrollment to FAISS index', { enrollmentId }); - } - - async remove(enrollmentId: string): Promise { - await this.initialize(); - this.store.delete(enrollmentId); - logger.info('Removed enrollment from FAISS index', { enrollmentId }); - } - - async search(embedding: number[], topK: number = 5): Promise> { - await this.initialize(); - - const normalized = [...embedding]; - this.normalizeInPlace(normalized); - - const scores: Array<{ id: string; similarity: number }> = []; - - for (const [id, vector] of this.store.entries()) { - const similarity = this.cosineSimilarity(normalized, vector); - scores.push({ id, similarity }); - } - - scores.sort((a, b) => b.similarity - a.similarity); - return scores.slice(0, topK); - } - - async save(): Promise { - await this.initialize(); - logger.info('FAISS index saved', { indexPath: this.indexPath, count: this.store.size }); - } - - private async loadFromDatabase(): Promise { - try { - const { prisma } = await import('@shieldai/db'); - const enrollments = await prisma.voiceEnrollment.findMany({ - select: { id: true, voiceHash: true }, - }); - // Note: voiceHash is stored, not the actual embedding vector - // In production, we'd store the full embedding vector - logger.info('Loaded enrollments from database', { count: enrollments.length }); - } catch (error) { - logger.warn('Failed to load enrollments from database', { error: error instanceof Error ? error.message : String(error) }); - } - } - - private cosineSimilarity(a: number[], b: number[]): number { - let dotProduct = 0; - let normA = 0; - let normB = 0; - - for (let i = 0; i < a.length; i++) { - dotProduct += a[i] * b[i]; - normA += a[i] * a[i]; - normB += b[i] * b[i]; - } - - const denominator = Math.sqrt(normA) * Math.sqrt(normB); - return denominator > 0 ? dotProduct / denominator : 0; - } - - private normalizeInPlace(vector: number[]): void { - const norm = Math.sqrt(vector.reduce((s, v) => s + v * v, 0)); - if (norm > 0) { - for (let i = 0; i < vector.length; i++) { - vector[i] /= norm; - } - } - } -} diff --git a/packages/api/src/services/voiceprint/index.ts b/packages/api/src/services/voiceprint/index.ts deleted file mode 100644 index 970d2fe..0000000 --- a/packages/api/src/services/voiceprint/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Config -export { - voicePrintEnv, - VoicePrintSource, - AnalysisJobStatus, - DetectionType, - ConfidenceLevel, - audioPreprocessingConfig, - voicePrintFeatureFlags, - voicePrintRateLimits, -} from './voiceprint.config'; - -// Feature flags -export { checkFlag, isFeatureEnabled } from './voiceprint.feature-flags'; - - - -// Services -export { - AudioPreprocessor, - VoiceEnrollmentService, - AnalysisService, - BatchAnalysisService, - EmbeddingService, - FAISSIndex, - audioPreprocessor, - voiceEnrollmentService, - analysisService, - batchAnalysisService, - embeddingService, -} from './voiceprint.service'; diff --git a/packages/api/src/services/voiceprint/logger.ts b/packages/api/src/services/voiceprint/logger.ts deleted file mode 100644 index 34c4118..0000000 --- a/packages/api/src/services/voiceprint/logger.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { FastifyLoggerOptions } from 'fastify'; - -export interface Logger { - info(message: string, context?: Record): void; - warn(message: string, context?: Record): void; - error(message: string, context?: Record): void; - debug(message: string, context?: Record): void; -} - -export class ConsoleLogger implements Logger { - info(message: string, context?: Record): void { - const timestamp = new Date().toISOString(); - const logContext = context ? ` ${JSON.stringify(context)}` : ''; - console.log(`[${timestamp}] [INFO] ${message}${logContext}`); - } - - warn(message: string, context?: Record): void { - const timestamp = new Date().toISOString(); - const logContext = context ? ` ${JSON.stringify(context)}` : ''; - console.warn(`[${timestamp}] [WARN] ${message}${logContext}`); - } - - error(message: string, context?: Record): void { - const timestamp = new Date().toISOString(); - const logContext = context ? ` ${JSON.stringify(context)}` : ''; - console.error(`[${timestamp}] [ERROR] ${message}${logContext}`); - } - - debug(message: string, context?: Record): void { - const timestamp = new Date().toISOString(); - const logContext = context ? ` ${JSON.stringify(context)}` : ''; - console.debug(`[${timestamp}] [DEBUG] ${message}${logContext}`); - } -} - -export const logger = new ConsoleLogger(); diff --git a/packages/api/src/services/voiceprint/voiceprint.config.ts b/packages/api/src/services/voiceprint/voiceprint.config.ts deleted file mode 100644 index f3bad81..0000000 --- a/packages/api/src/services/voiceprint/voiceprint.config.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { z } from 'zod'; -import { existsSync } from 'fs'; -import { checkFlag } from './voiceprint.feature-flags'; - -// P3-4 fix: Use strict() to catch typos in env var names -// P3-1 fix: Use safeParse() to avoid module-level crash on missing env vars -const envSchema = z.object({ - ECAPA_TDNN_MODEL_PATH: z.string().default('./models/ecapa-tdnn'), - ML_SERVICE_URL: z.string().default('http://localhost:8001'), - FAISS_INDEX_PATH: z.string().default('./data/voiceprint_faiss.index'), - AUDIO_STORAGE_BUCKET: z.string().default('voiceprint-audio'), - AUDIO_STORAGE_ENDPOINT: z.string().default('http://localhost:9000'), - SYNTHETIC_THRESHOLD: z.string().transform(Number).default('0.75'), - ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default('3'), - ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default('60'), - EMBEDDING_DIMENSIONS: z.string().transform(Number).default('192'), - BATCH_MAX_FILES: z.string().transform(Number).default('20'), - ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default('30000'), -}).strict(); - -const envInput = { - ECAPA_TDNN_MODEL_PATH: process.env.ECAPA_TDNN_MODEL_PATH, - ML_SERVICE_URL: process.env.ML_SERVICE_URL, - FAISS_INDEX_PATH: process.env.FAISS_INDEX_PATH, - AUDIO_STORAGE_BUCKET: process.env.AUDIO_STORAGE_BUCKET, - AUDIO_STORAGE_ENDPOINT: process.env.AUDIO_STORAGE_ENDPOINT, - SYNTHETIC_THRESHOLD: process.env.SYNTHETIC_THRESHOLD, - ENROLLMENT_MIN_DURATION_SEC: process.env.ENROLLMENT_MIN_DURATION_SEC, - ENROLLMENT_MAX_DURATION_SEC: process.env.ENROLLMENT_MAX_DURATION_SEC, - EMBEDDING_DIMENSIONS: process.env.EMBEDDING_DIMENSIONS, - BATCH_MAX_FILES: process.env.BATCH_MAX_FILES, - ANALYSIS_TIMEOUT_MS: process.env.ANALYSIS_TIMEOUT_MS, -}; - -const parsed = envSchema.safeParse(envInput); -export const voicePrintEnv = parsed.success - ? parsed.data - : envSchema.parse({}); // fallback to all defaults - -// P3-3 fix: Validate model path exists at startup (warn, not crash) -if (voicePrintEnv.ECAPA_TDNN_MODEL_PATH && !existsSync(voicePrintEnv.ECAPA_TDNN_MODEL_PATH)) { - console.warn( - `[VoicePrint] Model path not found: ${voicePrintEnv.ECAPA_TDNN_MODEL_PATH} (using mock model)` - ); -} - -if (!parsed.success) { - console.warn('[VoicePrint] Env validation warnings:', parsed.error.issues.map((i: z.ZodIssue) => `${i.path.join('.')}: ${i.message}`).join(', ')); -} - -// Audio source types -export enum VoicePrintSource { - UPLOAD = 'upload', - S3 = 's3', - URL = 'url', - REALTIME = 'realtime', -} - -// Analysis job status -export enum AnalysisJobStatus { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} - -// Detection result types -export enum DetectionType { - SYNTHETIC_VOICE = 'synthetic_voice', - VOICE_CLONE = 'voice_clone', - DEEPFAKE = 'deepfake', - NATURAL = 'natural', -} - -// Confidence levels -export enum ConfidenceLevel { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - VERY_HIGH = 'very_high', -} - -// Audio preprocessing configuration -export const audioPreprocessingConfig = { - sampleRate: 16000, - channels: 1, - bitDepth: 16, - vadThreshold: 0.5, - noiseReduction: true, - maxSilenceDurationMs: 500, -}; - -// Feature flags - use centralized system -export const voicePrintFeatureFlags = { - enableMLService: checkFlag('voiceprint.enable.ml.service', false), - enableFAISSIndex: checkFlag('voiceprint.enable.faiss.index', true), - enableBatchAnalysis: checkFlag('voiceprint.enable.batch.analysis', true), - enableRealtimeAnalysis: checkFlag('voiceprint.enable.realtime.analysis', false), - enableMockModel: checkFlag('voiceprint.enable.mock.model', true), -}; - -// Rate limits for voice analysis -export const voicePrintRateLimits = { - basic: { - analysesPerMinute: 5, - enrollmentsPerDay: 10, - maxAudioFileSizeMB: 50, - }, - plus: { - analysesPerMinute: 30, - enrollmentsPerDay: 50, - maxAudioFileSizeMB: 200, - }, - premium: { - analysesPerMinute: 100, - enrollmentsPerDay: 500, - maxAudioFileSizeMB: 500, - }, -}; diff --git a/packages/api/src/services/voiceprint/voiceprint.feature-flags.ts b/packages/api/src/services/voiceprint/voiceprint.feature-flags.ts deleted file mode 100644 index 5f62818..0000000 --- a/packages/api/src/services/voiceprint/voiceprint.feature-flags.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * VoicePrint Feature Flags - * Re-exports the checkFlag function from the centralized feature flag system - */ - -// Re-export the checkFlag and isFeatureEnabled functions from the spamshield feature flags module -export { checkFlag, isFeatureEnabled } from '../spamshield/feature-flags'; diff --git a/packages/api/src/services/voiceprint/voiceprint.service.ts b/packages/api/src/services/voiceprint/voiceprint.service.ts deleted file mode 100644 index 183ea30..0000000 --- a/packages/api/src/services/voiceprint/voiceprint.service.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { createHash } from 'crypto'; -import { prisma, VoiceEnrollment, VoiceAnalysis } from '@shieldai/db'; -import { - voicePrintEnv, - AnalysisJobStatus, - DetectionType, - audioPreprocessingConfig, - voicePrintFeatureFlags, -} from './voiceprint.config'; -import { checkFlag } from './voiceprint.feature-flags'; -import { logger } from './logger'; -import { EmbeddingService as ModularEmbeddingService } from './embedding.service'; -import { FAISSIndex as ModularFAISSIndex } from './faiss.index'; - -// Alias for backwards compatibility -const EmbeddingService = ModularEmbeddingService; -const FAISSIndex = ModularFAISSIndex; - -// Audio preprocessing service -export class AudioPreprocessor { - /** - * Normalize audio to 16kHz mono with VAD and noise reduction. - * Returns preprocessing metadata and the processed audio buffer. - */ - async preprocess( - audioBuffer: Buffer, - options?: { - sourceSampleRate?: number; - channels?: number; - } - ): Promise<{ - buffer: Buffer; - metadata: { - sampleRate: number; - channels: number; - duration: number; - format: string; - }; - }> { - const duration = this.estimateDuration(audioBuffer, options?.sourceSampleRate ?? 44100); - - if (duration < voicePrintEnv.ENROLLMENT_MIN_DURATION_SEC) { - throw new Error( - `Audio too short: ${duration.toFixed(1)}s < ${voicePrintEnv.ENROLLMENT_MIN_DURATION_SEC}s minimum` - ); - } - - if (duration > voicePrintEnv.ENROLLMENT_MAX_DURATION_SEC) { - throw new Error( - `Audio too long: ${duration.toFixed(1)}s > ${voicePrintEnv.ENROLLMENT_MAX_DURATION_SEC}s maximum` - ); - } - - // TODO: Integrate with Python librosa/torchaudio for actual preprocessing - // For MVP, return original buffer with target metadata - return { - buffer: audioBuffer, - metadata: { - sampleRate: audioPreprocessingConfig.sampleRate, - channels: audioPreprocessingConfig.channels, - duration, - format: 'wav', - }, - }; - } - - /** - * Apply Voice Activity Detection to remove silence segments. - */ - async applyVAD(buffer: Buffer): Promise { - // TODO: Integrate with Python webrtcvad or silero-vad - // For MVP, return original buffer - return buffer; - } - - /** - * Estimate audio duration from buffer size and sample rate. - */ - private estimateDuration( - buffer: Buffer, - sampleRate: number - ): number { - const bytesPerSample = 2; - const channels = 1; - const samples = buffer.length / (bytesPerSample * channels); - return samples / sampleRate; - } -} - -// Voice enrollment service -export class VoiceEnrollmentService { - /** - * Enroll a new voice profile from audio data. - */ - async enroll( - userId: string, - name: string, - audioBuffer: Buffer - ): Promise { - const preprocessor = new AudioPreprocessor(); - const processed = await preprocessor.preprocess(audioBuffer); - - const embeddingService = new EmbeddingService(); - const embedding = await embeddingService.extract(processed.buffer); - const voiceHash = this.computeEmbeddingHash(embedding); - - const enrollment = await prisma.voiceEnrollment.create({ - data: { - userId, - name, - voiceHash, - audioMetadata: { - ...processed.metadata, - embeddingDimensions: embedding.length, - enrollmentTimestamp: new Date().toISOString(), - }, - }, - }); - - // Index in FAISS for similarity search - const faissIndex = new FAISSIndex(); - await faissIndex.add(enrollment.id, embedding); - - return enrollment; - } - - /** - * List all enrollments for a user. - */ - async listEnrollments( - userId: string, - options?: { - isActive?: boolean; - limit?: number; - offset?: number; - } - ): Promise { - return prisma.voiceEnrollment.findMany({ - where: { - userId, - ...(options?.isActive !== undefined && { isActive: options.isActive }), - }, - orderBy: { createdAt: 'desc' }, - take: options?.limit ?? 50, - skip: options?.offset ?? 0, - }); - } - - /** - * Get a single enrollment by ID. - */ - async getEnrollment( - enrollmentId: string, - userId: string - ): Promise { - return prisma.voiceEnrollment.findFirst({ - where: { - id: enrollmentId, - userId, - }, - }); - } - - /** - * Remove (deactivate) an enrollment. - */ - async removeEnrollment( - enrollmentId: string, - userId: string - ): Promise { - const enrollment = await this.getEnrollment(enrollmentId, userId); - if (!enrollment) { - throw new Error('Enrollment not found'); - } - - const faissIndex = new FAISSIndex(); - await faissIndex.remove(enrollmentId); - - return prisma.voiceEnrollment.update({ - where: { id: enrollmentId }, - data: { isActive: false }, - }); - } - - /** - * Search for similar enrollments using FAISS. - */ - async findSimilar( - embedding: number[], - topK: number = 5 - ): Promise> { - const faissIndex = new FAISSIndex(); - const results = await faissIndex.search(embedding, topK); - - const enrollmentIds = results.map((r) => r.id); - const enrollments = await prisma.voiceEnrollment.findMany({ - where: { id: { in: enrollmentIds } }, - }); - const enrollmentMap = new Map(enrollments.map((e) => [e.id, e])); - - return results - .map((r) => ({ - enrollment: enrollmentMap.get(r.id), - similarity: r.similarity, - })) - .filter((r): r is { enrollment: VoiceEnrollment; similarity: number } => r.enrollment !== undefined); - } - - private computeEmbeddingHash(embedding: number[]): string { - const content = embedding.map((v) => v.toFixed(6)).join(','); - return `vp_${createHash('sha256').update(content).digest('hex').slice(0, 16)}_${embedding.length}`; - } -} - -// Audio analysis service -export class AnalysisService { - /** - * Analyze a single audio file for synthetic voice detection. - */ - async analyze( - userId: string, - audioBuffer: Buffer, - options?: { - enrollmentId?: string; - audioUrl?: string; - } - ): Promise { - const preprocessor = new AudioPreprocessor(); - const processed = await preprocessor.preprocess(audioBuffer); - - const audioHash = this.computeAudioHash(audioBuffer); - - const embeddingService = new EmbeddingService(); - const analysisResult = await embeddingService.analyze(processed.buffer); - - const isSynthetic = analysisResult.confidence >= voicePrintEnv.SYNTHETIC_THRESHOLD; - - const voiceAnalysis = await prisma.voiceAnalysis.create({ - data: { - userId, - enrollmentId: options?.enrollmentId, - audioHash, - isSynthetic, - confidence: analysisResult.confidence, - analysisResult: { - ...analysisResult, - processedMetadata: processed.metadata, - analysisTimestamp: new Date().toISOString(), - modelVersion: 'ecapa-tdnn-v1-mock', - }, - audioUrl: options?.audioUrl ?? '', - }, - }); - - return voiceAnalysis; - } - - /** - * Get analysis result by ID. - */ - async getResult( - analysisId: string, - userId: string - ): Promise { - return prisma.voiceAnalysis.findFirst({ - where: { - id: analysisId, - userId, - }, - }); - } - - /** - * Get analysis history for a user. - */ - async getHistory( - userId: string, - options?: { - limit?: number; - offset?: number; - isSynthetic?: boolean; - } - ): Promise { - return prisma.voiceAnalysis.findMany({ - where: { - userId, - ...(options?.isSynthetic !== undefined && { isSynthetic: options.isSynthetic }), - }, - orderBy: { createdAt: 'desc' }, - take: options?.limit ?? 50, - skip: options?.offset ?? 0, - }); - } - - private computeAudioHash(buffer: Buffer): string { - return `audio_${createHash('sha256').update(buffer).digest('hex').slice(0, 16)}`; - } -} - -// Batch analysis service -export class BatchAnalysisService { - private readonly maxConcurrency = 5; - - /** - * Analyze multiple audio files in a batch with parallel processing. - * Uses Promise.allSettled with concurrency control for better performance. - */ - async analyzeBatch( - userId: string, - files: Array<{ - name: string; - buffer: Buffer; - audioUrl?: string; - }>, - options?: { - enrollmentId?: string; - } - ): Promise<{ - jobId: string; - results: VoiceAnalysis[]; - summary: { - total: number; - synthetic: number; - natural: number; - failed: number; - }; - }> { - if (files.length > voicePrintEnv.BATCH_MAX_FILES) { - throw new Error( - `Batch too large: ${files.length} > ${voicePrintEnv.BATCH_MAX_FILES} max` - ); - } - - const jobId = `batch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; - logger.info('Starting batch analysis', { - jobId, - userId, - totalFiles: files.length, - enrollmentId: options?.enrollmentId - }); - - const analysisService = new AnalysisService(); - const results: VoiceAnalysis[] = []; - const errors: Array<{ name: string; error: string }> = []; - let synthetic = 0; - let natural = 0; - - // Process with concurrency control using chunked Promise.allSettled - const processChunk = async (chunk: typeof files) => { - const promises = chunk.map(async (file) => { - try { - const result = await analysisService.analyze(userId, file.buffer, { - enrollmentId: options?.enrollmentId, - audioUrl: file.audioUrl, - }); - return { success: true as const, result, name: file.name }; - } catch (error) { - const message = error instanceof Error ? error.message : 'Analysis failed'; - return { success: false as const, error: message, name: file.name }; - } - }); - - const outcomes = await Promise.allSettled(promises); - - for (const outcome of outcomes) { - if (outcome.status === 'fulfilled') { - if (outcome.value.success && outcome.value.result) { - results.push(outcome.value.result); - if (outcome.value.result.isSynthetic) { - synthetic++; - } else { - natural++; - } - } else if (!outcome.value.success) { - errors.push({ name: outcome.value.name, error: outcome.value.error }); - } - } - } - }; - - // Process files in chunks for concurrency control - for (let i = 0; i < files.length; i += this.maxConcurrency) { - const chunk = files.slice(i, i + this.maxConcurrency); - await processChunk(chunk); - } - - const failed = errors.length; - - // TODO: P3-2 - Persist batch jobId to database once schema is fixed - // Schema errors need to be resolved first (AnalysisJob relation issues) - logger.info('Batch analysis completed', { - jobId, - successfulResults: results.length, - failedCount: failed, - synthetic, - natural - }); - - return { - jobId, - results, - summary: { - total: files.length, - synthetic, - natural, - failed, - }, - }; - } -} - -// Re-export improved modular implementations -export { EmbeddingService } from './embedding.service'; -export { FAISSIndex } from './faiss.index'; - -// Export singleton instances for backwards compatibility -export const audioPreprocessor = new AudioPreprocessor(); -export const voiceEnrollmentService = new VoiceEnrollmentService(); -export const analysisService = new AnalysisService(); -export const batchAnalysisService = new BatchAnalysisService(); -export const embeddingService = new EmbeddingService(); diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json deleted file mode 100644 index e459899..0000000 --- a/packages/api/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/api/vitest.config.ts b/packages/api/vitest.config.ts deleted file mode 100644 index ce3f5c7..0000000 --- a/packages/api/vitest.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['src/**/*.test.ts', 'src/**/__tests__/**/*.test.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - reportsDirectory: './coverage', - include: ['src/**/*.ts'], - exclude: [ - 'src/**/*.d.ts', - 'src/**/__tests__/**/*.test.ts', - '**/node_modules/**', - ], - thresholds: { - statements: 80, - branches: 80, - functions: 80, - lines: 80, - }, - }, - }, -}); diff --git a/packages/core/src/audio/audio-stream-capture.ts b/packages/core/src/audio/audio-stream-capture.ts deleted file mode 100644 index 5d74b33..0000000 --- a/packages/core/src/audio/audio-stream-capture.ts +++ /dev/null @@ -1,434 +0,0 @@ -/** - * Audio Stream Capture Module - * - * Captures and processes audio frames from WebRTC streams for - * real-time call analysis. Provides echo cancellation, noise - * suppression, and auto-gain control. - * - * Security hardening (FRE-4497): - * - Proper destroy() lifecycle with awaited stop() before cleanup - * - Bounded internal buffers - * - AudioWorklet preferred over deprecated ScriptProcessorNode - * - Graceful error handling with bounded retry - */ - -// ── Types ──────────────────────────────────────────────────────────────────── - -export interface AudioStreamConfig { - sampleRate: number; - chunkSize: number; - echoCancellation: boolean; - noiseSuppression: boolean; - autoGainControl: boolean; - maxBufferLength: number; -} - -export interface AudioFrame { - timestamp: number; - data: Float32Array; - duration: number; -} - -export interface StreamMetrics { - framesCaptured: number; - totalDuration: number; - averageLevel: number; - peakLevel: number; - silenceRatio: number; - clipCount: number; -} - -export type StreamStatus = 'idle' | 'capturing' | 'paused' | 'stopped' | 'error'; - -// ── Constants ──────────────────────────────────────────────────────────────── - -const DEFAULT_CONFIG: AudioStreamConfig = { - sampleRate: 16000, - chunkSize: 1024, - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - maxBufferLength: 100, -}; - -// ── Audio Stream Capture ───────────────────────────────────────────────────── - -export class AudioStreamCapture { - private config: AudioStreamConfig; - private audioContext: AudioContext | null = null; - private stream: MediaStream | null = null; - private sourceNode: MediaStreamAudioSourceNode | null = null; - private analyserNode: AnalyserNode | null = null; - private scriptProcessor: ScriptProcessorNode | null = null; - private workletNode: AudioWorkletNode | null = null; - private status: StreamStatus = 'idle'; - private captureTimer?: number; - private frameBuffer: AudioFrame[] = []; - private metrics: StreamMetrics = { - framesCaptured: 0, - totalDuration: 0, - averageLevel: 0, - peakLevel: 0, - silenceRatio: 0, - clipCount: 0, - }; - private silenceFrames: number = 0; - - // Callbacks - public onFrame?: (frame: AudioFrame) => void; - public onSilence?: (duration: number) => void; - public onClip?: (peakLevel: number) => void; - public onError?: (error: Error) => void; - public onStatusChange?: (status: StreamStatus) => void; - - constructor(config: Partial = {}) { - this.config = { ...DEFAULT_CONFIG, ...config }; - } - - /** - * Start capturing audio from a MediaStream - */ - async start(stream?: MediaStream): Promise { - if (this.status === 'capturing') return; - - try { - // Use provided stream or create one from microphone - this.stream = stream || await navigator.mediaDevices.getUserMedia({ - audio: { - echoCancellation: this.config.echoCancellation, - noiseSuppression: this.config.noiseSuppression, - autoGainControl: this.config.autoGainControl, - sampleRate: this.config.sampleRate, - }, - }); - - this.audioContext = new AudioContext({ - sampleRate: this.config.sampleRate, - }); - - this.sourceNode = this.audioContext.createMediaStreamSource(this.stream); - this.analyserNode = this.audioContext.createAnalyser(); - this.analyserNode.fftSize = this.config.chunkSize * 2; - - this.sourceNode.connect(this.analyserNode); - - // Try AudioWorklet first, fall back to ScriptProcessorNode - if (await this.setupWorklet()) { - this.sourceNode.connect(this.workletNode!); - } else { - this.setupScriptProcessor(); - } - - this.status = 'capturing'; - this.onStatusChange?.(this.status); - - // Start periodic capture loop - this.startCaptureLoop(); - - // Handle stream end - this.stream.getAudioTracks()[0]?.addEventListener('ended', () => { - this.stop(); - }); - - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - this.status = 'error'; - this.onStatusChange?.(this.status); - this.onError?.(err); - throw err; - } - } - - /** - * Try to set up AudioWorklet (modern approach) - */ - private async setupWorklet(): Promise { - if (!this.audioContext) return false; - try { - // Inline worklet processor - const workletCode = ` - class AudioProcessor extends AudioWorkletProcessor { - process(inputs, outputs) { - const input = inputs[0]; - if (input && input[0]) { - const data = Array.from(input[0]); - this.port.postMessage({ type: 'audio', data }); - } - return true; - } - } - registerProcessor('audio-processor', AudioProcessor); - `; - const blob = new Blob([workletCode], { type: 'application/javascript' }); - const url = URL.createObjectURL(blob); - await this.audioContext.audioWorklet.addModule(url); - URL.revokeObjectURL(url); - - this.workletNode = new AudioWorkletNode(this.audioContext, 'audio-processor'); - this.workletNode.port.onmessage = (e: MessageEvent) => { - if (e.data.type === 'audio') { - this.processFrame(new Float32Array(e.data.data)); - } - }; - return true; - } catch { - return false; - } - } - - /** - * Fall back to ScriptProcessorNode (legacy, widely supported) - */ - private setupScriptProcessor(): void { - if (!this.audioContext || !this.analyserNode) return; - - this.scriptProcessor = this.audioContext.createScriptProcessor( - this.config.chunkSize, - 1, - 1 - ); - - this.scriptProcessor.onaudioprocess = (event) => { - const inputData = event.inputBuffer?.getChannelData(0); - if (inputData) { - this.processFrame(new Float32Array(inputData)); - } - }; - - this.analyserNode.connect(this.scriptProcessor); - this.scriptProcessor.connect(this.audioContext.destination); - } - - /** - * Process a single audio frame - */ - private processFrame(data: Float32Array): void { - const timestamp = this.audioContext?.currentTime ?? Date.now(); - const duration = data.length / this.config.sampleRate; - - const frame: AudioFrame = { - timestamp, - data, - duration, - }; - - // Bounded frame buffer - this.frameBuffer.push(frame); - if (this.frameBuffer.length > this.config.maxBufferLength) { - this.frameBuffer.shift(); - } - - // Update metrics - const level = this.computeRMS(data); - this.metrics.framesCaptured++; - this.metrics.totalDuration += duration; - this.metrics.averageLevel = (this.metrics.averageLevel * (this.metrics.framesCaptured - 1) + level) / this.metrics.framesCaptured; - this.metrics.peakLevel = Math.max(this.metrics.peakLevel, level); - - // Silence detection - if (level < 0.01) { - this.silenceFrames++; - if (this.silenceFrames > 10) { - this.onSilence?.(this.silenceFrames * duration); - } - } else { - this.silenceFrames = 0; - } - - // Clip detection - const hasClip = Array.from(data).some(s => Math.abs(s) > 0.98); - if (hasClip) { - this.metrics.clipCount++; - this.onClip?.(level); - } - - // Emit frame - this.onFrame?.(frame); - } - - /** - * Start periodic capture loop for analyser data - */ - private startCaptureLoop(): void { - const capture = () => { - if (this.status !== 'capturing' || !this.analyserNode) return; - - const bufferLength = this.analyserNode.fftSize; - const dataArray = new Float32Array(bufferLength); - this.analyserNode.getFloatTimeDomainData(dataArray); - - // Update silence ratio metric - const silenceSamples = Array.from(dataArray).filter(s => Math.abs(s) < 0.01).length; - this.metrics.silenceRatio = - (this.metrics.silenceRatio * (this.metrics.framesCaptured - 1) + silenceSamples / bufferLength) / - this.metrics.framesCaptured; - - this.captureTimer = window.setTimeout(capture, 50); - }; - capture(); - } - - /** - * Pause capture (keeps stream alive) - */ - pause(): void { - if (this.status !== 'capturing') return; - this.status = 'paused'; - this.onStatusChange?.(this.status); - if (this.captureTimer) { - window.clearTimeout(this.captureTimer); - } - this.audioContext?.suspend(); - } - - /** - * Resume capture - */ - async resume(): Promise { - if (this.status !== 'paused') return; - await this.audioContext?.resume(); - this.status = 'capturing'; - this.onStatusChange?.(this.status); - this.startCaptureLoop(); - } - - /** - * Stop and clean up all resources - * - * Fixed race condition (FRE-4497): - * - Awaits stop of all tracks before removing listeners - * - Disconnects nodes before closing context - * - Clears timers before final cleanup - */ - async destroy(): Promise { - // Stop capture loop - if (this.captureTimer) { - window.clearTimeout(this.captureTimer); - this.captureTimer = undefined; - } - - // Stop all stream tracks and wait - if (this.stream) { - const tracks = this.stream.getTracks(); - await Promise.all(tracks.map(track => new Promise(resolve => { - track.onended = resolve; - track.stop(); - }))); - } - - // Disconnect audio graph nodes - if (this.scriptProcessor) { - this.scriptProcessor.disconnect(); - this.scriptProcessor = null; - } - - if (this.workletNode) { - this.workletNode.disconnect(); - this.workletNode.port.onmessage = null; - this.workletNode = null; - } - - if (this.sourceNode) { - this.sourceNode.disconnect(); - this.sourceNode = null; - } - - if (this.analyserNode) { - this.analyserNode.disconnect(); - this.analyserNode = null; - } - - // Close audio context (awaited) - if (this.audioContext) { - await this.audioContext.close(); - this.audioContext = null; - } - - // Clear buffer - this.frameBuffer = []; - - this.status = 'stopped'; - - // Clear callbacks to prevent stale references (emit status before clearing) - const statusCb = this.onStatusChange; - this.onFrame = undefined; - this.onSilence = undefined; - this.onClip = undefined; - this.onError = undefined; - this.onStatusChange = undefined; - statusCb?.(this.status); - } - - /** - * Stop capture (synchronous, for quick stop) - */ - stop(): void { - if (this.captureTimer) { - window.clearTimeout(this.captureTimer); - this.captureTimer = undefined; - } - if (this.stream) { - this.stream.getTracks().forEach(track => track.stop()); - } - this.status = 'stopped'; - this.onStatusChange?.(this.status); - } - - /** - * Compute RMS level of audio data - */ - private computeRMS(data: Float32Array): number { - let sum = 0; - for (let i = 0; i < data.length; i++) { - sum += data[i] * data[i]; - } - return Math.sqrt(sum / data.length); - } - - /** - * Get current stream status - */ - getStatus(): StreamStatus { - return this.status; - } - - /** - * Get current metrics - */ - getMetrics(): StreamMetrics { - return { ...this.metrics }; - } - - /** - * Get recent frames (bounded) - */ - getRecentFrames(count = 10): AudioFrame[] { - return this.frameBuffer.slice(-count); - } - - /** - * Get stream metadata - */ - getMetadata(): { - isActive: boolean; - sampleRate: number; - channels: number; - } { - if (!this.stream) { - return { isActive: false, sampleRate: 0, channels: 0 }; - } - const audioTrack = this.stream.getAudioTracks()[0]; - return { - isActive: this.status === 'capturing', - sampleRate: this.config.sampleRate, - channels: audioTrack?.getSettings().channelCount || 1, - }; - } -} - -/** - * Factory function for creating audio stream capture - */ -export function createAudioStreamCapture(config?: Partial): AudioStreamCapture { - return new AudioStreamCapture(config); -} diff --git a/packages/core/src/audio/webrtc/audio-pipeline.ts b/packages/core/src/audio/webrtc/audio-pipeline.ts deleted file mode 100644 index 662b423..0000000 --- a/packages/core/src/audio/webrtc/audio-pipeline.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * Audio Processing Pipeline for Real-Time Analysis - * Coordinates WebRTC stream capture with VoicePreprocess for continuous analysis - */ - -import { WebRTCStreamCapture, createWebRTCCapture } from './stream-capture'; - -// Type definitions for real-time processing -export interface AudioChunk { - id: string; - timestamp: number; - data: Float32Array; - duration: number; -} - -export interface VoiceprintResult { - chunkId: string; - timestamp: number; - features: AudioFeatures; - embedding: number[]; - confidence: number; - status: 'complete' | 'error'; -} - -// Audio chunk configuration -export interface AudioChunkConfig { - chunkDuration: number; - overlapDuration: number; - sampleRate: number; -} - -// Preprocessor interfaces (copied from AudioPreprocessor for standalone usage) -export interface PreprocessedAudio { - audio: Buffer; - sampleRate: number; - channels: number; - durationSec: number; -} - -export interface AudioFeatures { - mfccs: number[][]; - zeroCrossingRate: number; - spectralCentroid: number; - spectralRollOff: number; - durationSec: number; -} - -interface PipelineConfig { - chunkDuration: number; // 5000ms - overlapDuration: number; // 1000ms - sampleRate: number; // 16000 Hz - onAnalysisComplete?: (result: VoiceprintResult) => void; - onStreamError?: (error: Error) => void; - onError?: (error: Error) => void; -} - -const DEFAULT_CONFIG: PipelineConfig = { - chunkDuration: 5000, - overlapDuration: 1000, - sampleRate: 16000 -}; - -export class AudioPipeline { - private streamCapture: WebRTCStreamCapture | null = null; - private isRunning: boolean = false; - private chunkBuffer: AudioChunk[] = []; - private maxBufferLength: number = 10; - - private onChunkReady?: (chunk: AudioChunk) => void; - private onAnalysisComplete?: (result: VoiceprintResult) => void; - private onPipelineError?: (error: Error) => void; - - constructor(private config: PipelineConfig = DEFAULT_CONFIG) {} - - /** - * Initialize pipeline components - */ - async initialize(): Promise { - // Initialize WebRTC stream capture - this.streamCapture = createWebRTCCapture({ - chunkDuration: this.config.chunkDuration, - overlapDuration: this.config.overlapDuration, - sampleRate: this.config.sampleRate - }); - - // Connect WebRTC chunk processing - this.streamCapture.onChunkReady = (rawAudio, timestamp) => { - this.handleRawChunk(rawAudio, timestamp); - }; - - // Connect stream error handling - this.streamCapture.onStreamError = (error) => { - this.onPipelineError?.(error); - }; - - console.log('[Pipeline] Initialized'); - } - - /** - * Process raw audio chunk from WebRTC - */ - private async handleRawChunk(rawAudio: Float32Array, timestamp: number): Promise { - try { - // Create audio chunk - const chunk: AudioChunk = { - id: `chunk-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - timestamp, - data: rawAudio, - duration: this.config.chunkDuration / 1000 - }; - - // Add to buffer - this.chunkBuffer.push(chunk); - - // Maintain buffer size - if (this.chunkBuffer.length > this.maxBufferLength) { - const removed = this.chunkBuffer.shift(); - if (removed) { - // Process removed chunk with overlap - await this.processChunk(removed); - } - } - - // Process current chunk - await this.processChunk(chunk); - - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - console.error('[Pipeline] Error processing chunk:', err); - this.onPipelineError?.(err); - } - } - - /** - * Convert Float32Array to 16-bit PCM Buffer - */ - private float32ArrayToBuffer(floatData: Float32Array): Buffer { - const int16Array = new Int16Array(floatData.length); - for (let i = 0; i < floatData.length; i++) { - // Clamp and scale to 16-bit range - const clamped = Math.max(-1, Math.min(1, floatData[i])); - int16Array[i] = Math.round(clamped * 32767); - } - return Buffer.from(int16Array.buffer); - } - - /** - * Process a single audio chunk (mock implementation) - */ - private async processChunk(chunk: AudioChunk): Promise { - try { - // Generate mock features - // Convert Float32Array to Buffer properly - const int16Array = new Int16Array(chunk.data.length); - for (let i = 0; i < chunk.data.length; i++) { - const clamped = Math.max(-1, Math.min(1, chunk.data[i])); - int16Array[i] = Math.round(clamped * 32767); - } - const dataBuffer = Buffer.from(int16Array.buffer); - const features: AudioFeatures = await extractMockFeatures(dataBuffer, chunk.duration); - - // Generate embedding (placeholder - would use actual embedding service) - const embedding = this.generatePlaceholderEmbedding(features); - - // Create result - const result: VoiceprintResult = { - chunkId: chunk.id, - timestamp: chunk.timestamp, - features, - embedding, - confidence: 0.95, // Placeholder - would come from actual analysis - status: 'complete' - }; - - // Emit result - this.onAnalysisComplete?.(result); - - return result; - - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - console.error('[Pipeline] Preprocessing error:', err); - this.onPipelineError?.(err); - throw err; - } - } - - /** - * Generate placeholder embedding (would use actual embedding service) - */ - private generatePlaceholderEmbedding(features: AudioFeatures): number[] { - // Placeholder embedding - would be replaced with actual embedding generation - const embedding: number[] = []; - - // Use spectral features as proxy for embedding - embedding.push(features.spectralCentroid); - embedding.push(features.spectralRollOff); - embedding.push(features.zeroCrossingRate); - - // MFCC summary (first few coefficients) - if (features.mfccs && features.mfccs.length > 0) { - for (let i = 0; i < Math.min(5, features.mfccs[0].length); i++) { - embedding.push(features.mfccs[0][i]); - } - } - - // Normalize and pad - while (embedding.length < 128) { - embedding.push(0); - } - - return embedding; - } - - /** - * Start the real-time analysis pipeline - */ - async start(): Promise { - if (this.isRunning) { - console.log('[Pipeline] Already running'); - return; - } - - try { - await this.initialize(); - if (this.streamCapture) { - await this.streamCapture.start(); - } - this.isRunning = true; - - console.log('[Pipeline] Real-time analysis started'); - - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - console.error('[Pipeline] Failed to start:', err); - this.onPipelineError?.(err); - throw err; - } - } - - /** - * Stop the pipeline - */ - async stop(): Promise { - if (!this.isRunning) { - return; - } - - try { - // Drain remaining buffer - while (this.chunkBuffer.length > 0) { - const chunk = this.chunkBuffer.shift(); - if (chunk) { - await this.processChunk(chunk); - } - } - - // Stop WebRTC capture - if (this.streamCapture) { - this.streamCapture.stop(); - } - - this.isRunning = false; - console.log('[Pipeline] Stopped'); - - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - console.error('[Pipeline] Error stopping:', err); - this.onPipelineError?.(err); - throw err; - } - } - - /** - * Get pipeline status - */ - getStatus(): { - isRunning: boolean; - bufferLength: number; - streamActive: boolean; - } { - return { - isRunning: this.isRunning, - bufferLength: this.chunkBuffer.length, - streamActive: this.streamCapture?.isRecording || false - }; - } -} - -/** - * Extract mock features from audio buffer - */ -async function extractMockFeatures(buffer: Buffer, duration: number): Promise { - // Simple mock feature extraction for demonstration - // In production, this would use actual audio processing - const numMfccs = 13; - const mfccs: number[][] = []; - - // Generate mock MFCCs based on buffer hash - const bufferHash = buffer.reduce((acc, byte) => acc + byte, 0); - for (let i = 0; i < numMfccs; i++) { - const coefficients: number[] = []; - for (let j = 0; j < 20; j++) { - coefficients.push(Math.abs(Math.sin((i * j + bufferHash) * 0.1)) * 0.5 + 0.25); - } - mfccs.push(coefficients); - } - - return { - mfccs, - zeroCrossingRate: 0.02 + Math.random() * 0.03, - spectralCentroid: 1000 + Math.random() * 2000, - spectralRollOff: 3000 + Math.random() * 1000, - durationSec: duration - }; - } diff --git a/packages/core/src/audio/webrtc/stream-capture.ts b/packages/core/src/audio/webrtc/stream-capture.ts deleted file mode 100644 index 296e467..0000000 --- a/packages/core/src/audio/webrtc/stream-capture.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * WebRTC Audio Stream Capture - * Captures audio from screen/audio sharing using WebRTC APIs - * Implements 5-second chunks with 1-second overlap for sliding window analysis - */ - -interface WebRTCStreamConfig { - chunkDuration: number; // 5000ms - overlapDuration: number; // 1000ms - sampleRate: number; // 16000 Hz for voiceprint compatibility -} - -const DEFAULT_CONFIG: WebRTCStreamConfig = { - chunkDuration: 5000, - overlapDuration: 1000, - sampleRate: 16000 -}; - -export class WebRTCStreamCapture { - private stream: MediaStream | null = null; - private audioContext: AudioContext | null = null; - private analyser: AnalyserNode | null = null; - private source: MediaStreamAudioSourceNode | null = null; - private _isRecording: boolean = false; - - private buffer: Float32Array = new Float32Array(0); - public onChunkReady?: (chunk: Float32Array, timestamp: number) => void; - public onStreamError?: (error: Error) => void; - - constructor(private config: WebRTCStreamConfig = DEFAULT_CONFIG) {} - - /** - * Check if currently recording - */ - public isRecording: boolean = false; - - /** - * Start capturing audio from screen/audio sharing - */ - async start(): Promise { - if (this.isRecording) { - console.log('[WebRTC] Already recording'); - return; - } - - try { - // Request screen/audio capture with audio - this.stream = await navigator.mediaDevices.getDisplayMedia({ - video: true, // Required for audio capture - audio: true - }); - - // Stop any existing tracks - this.stream.getTracks().forEach(track => track.stop()); - - // Create audio context and analyser - this.audioContext = new AudioContext({ - sampleRate: this.config.sampleRate - }); - this.analyser = this.audioContext.createAnalyser(); - this.analyser.fftSize = 2048; - - // Connect stream to audio graph - this.source = this.audioContext.createMediaStreamSource(this.stream); - this.source.connect(this.analyser); - - this.isRecording = true; - console.log('[WebRTC] Stream capture started'); - - // Start processing loop - this.processAudio(); - - // Handle stream termination - this.stream.getVideoTracks()[0].onended = () => { - console.log('[WebRTC] User stopped sharing'); - this.stop(); - }; - - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - console.error('[WebRTC] Failed to start stream capture:', err); - this.onStreamError?.(err); - throw err; - } - } - - /** - * Process audio in real-time with sliding window - */ - private processAudio(): void { - if (!this.audioContext || !this.analyser || !this.isRecording) return; - - if (!this.analyser) return; - - const bufferLength = this.analyser.fftSize; - const buffer = new Float32Array(bufferLength); - - const processFrame = () => { - if (!this.isRecording) return; - - if (!this.analyser) return; - - this.analyser.getFloatTimeDomainData(buffer); - - // Get current timestamp - const timestamp = this.audioContext?.currentTime ?? 0; - - // Extract audio data for current frame - // Use first 512 samples for voice analysis (reduced for faster processing) - const audioData = buffer.slice(0, 512); - - // Prepare chunk for analysis - if (audioData.length > 0) { - this.onChunkReady?.(audioData, timestamp); - } - - // Schedule next frame with overlap - const frameDuration = this.config.chunkDuration - this.config.overlapDuration; - setTimeout(processFrame, frameDuration); - }; - - processFrame(); - } - - /** - * Stop audio capture - */ - stop(): void { - this._isRecording = false; - - if (this.stream) { - this.stream.getTracks().forEach(track => track.stop()); - this.stream = null; - } - - if (this.source) { - this.source.disconnect(); - this.source = null; - } - - if (this.analyser) { - this.analyser.disconnect(); - this.analyser = null; - } - - if (this.audioContext) { - this.audioContext.close(); - this.audioContext = null; - } - - console.log('[WebRTC] Stream capture stopped'); - } - - /** - * Get stream metadata - */ - getMetadata(): { - isActive: boolean; - sampleRate: number; - channels: number; - } { - if (!this.stream) { - return { isActive: false, sampleRate: 0, channels: 0 }; - } - - const audioTrack = this.stream.getAudioTracks()[0]; - if (!audioTrack) { - return { isActive: true, sampleRate: this.config.sampleRate, channels: 1 }; - } - - return { - isActive: true, - sampleRate: this.config.sampleRate, - channels: audioTrack.getSettings().channelCount || 1 - }; - } -} - -/** - * Factory function for creating stream capture with auto-start - */ -export function createWebRTCCapture(config?: WebRTCStreamConfig): WebRTCStreamCapture { - return new WebRTCStreamCapture(config || DEFAULT_CONFIG); -} diff --git a/packages/core/src/inference/call-analysis-engine.ts b/packages/core/src/inference/call-analysis-engine.ts deleted file mode 100644 index 7bc7974..0000000 --- a/packages/core/src/inference/call-analysis-engine.ts +++ /dev/null @@ -1,443 +0,0 @@ -import { EventEmitter } from 'events'; - -/** - * Real-Time Call Analysis Engine - * - * Processes audio frames for sentiment analysis, event detection, - * anomaly detection, and call quality metrics. - * - * Security hardening (FRE-4497): - * - Bounded eventBuffer and anomalyBuffer with max size + FIFO eviction - * - Real quality metrics derived from audio signal properties - * - Configurable buffer sizes to prevent memory leaks on long calls - */ - -// ── Types ──────────────────────────────────────────────────────────────────── - -export interface CallAnalysisConfig { - maxEventBufferSize: number; - maxAnomalyBufferSize: number; - analysisIntervalMs: number; - silenceThreshold: number; - volumeSpikeThreshold: number; - interruptDurationMs: number; - overlapThreshold: number; -} - -export interface CallEvent { - type: 'interrupt' | 'overlap' | 'pause' | 'volume_spike' | 'silence' | 'speaker_change'; - timestamp: number; - duration?: number; - confidence: number; -} - -export interface Anomaly { - type: 'background_noise' | 'echo' | 'distortion' | 'dropout'; - timestamp: number; - confidence: number; - details?: Record; -} - -export interface CallQualityMetrics { - mosScore: number; - jitter: number; - packetLoss: number; - latency: number; - clarity: number; -} - -export interface SentimentResult { - label: 'positive' | 'neutral' | 'negative'; - score: number; - confidence: number; -} - -export interface AnalysisResult { - callId: string; - timestamp: number; - callQuality: CallQualityMetrics; - sentiment: SentimentResult; - events: CallEvent[]; - anomalies: Anomaly[]; -} - -// ── Constants ──────────────────────────────────────────────────────────────── - -const DEFAULT_CONFIG: CallAnalysisConfig = { - maxEventBufferSize: 200, - maxAnomalyBufferSize: 100, - analysisIntervalMs: 1000, - silenceThreshold: 0.01, - volumeSpikeThreshold: 0.85, - interruptDurationMs: 300, - overlapThreshold: 0.6, -}; - -// ── Engine ─────────────────────────────────────────────────────────────────── - -export class CallAnalysisEngine extends EventEmitter { - private config: CallAnalysisConfig; - private eventBuffer: CallEvent[] = []; - private anomalyBuffer: Anomaly[] = []; - private isActive = false; - private timer?: NodeJS.Timeout; - private currentCallId: string | null = null; - private frameHistory: Float32Array[] = []; - private maxFrameHistory: number = 60; - private lastSpeakerEnergy: number = 0; - - constructor(config: Partial = {}) { - super(); - this.config = { ...DEFAULT_CONFIG, ...config }; - } - - /** - * Start the analysis engine for a call - */ - start(callId: string): void { - if (this.isActive) { - this.emit('engine:warning', { message: 'Engine already active, resetting' }); - } - this.currentCallId = callId; - this.isActive = true; - this.eventBuffer = []; - this.anomalyBuffer = []; - this.frameHistory = []; - this.lastSpeakerEnergy = 0; - - this.timer = setInterval(() => this.runAnalysis(), this.config.analysisIntervalMs); - this.emit('engine:started', { callId }); - } - - /** - * Stop the analysis engine - */ - stop(): void { - this.isActive = false; - if (this.timer) { - clearInterval(this.timer); - this.timer = undefined; - } - const callId = this.currentCallId; - this.currentCallId = null; - this.emit('engine:stopped', { callId }); - } - - /** - * Ingest an audio frame for analysis - */ - ingestFrame(frame: Float32Array, timestamp: number): void { - if (!this.isActive || !this.currentCallId) return; - - // Bounded frame history - this.frameHistory.push(frame); - if (this.frameHistory.length > this.maxFrameHistory) { - this.frameHistory.shift(); - } - } - - /** - * Run periodic analysis on accumulated frames - */ - private runAnalysis(): void { - if (!this.isActive || !this.currentCallId || this.frameHistory.length === 0) return; - - const timestamp = Date.now(); - const frames = this.frameHistory.splice(0); - const events: CallEvent[] = []; - const anomalies: Anomaly[] = []; - - for (const frame of frames) { - // Detect events - const frameEvents = this.detectEvents(frame, timestamp); - events.push(...frameEvents); - - // Detect anomalies - const frameAnomalies = this.detectAnomalies(frame, timestamp); - anomalies.push(...frameAnomalies); - } - - // Compute quality metrics from actual signal properties - const callQuality = this.computeQualityMetrics(frames); - - // Compute sentiment from audio energy patterns - const sentiment = this.computeSentiment(frames); - - // Bounded buffers with FIFO eviction - if (events.length > 0) { - this.eventBuffer.push(...events); - while (this.eventBuffer.length > this.config.maxEventBufferSize) { - this.eventBuffer.shift(); - } - this.emit('events', { callId: this.currentCallId, events }); - } - - if (anomalies.length > 0) { - this.anomalyBuffer.push(...anomalies); - while (this.anomalyBuffer.length > this.config.maxAnomalyBufferSize) { - this.anomalyBuffer.shift(); - } - this.emit('anomalies', { callId: this.currentCallId, anomalies }); - } - - // Emit combined result - const result: AnalysisResult = { - callId: this.currentCallId, - timestamp, - callQuality, - sentiment, - events, - anomalies, - }; - this.emit('result', { callId: this.currentCallId, callQuality, sentiment, events, anomalies }); - } - - /** - * Detect call events from audio frame - */ - private detectEvents(frame: Float32Array, timestamp: number): CallEvent[] { - const events: CallEvent[] = []; - const energy = this.computeEnergy(frame); - const zeroCrossingRate = this.computeZeroCrossingRate(frame); - - // Silence detection - if (energy < this.config.silenceThreshold) { - events.push({ - type: 'silence', - timestamp, - confidence: 1.0 - energy / this.config.silenceThreshold, - }); - } - - // Volume spike detection - if (energy > this.config.volumeSpikeThreshold) { - events.push({ - type: 'volume_spike', - timestamp, - confidence: (energy - this.config.volumeSpikeThreshold) / (1.0 - this.config.volumeSpikeThreshold), - }); - } - - // Speaker change detection (energy shift) - const energyDelta = Math.abs(energy - this.lastSpeakerEnergy); - if (energyDelta > 0.3 && this.lastSpeakerEnergy > 0.05) { - events.push({ - type: 'speaker_change', - timestamp, - confidence: Math.min(energyDelta, 1.0), - }); - } - this.lastSpeakerEnergy = energy; - - // Interrupt detection (sudden energy drop after high energy) - if (this.lastSpeakerEnergy > 0.5 && energy < 0.1) { - events.push({ - type: 'interrupt', - timestamp, - duration: this.config.interruptDurationMs, - confidence: 0.7, - }); - } - - // Overlap detection (high zero-crossing rate with high energy) - if (zeroCrossingRate > 0.15 && energy > 0.4) { - events.push({ - type: 'overlap', - timestamp, - confidence: Math.min(zeroCrossingRate * 2, 1.0), - }); - } - - return events; - } - - /** - * Detect anomalies from audio frame - */ - private detectAnomalies(frame: Float32Array, timestamp: number): Anomaly[] { - const anomalies: Anomaly[] = []; - const energy = this.computeEnergy(frame); - - // Background noise: low energy with consistent frequency - const stdDev = this.computeStandardDeviation(frame); - if (energy < 0.15 && stdDev < 0.05 && stdDev > 0.001) { - anomalies.push({ - type: 'background_noise', - timestamp, - confidence: 0.6, - details: { energy, stdDev }, - }); - } - - // Echo detection: repeating patterns in frame - const echoScore = this.detectEchoPattern(frame); - if (echoScore > 0.5) { - anomalies.push({ - type: 'echo', - timestamp, - confidence: echoScore, - }); - } - - // Distortion: clipping detection (samples near ±1.0) - const clipCount = Array.from(frame).filter(s => Math.abs(s) > 0.95).length; - const clipRatio = clipCount / frame.length; - if (clipRatio > 0.05) { - anomalies.push({ - type: 'distortion', - timestamp, - confidence: Math.min(clipRatio * 5, 1.0), - details: { clipRatio }, - }); - } - - // Dropout: sudden silence in active audio - if (this.frameHistory.length > 5) { - const recentAvg = this.frameHistory.slice(-5).reduce((sum, f) => sum + this.computeEnergy(f), 0) / 5; - if (recentAvg > 0.3 && energy < 0.02) { - anomalies.push({ - type: 'dropout', - timestamp, - confidence: 0.8, - details: { previousEnergy: recentAvg, currentEnergy: energy }, - }); - } - } - - return anomalies; - } - - /** - * Compute call quality metrics from actual signal properties - */ - private computeQualityMetrics(frames: Float32Array[]): CallQualityMetrics { - if (frames.length === 0) { - return { mosScore: 4.5, jitter: 0.01, packetLoss: 0.0, latency: 50, clarity: 0.95 }; - } - - // Compute actual signal statistics - const energies = frames.map(f => this.computeEnergy(f)); - const avgEnergy = energies.reduce((s, e) => s + e, 0) / energies.length; - const energyVariance = energies.reduce((s, e) => s + Math.pow(e - avgEnergy, 2), 0) / energies.length; - - // MOS score based on signal quality indicators - const signalToNoise = avgEnergy / (Math.sqrt(energyVariance) + 0.001); - const mosScore = Math.max(1.0, Math.min(5.0, 1.0 + 0.8 * signalToNoise)); - - // Jitter from energy variance - const jitter = Math.min(energyVariance * 100, 50); - - // Packet loss estimated from frame gaps (simulated from dropout anomalies) - const dropoutCount = this.anomalyBuffer.filter(a => a.type === 'dropout').length; - const packetLoss = Math.min(dropoutCount / Math.max(frames.length, 1), 0.1); - - // Latency estimate (base + variance penalty) - const latency = 30 + jitter * 2; - - // Clarity from clipping ratio - const totalSamples = frames.reduce((s, f) => s + f.length, 0); - const clippedSamples = frames.reduce((s, f) => s + Array.from(f).filter(v => Math.abs(v) > 0.95).length, 0); - const clarity = Math.max(0.5, 1.0 - clippedSamples / totalSamples); - - return { mosScore, jitter, packetLoss, latency, clarity }; - } - - /** - * Compute sentiment from audio energy patterns - */ - private computeSentiment(frames: Float32Array[]): SentimentResult { - if (frames.length === 0) { - return { label: 'neutral', score: 0, confidence: 0.5 }; - } - - const energies = frames.map(f => this.computeEnergy(f)); - const avgEnergy = energies.reduce((s, e) => s + e, 0) / energies.length; - const variance = energies.reduce((s, e) => s + Math.pow(e - avgEnergy, 2), 0) / energies.length; - - // High energy + high variance => positive/excited - // Low energy + low variance => negative/calm - // Medium energy + medium variance => neutral - const activity = avgEnergy * (1 + variance); - - if (activity > 0.4) { - return { label: 'positive', score: Math.min(activity, 1.0), confidence: 0.6 }; - } else if (activity < 0.1) { - return { label: 'negative', score: Math.max(1.0 - activity * 5, 0), confidence: 0.5 }; - } - return { label: 'neutral', score: 0.5, confidence: 0.7 }; - } - - // ── Signal Processing Helpers ────────────────────────────────────────────── - - private computeEnergy(frame: Float32Array): number { - let sum = 0; - for (let i = 0; i < frame.length; i++) { - sum += frame[i] * frame[i]; - } - return Math.sqrt(sum / frame.length); - } - - private computeZeroCrossingRate(frame: Float32Array): number { - let crossings = 0; - for (let i = 1; i < frame.length; i++) { - if ((frame[i - 1] >= 0 && frame[i] < 0) || (frame[i - 1] < 0 && frame[i] >= 0)) { - crossings++; - } - } - return crossings / frame.length; - } - - private computeStandardDeviation(frame: Float32Array): number { - const mean = frame.reduce((s, v) => s + v, 0) / frame.length; - const variance = frame.reduce((s, v) => s + Math.pow(v - mean, 2), 0) / frame.length; - return Math.sqrt(variance); - } - - private detectEchoPattern(frame: Float32Array): number { - if (frame.length < 64) return 0; - const half = frame.length / 2; - let correlation = 0; - for (let i = 0; i < half; i++) { - correlation += frame[i] * frame[i + half]; - } - correlation /= half; - return Math.max(0, correlation); - } - - /** - * Get current analysis state - */ - getState(): { - isActive: boolean; - callId: string | null; - eventBufferSize: number; - anomalyBufferSize: number; - frameHistorySize: number; - } { - return { - isActive: this.isActive, - callId: this.currentCallId, - eventBufferSize: this.eventBuffer.length, - anomalyBufferSize: this.anomalyBuffer.length, - frameHistorySize: this.frameHistory.length, - }; - } - - /** - * Get buffered events (for history queries) - */ - getEvents(): CallEvent[] { - return [...this.eventBuffer]; - } - - /** - * Get buffered anomalies (for history queries) - */ - getAnomalies(): Anomaly[] { - return [...this.anomalyBuffer]; - } -} - -export function createCallAnalysisEngine(config?: Partial): CallAnalysisEngine { - return new CallAnalysisEngine(config); -} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json deleted file mode 100644 index c9ebbf7..0000000 --- a/packages/core/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": ["ES2020", "DOM"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "resolveJsonModule": true, - "moduleResolution": "node", - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/correlation/package.json b/packages/correlation/package.json deleted file mode 100644 index 1e15114..0000000 --- a/packages/correlation/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "@shieldai/correlation", - "version": "0.1.0", - "main": "./dist/index.js", - "types": "./dist/index.js", - "scripts": { - "build": "tsc", - "lint": "eslint src/" - }, - "dependencies": { - "@shieldai/db": "workspace:*", - "@shieldai/types": "workspace:*" - }, - "exports": { - ".": "./src/index.ts" - } -} diff --git a/packages/correlation/src/emitter.ts b/packages/correlation/src/emitter.ts deleted file mode 100644 index 8db3667..0000000 --- a/packages/correlation/src/emitter.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { correlationService } from "@shieldai/correlation"; - -export async function emitDarkWatchAlert( - userId: string, - exposureId: string, - alertId: string, - breachName: string, - severity: string, - channel: string, - dataType?: string[], - dataSource?: string -): Promise { - try { - await correlationService.ingestDarkWatchAlert(userId, alertId, { - exposureId, - breachName, - severity, - channel, - dataType, - dataSource, - }); - } catch (err) { - console.error(`[Correlation] DarkWatch alert emit failed:`, err); - } -} - -export async function emitSpamShieldAlert( - userId: string, - analysisId: string, - phoneNumber: string, - decision: string, - confidence: number, - reasons?: string[], - channel?: "call" | "sms", - hiyaReputationScore?: number, - truecallerSpamScore?: number -): Promise { - try { - await correlationService.ingestSpamShieldAlert(userId, analysisId, { - phoneNumber, - decision, - confidence, - reasons, - channel, - hiyaReputationScore, - truecallerSpamScore, - }); - } catch (err) { - console.error(`[Correlation] SpamShield alert emit failed:`, err); - } -} - -export async function emitVoicePrintAlert( - userId: string, - jobId: string, - verdict: string, - syntheticScore: number, - confidence: number, - matchedEnrollmentId?: string, - matchedSimilarity?: number, - analysisType?: string -): Promise { - try { - await correlationService.ingestVoicePrintAlert(userId, jobId, { - jobId, - verdict, - syntheticScore, - confidence, - matchedEnrollmentId, - matchedSimilarity, - analysisType, - }); - } catch (err) { - console.error(`[Correlation] VoicePrint alert emit failed:`, err); - } -} - -export async function emitCallAnalysisAlert( - userId: string, - callId: string, - eventType?: string, - mosScore?: number, - anomaly?: string, - sentiment?: { label: string; score: number } -): Promise { - const sourceAlertId = `call-${callId}-${Date.now()}`; - try { - await correlationService.ingestCallAnalysisAlert(userId, sourceAlertId, { - callId, - eventType, - mosScore, - anomaly, - sentiment, - }); - } catch (err) { - console.error(`[Correlation] CallAnalysis alert emit failed:`, err); - } -} diff --git a/packages/correlation/src/engine.ts b/packages/correlation/src/engine.ts deleted file mode 100644 index dc8177f..0000000 --- a/packages/correlation/src/engine.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { prisma } from "@shieldai/db"; -import { - AlertSource, - AlertCategory, - Severity, - EntityType, - CorrelationStatus, - NormalizedAlertInput, - CorrelationGroupOutput, - CorrelatedAlertOutput, - CorrelationQuery, -} from "@shieldai/types"; -import { alertNormalizer, AlertNormalizer } from "./normalizer"; - -const SEVERITY_RANK: Record = { - LOW: 0, - INFO: 1, - MEDIUM: 2, - WARNING: 3, - HIGH: 4, - CRITICAL: 5, -}; - -function higherSeverity(a: string, b: string): string { - return SEVERITY_RANK[a] >= SEVERITY_RANK[b] ? a : b; -} - -function entitiesOverlap( - a: Array<{ type: string; value: string }>, - b: Array<{ type: string; value: string }> -): boolean { - for (const ea of a) { - for (const eb of b) { - if (ea.type === eb.type && ea.value.toLowerCase() === eb.value.toLowerCase()) { - return true; - } - } - } - return false; -} - -type AlertRow = { - id: string; - source: string; - category: string; - severity: string; - userId: string; - title: string; - description: string; - entities: unknown; - sourceAlertId: string; - groupId: string | null; - payload: unknown; - createdAt: Date; -}; - -type GroupRow = { - id: string; - userId: string; - entities: unknown; - highestSeverity: string; - status: string; - alertCount: number; - summary: string | null; - resolvedAt: Date | null; - createdAt: Date; - updatedAt: Date; -}; - -export class CorrelationEngine { - private readonly timeWindowMinutes: number; - - constructor(timeWindowMinutes: number = 30) { - this.timeWindowMinutes = timeWindowMinutes; - } - - public async ingestAlert(input: NormalizedAlertInput): Promise { - const alert = await (prisma as any).normalizedAlert.create({ - data: { - source: input.source, - category: input.category, - severity: input.severity, - userId: input.userId, - title: input.title, - description: input.description, - entities: input.entities, - sourceAlertId: input.sourceAlertId, - payload: input.payload, - createdAt: input.timestamp || new Date(), - }, - }); - - const correlation = await this.findOrCreateCorrelation(alert as AlertRow); - - if (correlation) { - await (prisma as any).normalizedAlert.update({ - where: { id: alert.id }, - data: { groupId: correlation.id }, - }); - - const updated = await (prisma as any).normalizedAlert.findUnique({ - where: { id: alert.id }, - }); - - return this.toOutput(updated as AlertRow); - } - - return this.toOutput(alert as AlertRow); - } - - private async findOrCreateCorrelation( - alert: AlertRow - ): Promise { - const cutoff = new Date(Date.now() - this.timeWindowMinutes * 60 * 1000); - - const existingGroups = await (prisma as any).correlationGroup.findMany({ - where: { - userId: alert.userId, - status: CorrelationStatus.ACTIVE, - createdAt: { gte: cutoff }, - }, - include: { - alerts: { - where: { createdAt: { gte: cutoff } }, - }, - }, - }); - - const alertEntities = alert.entities as Array<{ type: string; value: string }>; - - for (const group of existingGroups) { - const groupEntities = group.entities as Array<{ type: string; value: string }>; - - if (entitiesOverlap(groupEntities, alertEntities)) { - const newSeverity = higherSeverity( - group.highestSeverity, - alert.severity - ); - - const updatedGroup = await (prisma as any).correlationGroup.update({ - where: { id: group.id }, - data: { - highestSeverity: newSeverity, - alertCount: group.alertCount + 1, - entities: this.mergeEntities(groupEntities, alertEntities), - }, - }); - - return updatedGroup; - } - } - - const uniqueSources = new Set(); - uniqueSources.add(alert.source); - - const uniqueCategories = new Set(); - uniqueCategories.add(alert.category); - - if (uniqueSources.size > 1 || uniqueCategories.size > 1) { - const newGroup = await (prisma as any).correlationGroup.create({ - data: { - userId: alert.userId, - entities: alert.entities, - highestSeverity: alert.severity, - status: CorrelationStatus.ACTIVE, - alertCount: 1, - summary: this.generateSummary( - alert.source, - alert.category, - alert.title - ), - }, - }); - - return newGroup; - } - - return null; - } - - private mergeEntities( - a: Array<{ type: string; value: string }>, - b: Array<{ type: string; value: string }> - ): Array<{ type: string; value: string }> { - const seen = new Map(); - for (const e of [...a, ...b]) { - const key = `${e.type}:${e.value.toLowerCase()}`; - if (!seen.has(key)) { - seen.set(key, e.value); - } - } - return Array.from(seen.entries()).map(([key, value]) => { - const [type] = key.split(":"); - return { type, value }; - }); - } - - private generateSummary( - source: string, - category: string, - title: string - ): string { - return `${source} - ${category}: ${title}`; - } - - public async getCorrelatedAlerts( - query: CorrelationQuery - ): Promise<{ alerts: CorrelatedAlertOutput[]; total: number }> { - const where: Record = {}; - - if (query.userId) where.userId = query.userId; - if (query.groupId) where.groupId = query.groupId; - if (query.source) where.source = query.source; - if (query.category) where.category = query.category; - if (query.severity) where.severity = query.severity; - - if (query.timeWindowMinutes) { - where.createdAt = { - gte: new Date(Date.now() - query.timeWindowMinutes * 60 * 1000), - }; - } - - if (query.entityType && query.entityId) { - where.entities = { - path: [], - contains: JSON.stringify({ type: query.entityType, value: query.entityId }), - }; - } - - const [alerts, total] = await Promise.all([ - (prisma as any).normalizedAlert.findMany({ - where, - orderBy: { createdAt: "desc" }, - take: query.limit || 50, - skip: query.offset || 0, - }), - (prisma as any).normalizedAlert.count({ where }), - ]); - - return { - alerts: alerts.map((a: AlertRow) => this.toOutput(a)), - total, - }; - } - - public async getCorrelationGroups( - query: CorrelationQuery - ): Promise<{ groups: CorrelationGroupOutput[]; total: number }> { - const where: Record = {}; - - if (query.userId) where.userId = query.userId; - if (query.status) where.status = query.status; - - if (query.timeWindowMinutes) { - where.createdAt = { - gte: new Date(Date.now() - query.timeWindowMinutes * 60 * 1000), - }; - } - - const [groups, total] = await Promise.all([ - (prisma as any).correlationGroup.findMany({ - where, - orderBy: { createdAt: "desc" }, - take: query.limit || 50, - skip: query.offset || 0, - include: { - alerts: { - orderBy: { createdAt: "desc" }, - take: 100, - }, - }, - }), - (prisma as any).correlationGroup.count({ where }), - ]); - - return { - groups: groups.map((g: GroupRow & { alerts: AlertRow[] }) => - this.toGroupOutput(g) - ), - total, - }; - } - - public async getGroupById( - groupId: string, - userId: string - ): Promise { - const group = await (prisma as any).correlationGroup.findUnique({ - where: { id: groupId, userId }, - include: { - alerts: { - orderBy: { createdAt: "asc" }, - }, - }, - }); - - return group ? this.toGroupOutput(group as GroupRow & { alerts: AlertRow[] }) : null; - } - - public async resolveGroup( - groupId: string, - userId: string, - status: string = CorrelationStatus.RESOLVED - ): Promise { - const group = await (prisma as any).correlationGroup.update({ - where: { id: groupId, userId }, - data: { - status, - resolvedAt: new Date(), - }, - include: { - alerts: { - orderBy: { createdAt: "asc" }, - }, - }, - }); - - return this.toGroupOutput(group as GroupRow & { alerts: AlertRow[] }); - } - - public async getDashboardData( - userId: string, - timeWindowMinutes: number = 60 - ): Promise<{ - totalAlerts: number; - activeCorrelations: number; - alertsBySource: Record; - alertsBySeverity: Record; - recentGroups: CorrelationGroupOutput[]; - }> { - const cutoff = new Date(Date.now() - timeWindowMinutes * 60 * 1000); - - const [totalAlerts, activeCorrelations, recentGroups] = await Promise.all([ - (prisma as any).normalizedAlert.count({ - where: { userId, createdAt: { gte: cutoff } }, - }), - (prisma as any).correlationGroup.count({ - where: { - userId, - status: CorrelationStatus.ACTIVE, - createdAt: { gte: cutoff }, - }, - }), - (prisma as any).correlationGroup.findMany({ - where: { - userId, - status: CorrelationStatus.ACTIVE, - createdAt: { gte: cutoff }, - }, - orderBy: { createdAt: "desc" }, - take: 10, - include: { alerts: { orderBy: { createdAt: "desc" }, take: 100 } }, - }), - ]); - - const alertsBySource: Record = {}; - const alertsBySeverity: Record = {}; - - const recentAlerts = await (prisma as any).normalizedAlert.findMany({ - where: { userId, createdAt: { gte: cutoff } }, - select: { source: true, severity: true }, - }); - - for (const alert of recentAlerts) { - alertsBySource[alert.source] = (alertsBySource[alert.source] || 0) + 1; - alertsBySeverity[alert.severity] = (alertsBySeverity[alert.severity] || 0) + 1; - } - - return { - totalAlerts, - activeCorrelations, - alertsBySource, - alertsBySeverity, - recentGroups: recentGroups.map( - (g: GroupRow & { alerts: AlertRow[] }) => this.toGroupOutput(g) - ), - }; - } - - private toOutput(alert: AlertRow): CorrelatedAlertOutput { - return { - id: alert.id, - source: alert.source as AlertSource, - category: alert.category as AlertCategory, - severity: alert.severity as Severity, - userId: alert.userId, - title: alert.title, - description: alert.description, - entities: alert.entities as Array<{ type: EntityType; value: string }>, - sourceAlertId: alert.sourceAlertId, - groupId: alert.groupId || "", - payload: alert.payload as Record, - createdAt: alert.createdAt, - }; - } - - private toGroupOutput( - group: GroupRow & { alerts: AlertRow[] } - ): CorrelationGroupOutput { - const sources = new Set(); - const categories = new Set(); - const entities = group.entities as Array<{ type: EntityType; value: string }>; - - for (const alert of group.alerts) { - sources.add(alert.source); - categories.add(alert.category); - } - - return { - id: group.id, - groupId: group.id, - alertCount: group.alertCount, - highestSeverity: group.highestSeverity as Severity, - status: group.status as CorrelationStatus, - entities, - sources: Array.from(sources) as AlertSource[], - categories: Array.from(categories) as AlertCategory[], - createdAt: group.createdAt, - updatedAt: group.updatedAt, - }; - } -} - -export const correlationEngine = new CorrelationEngine(); diff --git a/packages/correlation/src/index.ts b/packages/correlation/src/index.ts deleted file mode 100644 index 548e913..0000000 --- a/packages/correlation/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { alertNormalizer, AlertNormalizer } from "./normalizer"; -export { correlationEngine, CorrelationEngine } from "./engine"; -export { correlationService, CorrelationService } from "./service"; -export { - emitDarkWatchAlert, - emitSpamShieldAlert, - emitVoicePrintAlert, - emitCallAnalysisAlert, -} from "./emitter"; diff --git a/packages/correlation/src/normalizer.ts b/packages/correlation/src/normalizer.ts deleted file mode 100644 index 61f1cd8..0000000 --- a/packages/correlation/src/normalizer.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { - AlertSource, - AlertCategory, - Severity, - EntityTypes, - NormalizedAlertInput, -} from "@shieldai/types"; - -type EntityType = (typeof EntityTypes)[keyof typeof EntityTypes]; - -function sanitizePayload( - payload: Record, - maxDepth: number = 5 -): Record { - const seen = new WeakSet(); - const clone = (obj: unknown, depth: number): unknown => { - if (depth > maxDepth) return "[max depth]"; - if (obj === null || typeof obj !== "object") return obj; - if (seen.has(obj as object)) return "[circular]"; - seen.add(obj as object); - if (Array.isArray(obj)) return obj.map((item) => clone(item, depth + 1)); - return Object.fromEntries( - Object.entries(obj as Record).map(([k, v]) => [k, clone(v, depth + 1)]) - ); - }; - return clone(payload, 0) as Record; -} - -interface DarkWatchAlertPayload { - exposureId: string; - breachName: string; - severity: string; - channel: string; - dataType?: string[]; - dataSource?: string; -} - -interface SpamShieldAlertPayload { - phoneNumber: string; - decision: string; - confidence: number; - reasons?: string[]; - channel?: "call" | "sms"; - hiyaReputationScore?: number; - truecallerSpamScore?: number; -} - -interface VoicePrintAlertPayload { - jobId: string; - verdict: string; - syntheticScore: number; - confidence: number; - matchedEnrollmentId?: string; - matchedSimilarity?: number; - analysisType?: string; -} - -interface CallAnalysisAlertPayload { - callId: string; - eventType?: string; - mosScore?: number; - anomaly?: string; - sentiment?: { label: string; score: number }; -} - -const SEVERITY_MAP: Record = { - LOW: "LOW", - INFO: "INFO", - MEDIUM: "MEDIUM", - WARNING: "WARNING", - HIGH: "HIGH", - CRITICAL: "CRITICAL", -}; - -function mapSeverity(raw: string | number): Severity { - if (typeof raw === "number") { - if (raw >= 0.9) return "CRITICAL"; - if (raw >= 0.7) return "HIGH"; - if (raw >= 0.5) return "WARNING"; - if (raw >= 0.3) return "MEDIUM"; - if (raw >= 0.1) return "INFO"; - return "LOW"; - } - const upper = raw.toUpperCase(); - return SEVERITY_MAP[upper] ?? "INFO"; -} - -export class AlertNormalizer { - public normalizeDarkWatchAlert( - userId: string, - sourceAlertId: string, - payload: DarkWatchAlertPayload, - timestamp?: Date - ): NormalizedAlertInput { - const severity = mapSeverity(payload.severity); - const entities: Array<{ type: EntityType; value: string }> = []; - - if (payload.dataSource) { - entities.push({ type: EntityTypes.EMAIL, value: payload.breachName }); - } - - return { - source: AlertSource.DARKWATCH, - category: AlertCategory.BREACH_EXPOSURE, - severity, - userId, - title: `Breach Exposure: ${payload.breachName}`, - description: payload.dataType - ? `Data types exposed: ${payload.dataType.join(", ")} in ${payload.breachName}` - : `Exposure detected in ${payload.breachName}`, - entities, - sourceAlertId, - payload: sanitizePayload(payload as unknown as Record), - timestamp, - }; - } - - public normalizeSpamShieldAlert( - userId: string, - sourceAlertId: string, - payload: SpamShieldAlertPayload, - timestamp?: Date - ): NormalizedAlertInput { - const decision = payload.decision.toUpperCase(); - const severity = - decision === "BLOCK" - ? "HIGH" - : decision === "FLAG" - ? "WARNING" - : "INFO"; - - const channel = payload.channel === "sms" ? "sms" : "call"; - const category = - channel === "sms" - ? AlertCategory.SPAM_SMS - : AlertCategory.SPAM_CALL; - - const entities: Array<{ type: EntityType; value: string }> = [ - { type: EntityTypes.PHONE_NUMBER, value: payload.phoneNumber }, - ]; - - return { - source: AlertSource.SPAMSHIELD, - category, - severity, - userId, - title: `${channel === "sms" ? "SMS" : "Call"} ${decision}: ${payload.phoneNumber}`, - description: payload.reasons - ? `SpamShield ${decision} decision. Reasons: ${payload.reasons.join(", ")}` - : `SpamShield ${decision} decision with confidence ${Math.round(payload.confidence * 100)}%`, - entities, - sourceAlertId, - payload: sanitizePayload(payload as unknown as Record), - timestamp, - }; - } - - public normalizeVoicePrintAlert( - userId: string, - sourceAlertId: string, - payload: VoicePrintAlertPayload, - timestamp?: Date - ): NormalizedAlertInput { - const verdict = payload.verdict.toUpperCase(); - let severity: Severity; - let category: AlertCategory; - - if (payload.analysisType === "VOICE_MATCH" && payload.matchedEnrollmentId) { - category = AlertCategory.VOICE_MISMATCH; - severity = - payload.matchedSimilarity !== undefined && payload.matchedSimilarity > 0.85 - ? "MEDIUM" - : "LOW"; - } else { - category = AlertCategory.SYNTHETIC_VOICE; - severity = - verdict === "SYNTHETIC" - ? mapSeverity(payload.syntheticScore) - : verdict === "UNCERTAIN" - ? "MEDIUM" - : "INFO"; - } - - const entities: Array<{ type: EntityType; value: string }> = []; - if (payload.matchedEnrollmentId) { - entities.push({ type: EntityTypes.USER_ID, value: payload.matchedEnrollmentId }); - } - - return { - source: AlertSource.VOICEPRINT, - category, - severity, - userId, - title: `Voice ${verdict}: Job ${payload.jobId}`, - description: payload.analysisType - ? `Analysis type: ${payload.analysisType}. Verdict: ${verdict} (confidence: ${Math.round(payload.confidence * 100)}%)` - : `Synthetic voice detection: ${verdict} (score: ${payload.syntheticScore.toFixed(3)})`, - entities, - sourceAlertId, - payload: sanitizePayload(payload as unknown as Record), - timestamp, - }; - } - - public normalizeCallAnalysisAlert( - userId: string, - sourceAlertId: string, - payload: CallAnalysisAlertPayload, - timestamp?: Date - ): NormalizedAlertInput { - let category: AlertCategory; - let severity: Severity; - let title: string; - let description: string; - - if (payload.anomaly) { - category = AlertCategory.CALL_ANOMALY; - severity = "WARNING"; - title = `Call Anomaly: ${payload.anomaly}`; - description = `Anomaly "${payload.anomaly}" detected in call ${payload.callId}`; - } else if (payload.mosScore !== undefined) { - category = AlertCategory.CALL_QUALITY; - severity = - payload.mosScore < 2.5 - ? "CRITICAL" - : payload.mosScore < 3.5 - ? "HIGH" - : payload.mosScore < 4.0 - ? "MEDIUM" - : "INFO"; - title = `Call Quality: MOS ${payload.mosScore.toFixed(1)}`; - description = `MOS score ${payload.mosScore.toFixed(1)} for call ${payload.callId}`; - } else if (payload.eventType) { - category = AlertCategory.CALL_EVENT; - severity = "INFO"; - title = `Call Event: ${payload.eventType}`; - description = `Event "${payload.eventType}" during call ${payload.callId}`; - } else { - category = AlertCategory.CALL_EVENT; - severity = "INFO"; - title = `Call Alert: ${payload.callId}`; - description = `Alert for call ${payload.callId}`; - } - - const entities: Array<{ type: EntityType; value: string }> = [ - { type: EntityTypes.CALL_ID, value: payload.callId }, - ]; - - return { - source: AlertSource.CALL_ANALYSIS, - category, - severity, - userId, - title, - description, - entities, - sourceAlertId, - payload: sanitizePayload(payload as unknown as Record), - timestamp, - }; - } -} - -export const alertNormalizer = new AlertNormalizer(); diff --git a/packages/correlation/src/service.ts b/packages/correlation/src/service.ts deleted file mode 100644 index ade3e29..0000000 --- a/packages/correlation/src/service.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { - AlertSource, - AlertCategory, - Severity, - EntityType, - NormalizedAlertInput, - CorrelationGroupOutput, - CorrelatedAlertOutput, - CorrelationQuery, -} from "@shieldai/types"; -import { alertNormalizer, AlertNormalizer } from "./normalizer"; -import { correlationEngine, CorrelationEngine } from "./engine"; - -export class CorrelationService { - private normalizer: AlertNormalizer; - private engine: CorrelationEngine; - - constructor( - normalizer: AlertNormalizer = alertNormalizer, - engine: CorrelationEngine = correlationEngine - ) { - this.normalizer = normalizer; - this.engine = engine; - } - - public async ingestDarkWatchAlert( - userId: string, - sourceAlertId: string, - payload: { - exposureId: string; - breachName: string; - severity: string; - channel: string; - dataType?: string[]; - dataSource?: string; - }, - timestamp?: Date - ): Promise { - const normalized = this.normalizer.normalizeDarkWatchAlert( - userId, - sourceAlertId, - payload, - timestamp - ); - return this.engine.ingestAlert(normalized); - } - - public async ingestSpamShieldAlert( - userId: string, - sourceAlertId: string, - payload: { - phoneNumber: string; - decision: string; - confidence: number; - reasons?: string[]; - channel?: "call" | "sms"; - hiyaReputationScore?: number; - truecallerSpamScore?: number; - }, - timestamp?: Date - ): Promise { - const normalized = this.normalizer.normalizeSpamShieldAlert( - userId, - sourceAlertId, - payload, - timestamp - ); - return this.engine.ingestAlert(normalized); - } - - public async ingestVoicePrintAlert( - userId: string, - sourceAlertId: string, - payload: { - jobId: string; - verdict: string; - syntheticScore: number; - confidence: number; - matchedEnrollmentId?: string; - matchedSimilarity?: number; - analysisType?: string; - }, - timestamp?: Date - ): Promise { - const normalized = this.normalizer.normalizeVoicePrintAlert( - userId, - sourceAlertId, - payload, - timestamp - ); - return this.engine.ingestAlert(normalized); - } - - public async ingestCallAnalysisAlert( - userId: string, - sourceAlertId: string, - payload: { - callId: string; - eventType?: string; - mosScore?: number; - anomaly?: string; - sentiment?: { label: string; score: number }; - }, - timestamp?: Date - ): Promise { - const normalized = this.normalizer.normalizeCallAnalysisAlert( - userId, - sourceAlertId, - payload, - timestamp - ); - return this.engine.ingestAlert(normalized); - } - - public async ingestGenericAlert( - input: NormalizedAlertInput - ): Promise { - return this.engine.ingestAlert(input); - } - - public getCorrelatedAlerts(query: CorrelationQuery) { - return this.engine.getCorrelatedAlerts(query); - } - - public getCorrelationGroups(query: CorrelationQuery) { - return this.engine.getCorrelationGroups(query); - } - - public getGroupById(groupId: string, userId: string) { - return this.engine.getGroupById(groupId, userId); - } - - public resolveGroup(groupId: string, userId: string, status?: string) { - return this.engine.resolveGroup(groupId, userId, status as any); - } - - public getDashboardData(userId: string, timeWindowMinutes?: number) { - return this.engine.getDashboardData(userId, timeWindowMinutes); - } -} - -export const correlationService = new CorrelationService(); -export { alertNormalizer, correlationEngine }; diff --git a/packages/correlation/tsconfig.json b/packages/correlation/tsconfig.json deleted file mode 100644 index 49e05ce..0000000 --- a/packages/correlation/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src"] -} diff --git a/packages/db/package.json b/packages/db/package.json deleted file mode 100644 index 0b29155..0000000 --- a/packages/db/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@shieldai/db", - "version": "0.2.0", - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "build": "prisma generate && tsc", - "db:migrate": "prisma migrate dev", - "db:seed": "tsx prisma/seed.ts", - "db:studio": "prisma studio", - "db:push": "prisma db push", - "db:format": "prisma format", - "generate": "prisma generate" - }, - "dependencies": { - "@prisma/client": "^6.2.0", - "prisma": "^6.2.0", - "zod": "^4.3.6" - }, - "devDependencies": { - "tsx": "^4.19.0", - "typescript": "^5.3.3" - }, - "exports": { - ".": "./src/index.ts" - } -} diff --git a/packages/db/prisma/migrations/20260429132230_init/migration.sql b/packages/db/prisma/migrations/20260429132230_init/migration.sql deleted file mode 100644 index 0ff9011..0000000 --- a/packages/db/prisma/migrations/20260429132230_init/migration.sql +++ /dev/null @@ -1,152 +0,0 @@ --- CreateEnum -CREATE TYPE "SubscriptionTier" AS ENUM ('BASIC', 'PLUS', 'PREMIUM'); - --- CreateEnum -CREATE TYPE "IdentifierType" AS ENUM ('EMAIL', 'PHONE', 'SSN'); - --- CreateEnum -CREATE TYPE "WatchListStatus" AS ENUM ('ACTIVE', 'PAUSED'); - --- CreateEnum -CREATE TYPE "Severity" AS ENUM ('INFO', 'WARNING', 'CRITICAL'); - --- CreateEnum -CREATE TYPE "AlertChannel" AS ENUM ('EMAIL', 'PUSH', 'SMS'); - --- CreateEnum -CREATE TYPE "AlertStatus" AS ENUM ('PENDING', 'SENT', 'READ'); - --- CreateEnum -CREATE TYPE "ScanJobStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED'); - --- CreateEnum -CREATE TYPE "DataSource" AS ENUM ('HIBP', 'SECURITY_TRAILS', 'CENSYS', 'SHODAN', 'HONEYPOT'); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "email" TEXT NOT NULL, - "name" TEXT, - "subscriptionTier" "SubscriptionTier" NOT NULL DEFAULT 'BASIC', - "familyGroupId" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "WatchListItem" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "identifierType" "IdentifierType" NOT NULL, - "identifierValue" TEXT NOT NULL, - "identifierHash" TEXT NOT NULL, - "status" "WatchListStatus" NOT NULL DEFAULT 'ACTIVE', - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "WatchListItem_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Exposure" ( - "id" TEXT NOT NULL, - "watchListItemId" TEXT NOT NULL, - "dataSource" "DataSource" NOT NULL, - "breachName" TEXT NOT NULL, - "exposedAt" TIMESTAMP(3) NOT NULL, - "dataType" TEXT[], - "severity" "Severity" NOT NULL, - "details" TEXT, - "contentHash" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Exposure_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Alert" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "exposureId" TEXT NOT NULL, - "severity" "Severity" NOT NULL, - "channel" "AlertChannel" NOT NULL, - "status" "AlertStatus" NOT NULL DEFAULT 'PENDING', - "dedupKey" TEXT NOT NULL, - "sentAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Alert_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "ScanJob" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "status" "ScanJobStatus" NOT NULL DEFAULT 'PENDING', - "source" "DataSource", - "resultCount" INTEGER NOT NULL DEFAULT 0, - "errorMessage" TEXT, - "completedAt" TIMESTAMP(3), - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "ScanJob_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE INDEX "User_email_idx" ON "User"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "WatchListItem_identifierHash_key" ON "WatchListItem"("identifierHash"); - --- CreateIndex -CREATE INDEX "WatchListItem_userId_idx" ON "WatchListItem"("userId"); - --- CreateIndex -CREATE INDEX "WatchListItem_identifierHash_idx" ON "WatchListItem"("identifierHash"); - --- CreateIndex -CREATE UNIQUE INDEX "Exposure_contentHash_key" ON "Exposure"("contentHash"); - --- CreateIndex -CREATE INDEX "Exposure_watchListItemId_idx" ON "Exposure"("watchListItemId"); - --- CreateIndex -CREATE INDEX "Exposure_contentHash_idx" ON "Exposure"("contentHash"); - --- CreateIndex -CREATE INDEX "Exposure_dataSource_idx" ON "Exposure"("dataSource"); - --- CreateIndex -CREATE UNIQUE INDEX "Alert_exposureId_key" ON "Alert"("exposureId"); - --- CreateIndex -CREATE INDEX "Alert_userId_status_idx" ON "Alert"("userId", "status"); - --- CreateIndex -CREATE INDEX "Alert_dedupKey_idx" ON "Alert"("dedupKey"); - --- CreateIndex -CREATE INDEX "ScanJob_userId_status_idx" ON "ScanJob"("userId", "status"); - --- CreateIndex -CREATE INDEX "ScanJob_createdAt_idx" ON "ScanJob"("createdAt"); - --- AddForeignKey -ALTER TABLE "WatchListItem" ADD CONSTRAINT "WatchListItem_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Exposure" ADD CONSTRAINT "Exposure_watchListItemId_fkey" FOREIGN KEY ("watchListItemId") REFERENCES "WatchListItem"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Alert" ADD CONSTRAINT "Alert_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Alert" ADD CONSTRAINT "Alert_exposureId_fkey" FOREIGN KEY ("exposureId") REFERENCES "Exposure"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "ScanJob" ADD CONSTRAINT "ScanJob_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/20260501184031_add_spam_feedback_table/migration.sql b/packages/db/prisma/migrations/20260501184031_add_spam_feedback_table/migration.sql deleted file mode 100644 index 857e6af..0000000 --- a/packages/db/prisma/migrations/20260501184031_add_spam_feedback_table/migration.sql +++ /dev/null @@ -1,25 +0,0 @@ --- CreateTable -CREATE TABLE "SpamFeedback" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "phoneNumber" TEXT NOT NULL, - "phoneNumberHash" TEXT NOT NULL, - "isSpam" BOOLEAN NOT NULL, - "label" TEXT, - "metadata" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "SpamFeedback_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE INDEX "SpamFeedback_userId_idx" ON "SpamFeedback"("userId"); - --- CreateIndex -CREATE INDEX "SpamFeedback_phoneNumberHash_idx" ON "SpamFeedback"("phoneNumberHash"); - --- CreateIndex -CREATE INDEX "SpamFeedback_createdAt_idx" ON "SpamFeedback"("createdAt"); - --- AddForeignKey -ALTER TABLE "SpamFeedback" ADD CONSTRAINT "SpamFeedback_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/20260514000000_add_device_tokens/migration.sql b/packages/db/prisma/migrations/20260514000000_add_device_tokens/migration.sql deleted file mode 100644 index 46bf0c8..0000000 --- a/packages/db/prisma/migrations/20260514000000_add_device_tokens/migration.sql +++ /dev/null @@ -1,36 +0,0 @@ --- Create device types enum -DO $$ BEGIN - CREATE TYPE "DeviceType" AS ENUM('mobile', 'web', 'desktop'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; - --- Create platform enum -DO $$ BEGIN - CREATE TYPE "Platform" AS ENUM('ios', 'android', 'web'); -EXCEPTION - WHEN duplicate_object THEN null; -END $$; - --- Create device_tokens table -CREATE TABLE IF NOT EXISTS "device_tokens" ( - "id" UUID PRIMARY KEY DEFAULT gen_random_uuid(), - "userId" UUID NOT NULL REFERENCES "users"("id") ON DELETE CASCADE, - "deviceType" "DeviceType" NOT NULL DEFAULT 'mobile', - "token" TEXT NOT NULL UNIQUE, - "platform" "Platform" NOT NULL, - "appName" TEXT, - "appVersion" TEXT, - "osVersion" TEXT, - "model" TEXT, - "isActive" BOOLEAN NOT NULL DEFAULT true, - "lastUsedAt" TIMESTAMP NOT NULL DEFAULT NOW(), - "createdAt" TIMESTAMP NOT NULL DEFAULT NOW(), - "updatedAt" TIMESTAMP NOT NULL DEFAULT NOW() -); - --- Create indexes -CREATE INDEX IF NOT EXISTS "device_tokens_userId_idx" ON "device_tokens"("userId"); -CREATE INDEX IF NOT EXISTS "device_tokens_deviceType_idx" ON "device_tokens"("deviceType"); -CREATE INDEX IF NOT EXISTS "device_tokens_platform_idx" ON "device_tokens"("platform"); -CREATE INDEX IF NOT EXISTS "device_tokens_isActive_idx" ON "device_tokens"("isActive"); diff --git a/packages/db/prisma/migrations/migration_lock.toml b/packages/db/prisma/migrations/migration_lock.toml deleted file mode 100644 index 044d57c..0000000 --- a/packages/db/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (e.g., Git) -provider = "postgresql" diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma deleted file mode 100644 index d108f75..0000000 --- a/packages/db/prisma/schema.prisma +++ /dev/null @@ -1,918 +0,0 @@ -// Prisma schema for ShieldAI -// All models for the multi-service SaaS platform - -generator client { - provider = "prisma-client-js" -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -// ============================================ -// User & Authentication Models -// ============================================ - -model User { - id String @id @default(uuid()) - email String @unique - emailVerified DateTime? - name String? - image String? - role UserRole @default(user) - - // Relationships - accounts Account[] - sessions Session[] - deviceTokens DeviceToken[] - familyGroups FamilyGroupMember[] - familyGroupOwned FamilyGroup[] @relation("FamilyGroupOwner") - subscriptions Subscription[] - alerts Alert[] - voiceEnrollments VoiceEnrollment[] - voiceAnalyses VoiceAnalysis[] - spamFeedback SpamFeedback[] - spamRules SpamRule[] - normalizedAlerts NormalizedAlert[] - correlationGroups CorrelationGroup[] - securityReports SecurityReport[] - analysisJobs AnalysisJob[] - - // Audit - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([email]) - @@index([role]) -} - -enum UserRole { - user - family_admin - family_member - support -} - -enum DetectionVerdict { - NATURAL - SYNTHETIC - UNCERTAIN -} - -enum AnalysisType { - SYNTHETIC_DETECTION - VOICE_MATCH - BATCH -} - -enum AnalysisJobStatus { - PENDING - RUNNING - COMPLETED - FAILED -} - -model Account { - id String @id @default(uuid()) - userId String - provider String - providerAccountId String - access_token String? - refresh_token String? - expires_at Int? - token_type String? - scope String? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([userId, provider, providerAccountId]) - @@index([userId]) -} - -model Session { - id String @id @default(uuid()) - userId String - sessionToken String @unique - expires DateTime - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([sessionToken]) - @@index([userId]) -} - -model DeviceToken { - id String @id @default(uuid()) - userId String - deviceType DeviceType - token String @unique - platform Platform - appName String? - appVersion String? - osVersion String? - model String? - isActive Boolean @default(true) - lastUsedAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@index([deviceType]) - @@index([platform]) - @@index([isActive]) -} - -enum DeviceType { - mobile - web - desktop -} - -enum Platform { - ios - android - web -} - -// ============================================ -// Family & Subscription Models -// ============================================ - -model FamilyGroup { - id String @id @default(uuid()) - name String - ownerId String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - owner User @relation("FamilyGroupOwner", fields: [ownerId], references: [id]) - members FamilyGroupMember[] - subscriptions Subscription[] - - @@index([ownerId]) - @@index([name]) -} - -model FamilyGroupMember { - id String @id @default(uuid()) - groupId String - userId String - role FamilyMemberRole @default(member) - joinedAt DateTime @default(now()) - - group FamilyGroup @relation(fields: [groupId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([groupId, userId]) - @@index([groupId]) - @@index([userId]) -} - -enum FamilyMemberRole { - owner - admin - member -} - -model Subscription { - id String @id @default(uuid()) - userId String - familyGroupId String? - stripeId String? @unique - tier SubscriptionTier @default(basic) - status SubscriptionStatus @default(active) - currentPeriodStart DateTime - currentPeriodEnd DateTime - cancelAtPeriodEnd Boolean @default(false) - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - familyGroup FamilyGroup? @relation(fields: [familyGroupId], references: [id]) - - watchlistItems WatchlistItem[] - exposures Exposure[] - alerts Alert[] - propertyWatchlistItems PropertyWatchlistItem[] - removalRequests RemovalRequest[] - brokerListings BrokerListing[] - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@index([familyGroupId]) - @@index([stripeId]) - @@index([tier]) -} - -enum SubscriptionTier { - basic - plus - premium -} - -enum SubscriptionStatus { - active - past_due - canceled - unpaid - trialing -} - -// ============================================ -// DarkWatch Models (Dark Web Monitoring) -// ============================================ - -model WatchlistItem { - id String @id @default(uuid()) - subscriptionId String - type WatchlistType - value String - hash String // SHA-256 hash for deduplication - isActive Boolean @default(true) - - subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) - exposures Exposure[] - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([subscriptionId, type, hash]) - @@index([subscriptionId]) - @@index([type]) - @@index([hash]) -} - -enum WatchlistType { - email - phoneNumber - ssn - address - domain -} - -model Exposure { - id String @id @default(uuid()) - subscriptionId String - watchlistItemId String? - source ExposureSource - dataType WatchlistType - identifier String - identifierHash String - severity ExposureSeverity @default(info) - metadata Json? // Additional source-specific data - isFirstTime Boolean @default(false) - - subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) - watchlistItem WatchlistItem? @relation(fields: [watchlistItemId], references: [id]) - alerts Alert[] - - detectedAt DateTime - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([subscriptionId]) - @@index([watchlistItemId]) - @@index([source]) - @@index([severity]) - @@index([detectedAt]) -} - -enum ExposureSource { - hibp // Have I Been Pwned - securityTrails - censys - darkWebForum - shodan - honeypot -} - -enum ExposureSeverity { - info - warning - critical -} - -// ============================================ -// Notification & Alert Models -// ============================================ - -model Alert { - id String @id @default(uuid()) - subscriptionId String - userId String - exposureId String? - type AlertType - title String - message String - severity AlertSeverity @default(info) - isRead Boolean @default(false) - readAt DateTime? - channel AlertChannel[] // Array of notification channels - - subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - exposure Exposure? @relation(fields: [exposureId], references: [id]) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([subscriptionId]) - @@index([userId]) - @@index([isRead]) - @@index([createdAt]) -} - -enum AlertType { - exposure_detected - exposure_resolved - scan_complete - subscription_changed - system_warning -} - -enum AlertSeverity { - info - warning - critical -} - -enum AlertChannel { - email - push - sms -} - -// ============================================ -// VoicePrint Models (Voice Cloning Detection) -// ============================================ - -model VoiceEnrollment { - id String @id @default(uuid()) - userId String - name String - voiceHash String // FAISS embedding hash - audioMetadata Json? // Sample rate, duration, etc. - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - analyses VoiceAnalysis[] - - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@index([voiceHash]) -} - -model VoiceAnalysis { - id String @id @default(uuid()) - enrollmentId String? - userId String - audioHash String // Content hash of audio file - isSynthetic Boolean - confidence Float // 0.0 to 1.0 - analysisResult Json // Full ML analysis results - audioUrl String // S3 storage URL - - enrollment VoiceEnrollment? @relation(fields: [enrollmentId], references: [id]) - user User @relation(fields: [userId], references: [id]) - - createdAt DateTime @default(now()) - - @@index([userId]) - @@index([enrollmentId]) - @@index([audioHash]) -} - -model AnalysisJob { - id String @id @default(uuid()) - userId String - analysisType AnalysisType - audioFilePath String - status AnalysisJobStatus - errorMessage String? - completedAt DateTime? - createdAt DateTime @default(now()) - - user User @relation(fields: [userId], references: [id]) - result AnalysisResult? - - @@index([userId]) - @@index([status]) - @@index([createdAt]) -} - -model AnalysisResult { - id String @id @default(uuid()) - analysisJobId String @unique - syntheticScore Float - verdict DetectionVerdict - confidence Float - processingTimeMs Int - matchedEnrollmentId String? - matchedSimilarity Float? - modelVersion String? - - analysisJob AnalysisJob @relation(fields: [analysisJobId], references: [id]) - - createdAt DateTime @default(now()) - - @@index([analysisJobId]) - @@index([syntheticScore]) - @@index([verdict]) -} - -// ============================================ -// SpamShield Models (Spam Detection) -// ============================================ - -model SpamFeedback { - id String @id @default(uuid()) - userId String - phoneNumber String - phoneNumberHash String // SHA-256 hash - isSpam Boolean - confidence Float? // ML model confidence - feedbackType FeedbackType - metadata Json? // Call duration, time, etc. - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@index([phoneNumberHash]) - @@index([isSpam]) -} - -enum FeedbackType { - initial_detection - user_confirmation - user_rejection - auto_learned -} - -model SpamRule { - id String @id @default(uuid()) - userId String? - isGlobal Boolean @default(false) - ruleType RuleType - pattern String - action RuleAction - priority Int @default(0) - isActive Boolean @default(true) - - user User? @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([userId]) - @@index([isGlobal]) - @@index([ruleType]) -} - -enum RuleType { - phoneNumber - areaCode - prefix - pattern - reputation -} - -enum RuleAction { - block - flag - allow - challenge -} - -// ============================================ -// Audit & Analytics Models -// ============================================ - -model AuditLog { - id String @id @default(uuid()) - userId String? - action String - resource String - resourceId String? - changes Json? // Before/after values - metadata Json? - ipAddress String? - userAgent String? - - createdAt DateTime @default(now()) - - @@index([userId]) - @@index([action]) - @@index([resource]) - @@index([createdAt]) -} - -model KPISnapshot { - id String @id @default(uuid()) - date DateTime @unique - metricName String - metricValue Float - metadata Json? - - createdAt DateTime @default(now()) - - @@index([metricName]) - @@index([date]) -} - -// ============================================ -// Cross-Service Alert Correlation Models -// ============================================ - -enum AlertSource { - DARKWATCH - SPAMSHIELD - VOICEPRINT - CALL_ANALYSIS - HOME_TITLE - INFO_BROKER -} - -enum AlertCategory { - BREACH_EXPOSURE - SPAM_CALL - SPAM_SMS - SYNTHETIC_VOICE - VOICE_MISMATCH - CALL_ANOMALY - CALL_QUALITY - CALL_EVENT - HOME_TITLE - INFO_BROKER_LISTING - INFO_BROKER_REMOVAL -} - -enum NormalizedAlertSeverity { - LOW - INFO - MEDIUM - WARNING - HIGH - CRITICAL -} - -enum CorrelationStatus { - ACTIVE - RESOLVED - FALSE_POSITIVE -} - -model NormalizedAlert { - id String @id @default(uuid()) - source AlertSource - category AlertCategory - severity NormalizedAlertSeverity - userId String - title String - description String - entities Json - sourceAlertId String - groupId String? - payload Json? - createdAt DateTime - updatedAt DateTime @default(now()) @updatedAt - - correlationGroup CorrelationGroup? @relation(fields: [groupId], references: [id]) - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([sourceAlertId]) - @@index([userId]) - @@index([groupId]) - @@index([source]) - @@index([severity]) - @@index([createdAt]) - @@index([userId, createdAt]) -} - -model CorrelationGroup { - id String @id @default(uuid()) - userId String - entities Json - highestSeverity NormalizedAlertSeverity - status CorrelationStatus @default(ACTIVE) - alertCount Int @default(0) - summary String? - resolvedAt DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - - alerts NormalizedAlert[] - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@index([userId]) - @@index([status]) - @@index([userId, status]) - @@index([createdAt]) -} - -// ============================================ -// Report Generation Models -// ============================================ - -enum ReportType { - MONTHLY_PLUS - ANNUAL_PREMIUM - WEEKLY_DIGEST -} - -enum ReportStatus { - PENDING - GENERATING - COMPLETED - FAILED - DELIVERED -} - -model SecurityReport { - id String @id @default(uuid()) - userId String - subscriptionId String - reportType ReportType - status ReportStatus @default(PENDING) - periodStart DateTime - periodEnd DateTime - title String - summary String? - htmlContent String? - pdfUrl String? - dataPayload Json? - error String? - scheduledFor DateTime? - deliveredAt DateTime? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - - @@index([userId]) - @@index([subscriptionId]) - @@index([reportType]) - @@index([status]) - @@index([periodStart, periodEnd]) - @@index([createdAt]) -} - -// ============================================ -// Waitlist & Marketing Models -// ============================================ - -model WaitlistEntry { - id String @id @default(uuid()) - email String - name String? - source String? // landing_page, blog, referral, social, paid_search - tier SubscriptionTier? // interest level - utmSource String? - utmMedium String? - utmCampaign String? - metadata Json? // Browser, device, location, etc. - - // Conversion tracking - convertedAt DateTime? - convertedToUserId String? - convertedToSubscriptionId String? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([email]) - @@index([source]) - @@index([createdAt]) -} - -model BlogPost { - id String @id @default(uuid()) - slug String @unique - title String - excerpt String? - content String - authorName String? - coverImageUrl String? - tags String[] // Array of tag strings - published Boolean @default(false) - publishedAt DateTime? - viewCount Int @default(0) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([slug]) - @@index([published, publishedAt]) - @@index([tags]) -} - -// ============================================ -// Home Title Service Models -// ============================================ - -enum PropertyChangeType { - tax_change - deed_change - ownership_transfer - lien_filing - metadata_change -} - -enum PropertyChangeSeverity { - info - warning - critical -} - -model PropertyWatchlistItem { - id String @id @default(uuid()) - subscriptionId String - address String - parcelId String? - ownerName String? - streetAddress String - city String? @default("") - state String? @default("") - zipCode String? @default("") - latitude Float? - longitude Float? - isActive Boolean @default(true) - - subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) - snapshots PropertySnapshot[] - changes PropertyChange[] - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@unique([subscriptionId, parcelId]) - @@index([subscriptionId]) - @@index([parcelId]) - @@index([address]) -} - -model PropertySnapshot { - id String @id @default(uuid()) - propertyWatchlistItemId String - subscriptionId String - capturedAt DateTime - ownerName String - address Json // { streetNumber, streetName, streetType, unit, city, state, zip, latitude?, longitude? } - deedDate String? - taxId String? - propertyType String @default("residential") - taxAmount Float? - lienCount Int @default(0) - - propertyWatchlistItem PropertyWatchlistItem @relation(fields: [propertyWatchlistItemId], references: [id], onDelete: Cascade) - changes PropertyChange[] @relation("SnapshotChanges") - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - @@index([propertyWatchlistItemId]) - @@index([subscriptionId]) - @@index([capturedAt]) -} - -model PropertyChange { - id String @id @default(uuid()) - propertyWatchlistItemId String - snapshotId String? - changeType PropertyChangeType - severity PropertyChangeSeverity @default(info) - details Json? - detectedAt DateTime @default(now()) - - propertyWatchlistItem PropertyWatchlistItem @relation(fields: [propertyWatchlistItemId], references: [id], onDelete: Cascade) - snapshot PropertySnapshot? @relation("SnapshotChanges", fields: [snapshotId], references: [id]) - - @@index([propertyWatchlistItemId]) - @@index([snapshotId]) - @@index([changeType]) -} - -// ============================================ -// Info Broker Removal Models -// ============================================ - -enum BrokerCategory { - PEOPLE_SEARCH - BACKGROUND_CHECK - PUBLIC_RECORDS - REVERSE_LOOKUP - SOCIAL_MEDIA -} - -enum RemovalMethod { - AUTOMATED - MANUAL_FORM - EMAIL - PHONE - MAIL - NONE -} - -enum RemovalStatus { - PENDING - SUBMITTED - IN_PROGRESS - COMPLETED - FAILED - REJECTED - CANCELLED -} - -model InfoBroker { - id String @id @default(uuid()) - name String - domain String @unique - category BrokerCategory - removalMethod RemovalMethod - removalUrl String? - requiresAccount Boolean @default(false) - requiresVerification Boolean @default(false) - estimatedDays Int @default(14) - isActive Boolean @default(true) - - removalRequests RemovalRequest[] - - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - - @@index([category]) - @@index([isActive]) - @@index([removalMethod]) -} - -model RemovalRequest { - id String @id @default(uuid()) - subscriptionId String - brokerId String - status RemovalStatus @default(PENDING) - personalInfo Json // { fullName, email?, phone?, address?, dob? } - method RemovalMethod - attempts Int @default(0) - nextRetryAt DateTime? - submittedAt DateTime? - completedAt DateTime? - error String? - notes String? - metadata Json? // Broker response data, tracking info - - broker InfoBroker @relation(fields: [brokerId], references: [id]) - subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) - brokerListings BrokerListing[] - - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - - @@index([subscriptionId]) - @@index([brokerId]) - @@index([status]) - @@index([submittedAt]) - @@index([subscriptionId, status]) -} - -model BrokerListing { - id String @id @default(uuid()) - subscriptionId String - brokerId String - removalRequestId String? - url String - dataFound Json // Fields found on the listing - screenshotUrl String? - isRemoved Boolean @default(false) - removedAt DateTime? - - removalRequest RemovalRequest? @relation(fields: [removalRequestId], references: [id]) - subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade) - - scannedAt DateTime @default(now()) - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - - @@index([subscriptionId]) - @@index([brokerId]) - @@index([removalRequestId]) - @@index([isRemoved]) - @@index([subscriptionId, isRemoved]) -} diff --git a/packages/db/prisma/seed.ts b/packages/db/prisma/seed.ts deleted file mode 100644 index 1251cc8..0000000 --- a/packages/db/prisma/seed.ts +++ /dev/null @@ -1,24 +0,0 @@ -import prisma from "../src"; - -async function main() { - const user = await prisma.user.upsert({ - where: { email: "dev@shieldai.local" }, - update: {}, - create: { - email: "dev@shieldai.local", - name: "Dev User", - role: "user", - }, - }); - - console.log("Seeded user:", user.email); -} - -main() - .catch((e) => { - console.error(e); - process.exit(1); - }) - .finally(async () => { - await prisma.$disconnect(); - }); diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts deleted file mode 100644 index d0edf33..0000000 --- a/packages/db/src/index.ts +++ /dev/null @@ -1,92 +0,0 @@ -// ============================================ -// Consolidated @shieldai/db package -// ============================================ -// Merges functionality from: -// - @shieldai/db (Prisma v6.2.0, FieldEncryptionService) -// - @shieldsai/shared-db (singleton pattern, type exports) -// ============================================ - -import { PrismaClient } from '@prisma/client'; -import { FieldEncryptionService } from './services/field-encryption.service'; - -// ============================================ -// Singleton Pattern (from shared-db) -// ============================================ -const globalForPrisma = globalThis as unknown as { - prisma: PrismaClient | undefined; -}; - -export const prisma = - globalForPrisma.prisma ?? - new PrismaClient({ - log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'], - }); - -if (process.env.NODE_ENV === 'development') { - globalForPrisma.prisma = prisma; -} - -export default prisma; - -// ============================================ -// Services (from @shieldai/db) -// ============================================ -export { FieldEncryptionService }; - -// ============================================ -// Type Exports (from shared-db) -// ============================================ -export type { - User, - Account, - Session, - FamilyGroup, - FamilyGroupMember, - Subscription, - WatchlistItem, - PropertyWatchlistItem, - PropertySnapshot, - PropertyChange, - Exposure, - Alert, - VoiceEnrollment, - VoiceAnalysis, - AnalysisJob, - AnalysisResult, - SpamFeedback, - SpamRule, - AuditLog, - KPISnapshot, - SecurityReport, - WaitlistEntry, - BlogPost, - InfoBroker, - RemovalRequest, - BrokerListing, - UserRole, - FamilyMemberRole, - SubscriptionTier, - SubscriptionStatus, - WatchlistType, - PropertyChangeType, - PropertyChangeSeverity, - ExposureSource, - ExposureSeverity, - AlertType, - AlertSeverity, - AlertChannel, - FeedbackType, - RuleType, - RuleAction, - ReportType, - ReportStatus, - RemovalStatus, - RemovalMethod, - BrokerCategory, - AnalysisType, - AnalysisJobStatus, - DetectionVerdict, -} from '@prisma/client'; - -export * as PrismaModels from '@prisma/client'; -export type { PrismaClient }; diff --git a/packages/db/src/services/field-encryption.service.ts b/packages/db/src/services/field-encryption.service.ts deleted file mode 100644 index 92c2637..0000000 --- a/packages/db/src/services/field-encryption.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import crypto from 'crypto'; - -if (!process.env.PII_ENCRYPTION_KEY) { - throw new Error("PII_ENCRYPTION_KEY environment variable is required — set it before starting the server"); -} -const ENCRYPTION_KEY = process.env.PII_ENCRYPTION_KEY; -const IV_LENGTH = 16; - -export class FieldEncryptionService { - static encrypt(text: string): string { - const iv = crypto.randomBytes(IV_LENGTH); - const key = crypto.createHash('sha256').update(ENCRYPTION_KEY).digest(); - const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); - - let encrypted = cipher.update(text, 'utf8', 'base64'); - encrypted += cipher.final('base64'); - - return `${iv.toString('base64')}:${encrypted}`; - } - - static decrypt(encryptedText: string): string { - const [ivBase64, ciphertext] = encryptedText.split(':'); - const iv = Buffer.from(ivBase64, 'base64'); - const key = crypto.createHash('sha256').update(ENCRYPTION_KEY).digest(); - const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); - - let decrypted = decipher.update(ciphertext, 'base64', 'utf8'); - decrypted += decipher.final('utf8'); - - return decrypted; - } - - static hashPhoneNumber(phoneNumber: string): string { - return crypto.createHash('sha256').update(phoneNumber).digest('hex'); - } -} diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json deleted file mode 100644 index 0acc0df..0000000 --- a/packages/db/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "module": "ES2022", - "moduleResolution": "Bundler" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/extension/package.json b/packages/extension/package.json deleted file mode 100644 index 27affeb..0000000 --- a/packages/extension/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@shieldai/extension", - "version": "0.1.0", - "private": true, - "description": "ShieldAI Browser Extension - Phishing & Spam Protection", - "scripts": { - "build": "vite build", - "build:chrome": "vite build --mode chrome", - "build:firefox": "vite build --mode firefox", - "dev": "vite build --watch --mode chrome", - "test": "vitest run", - "lint": "eslint src/" - }, - "dependencies": { - "@shieldai/types": "workspace:*" - }, - "devDependencies": { - "@types/chrome": "^0.0.268", - "vite": "^5.4.0", - "typescript": "^5.7.0", - "vitest": "^4.1.5" - } -} diff --git a/packages/extension/public/icons/icon128.png b/packages/extension/public/icons/icon128.png deleted file mode 100644 index 7b6cd87..0000000 Binary files a/packages/extension/public/icons/icon128.png and /dev/null differ diff --git a/packages/extension/public/icons/icon128.svg b/packages/extension/public/icons/icon128.svg deleted file mode 100644 index 7354f64..0000000 --- a/packages/extension/public/icons/icon128.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/packages/extension/public/icons/icon16.png b/packages/extension/public/icons/icon16.png deleted file mode 100644 index 1534b6b..0000000 Binary files a/packages/extension/public/icons/icon16.png and /dev/null differ diff --git a/packages/extension/public/icons/icon48.png b/packages/extension/public/icons/icon48.png deleted file mode 100644 index 185f80a..0000000 Binary files a/packages/extension/public/icons/icon48.png and /dev/null differ diff --git a/packages/extension/public/manifest.json b/packages/extension/public/manifest.json deleted file mode 100644 index 679f598..0000000 --- a/packages/extension/public/manifest.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "manifest_version": 3, - "name": "ShieldAI - Phishing & Spam Protection", - "version": "0.1.0", - "description": "Real-time phishing detection and spam protection powered by ShieldAI", - "background": { - "service_worker": "background.js" - }, - "permissions": [ - "activeTab", - "storage", - "tabs", - "scripting", - "declarativeNetRequest", - "notifications" - ], - "host_permissions": [ - "https://*/*", - "http://*/*" - ], - "action": { - "default_popup": "popup.html", - "default_icon": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - } - }, - "icons": { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - }, - "options_page": "options.html", - "content_scripts": [ - { - "matches": ["https://*/*", "http://*/*"], - "js": ["content.js"], - "run_at": "document_start", - "all_frames": false - } - ], - "declarative_net_request": { - "rule_resources": [ - { - "id": "phishing_rules", - "enabled": true, - "path": "rules/phishing-rules.json" - } - ] - } -} diff --git a/packages/extension/public/options.html b/packages/extension/public/options.html deleted file mode 100644 index fbac82f..0000000 --- a/packages/extension/public/options.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - ShieldAI Options - - - -

🛡️ ShieldAI Options

-

Configure your phishing & spam protection

- -
-
Connection
-
- - -
-
- - -
-
- -
-
Protection Settings
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
Blocked Domains
-
    -
    - - -
    -
    - -
    -
    Allowed Domains (Whitelist)
    -
      -
      - - -
      -
      - -
      - - -
      - -
      Settings saved!
      - - - - diff --git a/packages/extension/public/popup.html b/packages/extension/public/popup.html deleted file mode 100644 index 29afc1e..0000000 --- a/packages/extension/public/popup.html +++ /dev/null @@ -1,271 +0,0 @@ - - - - - - ShieldAI Protection - - - - - - - - - - diff --git a/packages/extension/public/rules/phishing-rules.json b/packages/extension/public/rules/phishing-rules.json deleted file mode 100644 index f0e516c..0000000 --- a/packages/extension/public/rules/phishing-rules.json +++ /dev/null @@ -1,58 +0,0 @@ -[ - { - "id": 1, - "priority": 1, - "action": { "type": "BLOCK" }, - "condition": { - "urlFilter": "*://*login-secure-portal*/*", - "resourceTypes": ["main_frame"] - } - }, - { - "id": 2, - "priority": 1, - "action": { "type": "BLOCK" }, - "condition": { - "urlFilter": "*://*account-verify-now*/*", - "resourceTypes": ["main_frame"] - } - }, - { - "id": 3, - "priority": 1, - "action": { "type": "BLOCK" }, - "condition": { - "urlFilter": "*://*secure-auth-signin*/*", - "resourceTypes": ["main_frame"] - } - }, - { - "id": 4, - "priority": 1, - "action": { "type": "BLOCK" }, - "condition": { - "urlFilter": "*://*wallet-connect-verify*/*", - "resourceTypes": ["main_frame"] - } - }, - { - "id": 5, - "priority": 2, - "action": { "type": "redirect", "redirect": { "url": "chrome-extension://__MSG_@@extension_id__/popup.html" } }, - "condition": { - "urlFilter": "*://*.tk/*", - "resourceTypes": ["main_frame"], - "domainMatches": ["*.tk"] - } - }, - { - "id": 6, - "priority": 2, - "action": { "type": "redirect", "redirect": { "url": "chrome-extension://__MSG_@@extension_id__/popup.html" } }, - "condition": { - "urlFilter": "*://*.xyz/*", - "resourceTypes": ["main_frame"], - "domainMatches": ["*.xyz"] - } - } -] diff --git a/packages/extension/src/background/index.ts b/packages/extension/src/background/index.ts deleted file mode 100644 index 2f1e062..0000000 --- a/packages/extension/src/background/index.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { UrlCheckResult, UrlVerdict, ThreatInfo, BackgroundMessage, MessageType, SubscriptionTier, PhishingReport, ExtensionSettings } from '../types'; -import { urlCache, CACHE_TTL } from '../lib/cache'; -import { phishingDetector } from '../lib/phishing-detector'; -import { settingsManager } from '../lib/settings'; -import { shieldApiClient } from '../lib/api-client'; - -let threatsBlockedToday = 0; -let urlsCheckedToday = 0; -let lastThreat: ThreatInfo | null = null; - -chrome.runtime.onInstalled.addListener(async () => { - await urlCache.loadFromStorage(); - await settingsManager.load(); - - const stats = await chrome.storage.local.get('dailyStats'); - if (stats.dailyStats && stats.dailyStats.date === new Date().toDateString()) { - threatsBlockedToday = stats.dailyStats.threatsBlocked; - urlsCheckedToday = stats.dailyStats.urlsChecked; - } else { - threatsBlockedToday = 0; - urlsCheckedToday = 0; - await saveDailyStats(); - } - - chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((details) => { - chrome.storage.local.get('blockedRequests').then((data) => { - const blocked = data.blockedRequests || []; - blocked.push({ ruleId: details.rule?.ruleId || 0, url: details.request?.url || '', timestamp: Date.now() }); - if (blocked.length > 100) blocked.shift(); - chrome.storage.local.set({ blockedRequests: blocked }); - }); - }); -}); - -chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { - if (changeInfo.status !== 'loading' || !tab.url) return; - - const enabled = await settingsManager.isProtectionEnabled(); - if (!enabled) return; - - const url = tab.url; - if (url.startsWith('chrome://') || url.startsWith('chrome-extension://')) return; - - await analyzeAndAct(url, tabId); -}); - -async function analyzeAndAct(url: string, tabId: number): Promise { - urlsCheckedToday++; - const startTime = Date.now(); - - const allowed = await isShieldAIUrl(url); - if (allowed) return; - - const blocked = await settingsManager.isDomainBlocked(extractDomain(url)); - if (blocked) { - threatsBlockedToday++; - await showBlockedPage(tabId, url); - await saveDailyStats(); - return; - } - - const cached = await urlCache.get(url); - if (cached) { - broadcastResult(cached, tabId); - if (cached.verdict === UrlVerdict.PHISHING) { - const features = await settingsManager.getFeatures(); - if (features.activeBlocking) { - threatsBlockedToday++; - await showBlockedPage(tabId, url); - } else { - showWarningNotification(cached); - } - } - await saveDailyStats(); - return; - } - - const heuristic = phishingDetector.analyzeUrl(url); - let result: UrlCheckResult = { - url, - domain: extractDomain(url), - verdict: heuristic.verdict, - confidence: heuristic.score / 100, - threats: heuristic.threats, - cached: false, - latencyMs: Date.now() - startTime, - timestamp: Date.now(), - }; - - const apiResult = await shieldApiClient.checkUrl(url); - if (apiResult && apiResult.threats.length > 0) { - result = apiResult; - } - - const darkWatchEnabled = await settingsManager.get(); - if (darkWatchEnabled.darkWatchEnabled) { - const exposure = await shieldApiClient.checkDomainExposure(result.domain); - if (exposure && exposure.exposed) { - result.threats.push({ - type: 'credential_exposure' as any, - severity: 4, - source: 'darkwatch', - description: `Credentials for ${result.domain} found in ${exposure.sources.length} breach(es)`, - }); - if (result.verdict === UrlVerdict.SAFE) { - result.verdict = UrlVerdict.EXPOSED_CREDENTIALS; - } - } - } - - await urlCache.set(url, result); - broadcastResult(result, tabId); - - if (result.verdict === UrlVerdict.PHISHING) { - const features = await settingsManager.getFeatures(); - if (features.activeBlocking) { - threatsBlockedToday++; - await showBlockedPage(tabId, url); - } else { - showWarningNotification(result); - } - } - - if (result.threats.length > 0) { - lastThreat = result.threats[0]; - } - - await saveDailyStats(); -} - -function broadcastResult(result: UrlCheckResult, tabId: number): void { - chrome.tabs.sendMessage(tabId, { - type: MessageType.CHECK_URL_RESPONSE, - payload: result, - }).catch(() => {}); -} - -async function showBlockedPage(tabId: number, url: string): Promise { - const blockedUrl = chrome.runtime.getURL(`popup.html?blocked=${encodeURIComponent(url)}`); - await chrome.tabs.update(tabId, { url: blockedUrl }); -} - -async function showWarningNotification(result: UrlCheckResult): Promise { - const settings = await settingsManager.get(); - if (!settings.showNotifications) return; - await chrome.notifications.create({ - type: 'basic', - iconUrl: 'icons/icon48.png', - title: 'ShieldAI Warning', - message: `${result.verdict.toUpperCase()}: ${result.domain}`, - priority: result.verdict === UrlVerdict.PHISHING ? 2 : 0, - }); -} - -async function saveDailyStats(): Promise { - await chrome.storage.local.set({ - dailyStats: { - date: new Date().toDateString(), - threatsBlocked: threatsBlockedToday, - urlsChecked: urlsCheckedToday, - }, - }); - await urlCache.persistToStorage(); -} - -function extractDomain(url: string): string { - try { - return new URL(url).hostname; - } catch { - return url; - } -} - -async function isShieldAIUrl(url: string): Promise { - const settings = await settingsManager.get(); - const domain = extractDomain(url); - const apiDomain = new URL(settings.apiBaseUrl).hostname; - return domain === apiDomain || await settingsManager.isDomainAllowed(domain); -} - -chrome.runtime.onMessage.addListener( - (message: BackgroundMessage, sender, sendResponse) => { - handleMessage(message, sender).then((res) => { - if (res) sendResponse(res); - }); - return true; - } -); - -async function handleMessage( - message: BackgroundMessage, - sender: chrome.runtime.MessageSender -): Promise | void> { - switch (message.type) { - case MessageType.CHECK_URL: { - const url = message.payload?.url as string; - if (!url) return; - const tabId = sender.tab?.id || 0; - await analyzeAndAct(url, tabId); - break; - } - - case MessageType.GET_SETTINGS: - return { settings: await settingsManager.get() }; - - case MessageType.UPDATE_SETTINGS: - return { settings: await settingsManager.update(message.payload as Partial) }; - - case MessageType.REPORT_PHISHING: { - const payload = message.payload as Record | undefined; - if (!payload || typeof payload.url !== 'string' || typeof payload.pageTitle !== 'string') { - return { success: false, error: 'Missing url or pageTitle' }; - } - const report: PhishingReport = { - url: payload.url, - pageTitle: payload.pageTitle, - tabId: (payload.tabId as number) || 0, - timestamp: (payload.timestamp as number) || Date.now(), - reason: (payload.reason as string) || 'Manual report', - heuristics: (payload.heuristics as Record) || {}, - }; - const success = await shieldApiClient.submitPhishingReport(report); - return { success }; - } - - case MessageType.GET_POPUP_DATA: - return { - protectionEnabled: await settingsManager.isProtectionEnabled(), - tier: await settingsManager.getTier(), - threatsBlockedToday, - urlsCheckedToday, - lastThreat, - isLoggedIn: await settingsManager.isLoggedIn(), - }; - - case MessageType.TOGGLE_PROTECTION: { - const enabled = await settingsManager.toggleProtection(); - return { enabled }; - } - - case MessageType.AUTH_LOGOUT: { - await settingsManager.update({ authToken: null, userId: null, tier: null }); - return { success: true }; - } - } -} diff --git a/packages/extension/src/content/index.ts b/packages/extension/src/content/index.ts deleted file mode 100644 index 6e57d87..0000000 --- a/packages/extension/src/content/index.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { BackgroundMessage, MessageType, UrlCheckResult, UrlVerdict } from '../types'; - -let currentUrlVerdict: UrlVerdict | null = null; -let statusBar: HTMLElement | null = null; - -chrome.runtime.onMessage.addListener( - (message: BackgroundMessage) => { - switch (message.type) { - case MessageType.CHECK_URL_RESPONSE: { - const result = message.payload as UrlCheckResult; - currentUrlVerdict = result.verdict; - updateStatusBar(result); - injectPageBanner(result); - highlightSuspiciousLinks(result); - break; - } - } - } -); - -chrome.runtime.onInstalled.addListener(() => { - chrome.storage.sync.get('shieldaiSettings', (data) => { - if (data.shieldaiSettings?.enabled !== false) { - requestUrlCheck(); - } - }); -}); - -function requestUrlCheck(): void { - const url = window.location.href; - if (url.startsWith('chrome://') || url.startsWith('chrome-extension://')) return; - - chrome.runtime.sendMessage({ type: MessageType.CHECK_URL, payload: { url } }).catch(() => {}); -} - -function updateStatusBar(result: UrlCheckResult): void { - if (!statusBar) { - statusBar = document.createElement('div'); - statusBar.id = 'shieldai-status-bar'; - Object.assign(statusBar.style, { - position: 'fixed', - top: '0', - left: '0', - right: '0', - height: '3px', - zIndex: '2147483647', - transition: 'background-color 0.3s ease', - }); - document.documentElement.insertBefore(statusBar, document.documentElement.firstChild); - } - - const colors: Record = { - [UrlVerdict.SAFE]: '#22c55e', - [UrlVerdict.SUSPICIOUS]: '#f59e0b', - [UrlVerdict.PHISHING]: '#ef4444', - [UrlVerdict.SPAM]: '#f97316', - [UrlVerdict.EXPOSED_CREDENTIALS]: '#a855f7', - [UrlVerdict.UNKNOWN]: '#6b7280', - }; - - statusBar.style.backgroundColor = colors[result.verdict] || colors[UrlVerdict.UNKNOWN]; - statusBar.title = `ShieldAI: ${result.verdict} (${result.threats.length} threat${result.threats.length !== 1 ? 's' : ''})`; -} - -function injectPageBanner(result: UrlCheckResult): void { - const existing = document.getElementById('shieldai-banner'); - if (existing) existing.remove(); - - if (result.verdict === UrlVerdict.SAFE || result.verdict === UrlVerdict.UNKNOWN) return; - - const banner = document.createElement('div'); - banner.id = 'shieldai-banner'; - banner.innerHTML = ` -
      - 🛡️ - ShieldAI: ${result.verdict.toUpperCase()} — ${result.threats[0]?.description || 'Potential threat detected'} - -
      - `; - - Object.assign(banner.style, { - position: 'fixed', - top: '3px', - left: '0', - right: '0', - zIndex: '2147483646', - backgroundColor: result.verdict === UrlVerdict.PHISHING ? '#fef2f2' : '#fffbeb', - borderBottom: `2px solid ${result.verdict === UrlVerdict.PHISHING ? '#ef4444' : '#f59e0b'}`, - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', - fontSize: '13px', - color: '#374151', - }); - - const content = banner.querySelector('#shieldai-banner-content') as HTMLElement; - Object.assign(content.style, { - maxWidth: '800px', - margin: '0 auto', - padding: '8px 16px', - }); - - document.documentElement.insertBefore(banner, document.documentElement.firstChild.nextSibling); - - banner.querySelector('#shieldai-dismiss')?.addEventListener('click', () => { - banner.remove(); - }); -} - -function highlightSuspiciousLinks(result: UrlCheckResult): void { - if (result.verdict === UrlVerdict.SAFE) return; - - const links = document.querySelectorAll('a[href]'); - links.forEach((link) => { - const href = link.getAttribute('href'); - if (!href) return; - - try { - const linkDomain = new URL(href, window.location.href).hostname; - const pageDomain = window.location.hostname; - - if (linkDomain !== pageDomain && !linkDomain.includes(pageDomain)) { - link.classList.add('shieldai-external-link'); - link.title = `ShieldAI: External link → ${linkDomain}`; - } - } catch { - // Relative or malformed URL - } - }); - - const style = document.createElement('style'); - style.id = 'shieldai-link-styles'; - style.textContent = ` - a.shieldai-external-link::after { - content: " ↗"; - opacity: 0.5; - font-size: 0.8em; - } - `; - document.head.appendChild(style); -} - -requestUrlCheck(); diff --git a/packages/extension/src/lib/api-client.ts b/packages/extension/src/lib/api-client.ts deleted file mode 100644 index c368bfc..0000000 --- a/packages/extension/src/lib/api-client.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { UrlCheckResult, UrlVerdict, ThreatInfo, PhishingReport, SubscriptionTier } from '../types'; -import { settingsManager } from './settings'; -import { API_TIMEOUT } from './cache'; - -export class ShieldApiClient { - private baseUrl: string = ''; - - async checkUrl(url: string): Promise { - const settings = await settingsManager.get(); - const token = settings.authToken; - if (!token) return null; - - const startTime = Date.now(); - try { - const response = await fetch(`${settings.apiBaseUrl}/extension/url-check`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ url }), - signal: AbortSignal.timeout(API_TIMEOUT), - }); - - if (!response.ok) return null; - - const data = await response.json(); - const latency = Date.now() - startTime; - - return { - url, - domain: new URL(url).hostname, - verdict: data.verdict || UrlVerdict.UNKNOWN, - confidence: data.confidence || 0, - threats: (data.threats || []) as ThreatInfo[], - cached: false, - latencyMs: latency, - timestamp: Date.now(), - }; - } catch { - return null; - } - } - - async checkDomainExposure(domain: string): Promise<{ exposed: boolean; sources: string[] } | null> { - const settings = await settingsManager.get(); - const token = settings.authToken; - if (!token || !settings.darkWatchEnabled) return null; - - try { - const response = await fetch(`${settings.apiBaseUrl}/darkwatch/exposures/check`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify({ domain }), - signal: AbortSignal.timeout(API_TIMEOUT), - }); - - if (!response.ok) return null; - return response.json(); - } catch { - return null; - } - } - - async submitPhishingReport(report: PhishingReport): Promise { - const settings = await settingsManager.get(); - const token = settings.authToken; - if (!token) return false; - - try { - const response = await fetch(`${settings.apiBaseUrl}/extension/phishing-report`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - body: JSON.stringify(report), - signal: AbortSignal.timeout(3000), - }); - return response.ok; - } catch { - return false; - } - } - - async authenticate(apiKey: string): Promise<{ userId: string; tier: SubscriptionTier } | null> { - try { - const settings = await settingsManager.get(); - const response = await fetch(`${settings.apiBaseUrl}/extension/auth`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}`, - }, - signal: AbortSignal.timeout(5000), - }); - - if (!response.ok) return null; - return response.json(); - } catch { - return null; - } - } - - async getProtectionStats(): Promise<{ - threatsBlockedToday: number; - urlsCheckedToday: number; - } | null> { - const settings = await settingsManager.get(); - const token = settings.authToken; - if (!token) return null; - - try { - const response = await fetch(`${settings.apiBaseUrl}/extension/stats`, { - headers: { - 'Authorization': `Bearer ${token}`, - }, - signal: AbortSignal.timeout(3000), - }); - - if (!response.ok) return null; - return response.json(); - } catch { - return null; - } - } -} - -export const shieldApiClient = new ShieldApiClient(); diff --git a/packages/extension/src/lib/cache.ts b/packages/extension/src/lib/cache.ts deleted file mode 100644 index dd1197e..0000000 --- a/packages/extension/src/lib/cache.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { UrlCheckResult } from '../types'; - -const CACHE_TTL_MS = 5 * 60 * 1000; -const API_TIMEOUT_MS = 500; -const MAX_CACHE_SIZE = 5000; - -export class UrlCache { - private cache: Map = new Map(); - - async get(url: string): Promise { - const normalized = this.normalizeUrl(url); - const entry = this.cache.get(normalized); - - if (!entry) return null; - - if (Date.now() > entry.expiresAt) { - this.cache.delete(normalized); - return null; - } - - return { ...entry.result, cached: true }; - } - - async set(url: string, result: UrlCheckResult): Promise { - const normalized = this.normalizeUrl(url); - - if (this.cache.size >= MAX_CACHE_SIZE) { - const firstKey = this.cache.keys().next().value; - if (firstKey) this.cache.delete(firstKey); - } - - this.cache.set(normalized, { - result, - expiresAt: Date.now() + CACHE_TTL_MS, - }); - } - - async persistToStorage(): Promise { - const entries: Record = {}; - for (const [key, value] of this.cache.entries()) { - entries[key] = value; - } - await chrome.storage.local.set({ urlCache: entries }); - } - - async loadFromStorage(): Promise { - const data = await chrome.storage.local.get('urlCache') as { urlCache: Record }; - if (data.urlCache) { - const now = Date.now(); - for (const [key, entry] of Object.entries(data.urlCache)) { - if (now <= entry.expiresAt) { - this.cache.set(key, entry); - } - } - } - } - - getStats(): { size: number; max: number } { - return { size: this.cache.size, max: MAX_CACHE_SIZE }; - } - - clear(): void { - this.cache.clear(); - } - - private normalizeUrl(url: string): string { - try { - const u = new URL(url); - u.hash = ''; - u.search = ''; - return u.toString(); - } catch { - return url.toLowerCase(); - } - } -} - -export const urlCache = new UrlCache(); -export const CACHE_TTL = CACHE_TTL_MS; -export const API_TIMEOUT = API_TIMEOUT_MS; diff --git a/packages/extension/src/lib/phishing-detector.ts b/packages/extension/src/lib/phishing-detector.ts deleted file mode 100644 index b16cadf..0000000 --- a/packages/extension/src/lib/phishing-detector.ts +++ /dev/null @@ -1,277 +0,0 @@ -import { ThreatType, UrlVerdict, ThreatInfo } from '../types'; - -export class PhishingDetector { - private knownSuspiciousTlds: Set = new Set([ - '.tk', '.ml', '.ga', '.cf', '.gq', '.xyz', '.top', '.click', '.link', '.work', - ]); - - private commonBrands: Map = new Map([ - ['google', ['gmail', 'drive', 'docs', 'maps', 'play', 'chrome', 'youtube']], - ['apple', ['icloud', 'appstore', 'icloud_content', 'appleid']], - ['amazon', ['aws', 'amazonaws', 'amazon-adsystem', 'prime-video']], - ['microsoft', ['office', 'outlook', 'onedrive', 'teams', 'azure', 'windows']], - ['facebook', ['fb', 'fbcdn', 'instagram', 'whatsapp', 'messenger']], - ['paypal', ['paypalobjects', 'paypal-web', 'xoom']], - ['netflix', ['nflximg', 'nflxso', 'nflxvideo', 'nflxext']], - ['bank', ['chase', 'bofa', 'wellsfargo', 'citi', 'hsbc', 'barclays']], - ]); - - analyzeUrl(url: string): { verdict: UrlVerdict; threats: ThreatInfo[]; score: number } { - const threats: ThreatInfo[] = []; - let score = 0; - - try { - const parsed = new URL(url); - const hostname = parsed.hostname.toLowerCase(); - const domainParts = hostname.split('.'); - const tld = domainParts[domainParts.length - 1]; - - score += this.checkTld(tld, domainParts, threats); - score += this.checkEntropy(parsed.pathname + parsed.search, threats); - score += this.checkTyposquatting(hostname, threats); - score += this.checkIpAddress(hostname, threats); - score += this.checkLongUrl(url, threats); - score += this.checkSubdomainDepth(domainParts, threats); - score += this.checkHttpsProtocol(parsed.protocol, threats); - score += this.checkRedirectPatterns(parsed.search, threats); - score += this.checkEncodedChars(url, threats); - score += this.checkBrandImpersonation(hostname, threats); - } catch { - return { - verdict: UrlVerdict.UNKNOWN, - threats: [{ type: ThreatType.PHISHING_HEURISTIC, severity: 3, source: 'heuristic', description: 'Malformed URL' }], - score: 30, - }; - } - - const verdict = score >= 70 ? UrlVerdict.PHISHING - : score >= 40 ? UrlVerdict.SUSPICIOUS - : score >= 20 ? UrlVerdict.SPAM - : UrlVerdict.SAFE; - - return { verdict, threats, score }; - } - - private checkTld(tld: string, parts: string[], threats: ThreatInfo[]): number { - if (this.knownSuspiciousTlds.has(`.${tld}`)) { - threats.push({ - type: ThreatType.DOMAIN_AGE, - severity: 4, - source: 'heuristic', - description: `Suspicious TLD: .${tld}`, - }); - return 25; - } - if (parts.length === 1) { - threats.push({ - type: ThreatType.DOMAIN_AGE, - severity: 3, - source: 'heuristic', - description: 'Single-label domain (possible local or new domain)', - }); - return 15; - } - return 0; - } - - private checkEntropy(pathname: string, threats: ThreatInfo[]): number { - if (!pathname || pathname.length < 20) return 0; - const entropy = this.calculateEntropy(pathname); - if (entropy > 4.5) { - threats.push({ - type: ThreatType.URL_ENTROPY, - severity: 4, - source: 'heuristic', - description: `High URL path entropy (${entropy.toFixed(2)}) suggests obfuscation`, - }); - return 20; - } - return 0; - } - - private checkTyposquatting(hostname: string, threats: ThreatInfo[]): number { - for (const [brand, subdomains] of this.commonBrands) { - const brandParts = hostname.split('.'); - const mainDomain = brandParts.slice(0, -1).join('.'); - const firstLabel = mainDomain.split('.')[0]; - - if (mainDomain.includes(brand) && mainDomain !== brand) { - const editDist = this.levenshteinDistance(firstLabel, brand); - if (editDist <= 2 && editDist > 0) { - threats.push({ - type: ThreatType.TYPOSQUAT, - severity: 5, - source: 'heuristic', - description: `Possible typosquat of "${brand}" (edit distance: ${editDist})`, - }); - return 35; - } - } - - const editDist = this.levenshteinDistance(firstLabel, brand); - if (editDist <= 2 && editDist > 0 && firstLabel.length >= brand.length - 1) { - threats.push({ - type: ThreatType.TYPOSQUAT, - severity: 5, - source: 'heuristic', - description: `Possible typosquat of "${brand}" (edit distance: ${editDist})`, - }); - return 35; - } - - for (const sub of subdomains) { - if (hostname.includes(sub) && !hostname.startsWith(`${sub}.` + brandParts[brandParts.length - 1])) { - threats.push({ - type: ThreatType.TYPOSQUAT, - severity: 3, - source: 'heuristic', - description: `Domain contains "${sub}" but not an official ${brand} subdomain`, - }); - return 15; - } - } - } - return 0; - } - - private checkIpAddress(hostname: string, threats: ThreatInfo[]): number { - const ipPattern = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; - if (ipPattern.test(hostname) && hostname !== '127.0.0.1' && hostname !== 'localhost') { - threats.push({ - type: ThreatType.PHISHING_HEURISTIC, - severity: 4, - source: 'heuristic', - description: `IP address used as hostname: ${hostname}`, - }); - return 25; - } - return 0; - } - - private checkLongUrl(url: string, threats: ThreatInfo[]): number { - if (url.length > 200) { - threats.push({ - type: ThreatType.PHISHING_HEURISTIC, - severity: 3, - source: 'heuristic', - description: `Unusually long URL (${url.length} chars)`, - }); - return 15; - } - return 0; - } - - private checkSubdomainDepth(parts: string[], threats: ThreatInfo[]): number { - if (parts.length > 5) { - threats.push({ - type: ThreatType.PHISHING_HEURISTIC, - severity: 3, - source: 'heuristic', - description: `Deep subdomain nesting (${parts.length} levels)`, - }); - return 15; - } - return 0; - } - - private checkHttpsProtocol(protocol: string, threats: ThreatInfo[]): number { - if (protocol === 'http:') { - threats.push({ - type: ThreatType.MIXED_CONTENT, - severity: 2, - source: 'heuristic', - description: 'Page loaded over HTTP (not HTTPS)', - }); - return 10; - } - return 0; - } - - private checkRedirectPatterns(query: string, threats: ThreatInfo[]): number { - const redirectParams = ['redirect', 'url', 'dest', 'return', 'next', 'target']; - let count = 0; - for (const param of redirectParams) { - if (query.includes(`${param}=`)) count++; - } - if (count >= 2) { - threats.push({ - type: ThreatType.REDIRECT_CHAIN, - severity: 3, - source: 'heuristic', - description: `Multiple redirect parameters detected (${count})`, - }); - return 15; - } - return 0; - } - - private checkEncodedChars(url: string, threats: ThreatInfo[]): number { - const encodedPattern = /(%[0-9a-fA-F]{2}){3,}/g; - const matches = url.match(encodedPattern); - if (matches && matches.length > 0) { - threats.push({ - type: ThreatType.URL_ENTROPY, - severity: 3, - source: 'heuristic', - description: 'Excessive URL encoding detected', - }); - return 15; - } - return 0; - } - - private checkBrandImpersonation(hostname: string, threats: ThreatInfo[]): number { - const impersonationPatterns = [ - /login[-_]?(secure|portal|page|form)/i, - /account[-_]?(verify|confirm|update)/i, - /secure[-_]?(signin|auth|login)/i, - /wallet[-_]?(connect|link|verify)/i, - ]; - for (const pattern of impersonationPatterns) { - if (pattern.test(hostname)) { - threats.push({ - type: ThreatType.PHISHING_HEURISTIC, - severity: 4, - source: 'heuristic', - description: `Common phishing pattern detected: ${hostname}`, - }); - return 20; - } - } - return 0; - } - - private calculateEntropy(str: string): number { - const freq: Record = {}; - for (const char of str) { - freq[char] = (freq[char] || 0) + 1; - } - let entropy = 0; - const len = str.length; - for (const count of Object.values(freq)) { - const p = count / len; - entropy -= p * Math.log2(p); - } - return entropy; - } - - private levenshteinDistance(a: string, b: string): number { - const matrix: number[][] = []; - for (let i = 0; i <= b.length; i++) matrix[i] = [i]; - for (let j = 0; j <= a.length; j++) matrix[0][j] = j; - for (let i = 1; i <= b.length; i++) { - for (let j = 1; j <= a.length; j++) { - matrix[i][j] = b[i - 1] === a[j - 1] - ? matrix[i - 1][j - 1] - : Math.min( - matrix[i - 1][j - 1] + 1, - matrix[i][j - 1] + 1, - matrix[i - 1][j] + 1, - ); - } - } - return matrix[b.length][a.length]; - } -} - -export const phishingDetector = new PhishingDetector(); diff --git a/packages/extension/src/lib/settings.ts b/packages/extension/src/lib/settings.ts deleted file mode 100644 index 5a97e75..0000000 --- a/packages/extension/src/lib/settings.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { ExtensionSettings, SubscriptionTier, TIER_FEATURES, MessageType } from '../types'; - -const DEFAULT_SETTINGS: ExtensionSettings = { - apiKey: '', - apiBaseUrl: 'https://api.shieldai.com', - authToken: null, - userId: null, - tier: null, - enabled: true, - activeBlocking: false, - darkWatchEnabled: false, - spamProtectionEnabled: true, - showNotifications: true, - blockedDomains: [], - allowedDomains: [], - lastSyncAt: null, -}; - -export class SettingsManager { - private settings: ExtensionSettings = { ...DEFAULT_SETTINGS }; - private loaded = false; - - async load(): Promise { - if (this.loaded) return this.settings; - - const stored = await chrome.storage.sync.get('shieldaiSettings'); - if (stored.shieldaiSettings) { - this.settings = { ...DEFAULT_SETTINGS, ...stored.shieldaiSettings }; - } - this.loaded = true; - return this.settings; - } - - async get(): Promise { - if (!this.loaded) await this.load(); - return { ...this.settings }; - } - - async update(partial: Partial): Promise { - await this.load(); - this.settings = { ...this.settings, ...partial }; - await chrome.storage.sync.set({ shieldaiSettings: this.settings }); - return { ...this.settings }; - } - - async getAuthToken(): Promise { - await this.load(); - return this.settings.authToken; - } - - async isLoggedIn(): Promise { - await this.load(); - return this.settings.authToken !== null && this.settings.userId !== null; - } - - async getTier(): Promise { - await this.load(); - return this.settings.tier; - } - - async getFeatures(): Promise { - const tier = await this.getTier(); - if (tier) return TIER_FEATURES[tier]; - return TIER_FEATURES[SubscriptionTier.BASIC]; - } - - async isDomainBlocked(domain: string): Promise { - await this.load(); - return this.settings.blockedDomains.some( - (d) => d.toLowerCase() === domain.toLowerCase() - ); - } - - async isDomainAllowed(domain: string): Promise { - await this.load(); - return this.settings.allowedDomains.some( - (d) => d.toLowerCase() === domain.toLowerCase() - ); - } - - async isProtectionEnabled(): Promise { - await this.load(); - return this.settings.enabled; - } - - async toggleProtection(): Promise { - await this.load(); - this.settings.enabled = !this.settings.enabled; - await chrome.storage.sync.set({ shieldaiSettings: this.settings }); - return this.settings.enabled; - } - - async addBlockedDomain(domain: string): Promise { - await this.load(); - const lower = domain.toLowerCase(); - if (!this.settings.blockedDomains.includes(lower)) { - this.settings.blockedDomains.push(lower); - await chrome.storage.sync.set({ shieldaiSettings: this.settings }); - } - } - - async removeBlockedDomain(domain: string): Promise { - await this.load(); - this.settings.blockedDomains = this.settings.blockedDomains.filter( - (d) => d !== domain.toLowerCase() - ); - await chrome.storage.sync.set({ shieldaiSettings: this.settings }); - } - - async reset(): Promise { - this.settings = { ...DEFAULT_SETTINGS }; - this.loaded = true; - await chrome.storage.sync.set({ shieldaiSettings: this.settings }); - } -} - -export const settingsManager = new SettingsManager(); diff --git a/packages/extension/src/options/options.html b/packages/extension/src/options/options.html deleted file mode 100644 index fbac82f..0000000 --- a/packages/extension/src/options/options.html +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - ShieldAI Options - - - -

      🛡️ ShieldAI Options

      -

      Configure your phishing & spam protection

      - -
      -
      Connection
      -
      - - -
      -
      - - -
      -
      - -
      -
      Protection Settings
      -
      - - -
      -
      - - -
      -
      - - -
      -
      - - -
      -
      - - -
      -
      - -
      -
      Blocked Domains
      -
        -
        - - -
        -
        - -
        -
        Allowed Domains (Whitelist)
        -
          -
          - - -
          -
          - -
          - - -
          - -
          Settings saved!
          - - - - diff --git a/packages/extension/src/options/options.ts b/packages/extension/src/options/options.ts deleted file mode 100644 index 3cea5ec..0000000 --- a/packages/extension/src/options/options.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { BackgroundMessage, MessageType } from '../types'; - -const apiUrlInput = document.getElementById('api-url') as HTMLInputElement; -const authTokenInput = document.getElementById('auth-token') as HTMLInputElement; -const enabledCheckbox = document.getElementById('enabled') as HTMLInputElement; -const activeBlockingCheckbox = document.getElementById('active-blocking') as HTMLInputElement; -const darkwatchCheckbox = document.getElementById('darkwatch-enabled') as HTMLInputElement; -const spamCheckbox = document.getElementById('spam-enabled') as HTMLInputElement; -const notificationsCheckbox = document.getElementById('notifications') as HTMLInputElement; -const blockedDomainsList = document.getElementById('blocked-domains') as HTMLElement; -const allowedDomainsList = document.getElementById('allowed-domains') as HTMLElement; -const newBlockedInput = document.getElementById('new-blocked-domain') as HTMLInputElement; -const newAllowedInput = document.getElementById('new-allowed-domain') as HTMLInputElement; -const saveBtn = document.getElementById('save-btn') as HTMLButtonElement; -const resetBtn = document.getElementById('reset-btn') as HTMLButtonElement; -const toast = document.getElementById('toast') as HTMLElement; - -loadSettings(); - -function loadSettings(): void { - chrome.runtime.sendMessage({ type: MessageType.GET_SETTINGS }, (response) => { - const settings = (response as { settings: Record }).settings; - if (!settings) return; - - apiUrlInput.value = settings.apiBaseUrl || 'https://api.shieldai.com'; - authTokenInput.value = settings.authToken || ''; - enabledCheckbox.checked = settings.enabled !== false; - activeBlockingCheckbox.checked = !!settings.activeBlocking; - darkwatchCheckbox.checked = !!settings.darkWatchEnabled; - spamCheckbox.checked = settings.spamProtectionEnabled !== false; - notificationsCheckbox.checked = settings.showNotifications !== false; - - renderDomainList(blockedDomainsList, (settings.blockedDomains || []) as string[], 'blocked'); - renderDomainList(allowedDomainsList, (settings.allowedDomains || []) as string[], 'allowed'); - }); -} - -function renderDomainList(container: HTMLElement, domains: string[], type: string): void { - container.innerHTML = domains.map((d, i) => ` -
        • - ${d} - -
        • - `).join(''); -} - -saveBtn.addEventListener('click', () => { - chrome.runtime.sendMessage({ - type: MessageType.UPDATE_SETTINGS, - payload: { - apiBaseUrl: apiUrlInput.value, - authToken: authTokenInput.value || null, - enabled: enabledCheckbox.checked, - activeBlocking: activeBlockingCheckbox.checked, - darkWatchEnabled: darkwatchCheckbox.checked, - spamProtectionEnabled: spamCheckbox.checked, - showNotifications: notificationsCheckbox.checked, - }, - }, () => { - showToast('Settings saved!'); - }); -}); - -resetBtn.addEventListener('click', () => { - chrome.storage.sync.set({ shieldaiSettings: null }, () => { - chrome.runtime.sendMessage({ type: MessageType.GET_SETTINGS }, (response) => { - loadSettings(); - showToast('Settings reset to defaults'); - }); - }); -}); - -document.getElementById('add-blocked')?.addEventListener('click', () => { - const domain = newBlockedInput.value.trim().toLowerCase(); - if (!domain) return; - - chrome.runtime.sendMessage({ type: MessageType.GET_SETTINGS }, (response) => { - const settings = (response as { settings: Record }).settings; - const domains = [...(settings.blockedDomains || []), domain]; - chrome.runtime.sendMessage({ - type: MessageType.UPDATE_SETTINGS, - payload: { blockedDomains: domains }, - }, () => { - newBlockedInput.value = ''; - renderDomainList(blockedDomainsList, domains, 'blocked'); - showToast(`Added ${domain} to blocked list`); - }); - }); -}); - -document.getElementById('add-allowed')?.addEventListener('click', () => { - const domain = newAllowedInput.value.trim().toLowerCase(); - if (!domain) return; - - chrome.runtime.sendMessage({ type: MessageType.GET_SETTINGS }, (response) => { - const settings = (response as { settings: Record }).settings; - const domains = [...(settings.allowedDomains || []), domain]; - chrome.runtime.sendMessage({ - type: MessageType.UPDATE_SETTINGS, - payload: { allowedDomains: domains }, - }, () => { - newAllowedInput.value = ''; - renderDomainList(allowedDomainsList, domains, 'allowed'); - showToast(`Added ${domain} to allowed list`); - }); - }); -}); - -function showToast(message: string): void { - toast.textContent = message; - toast.classList.add('show'); - setTimeout(() => toast.classList.remove('show'), 3000); -} diff --git a/packages/extension/src/popup/popup.html b/packages/extension/src/popup/popup.html deleted file mode 100644 index 29afc1e..0000000 --- a/packages/extension/src/popup/popup.html +++ /dev/null @@ -1,271 +0,0 @@ - - - - - - ShieldAI Protection - - - - - - - - - - diff --git a/packages/extension/src/popup/popup.ts b/packages/extension/src/popup/popup.ts deleted file mode 100644 index 217bb58..0000000 --- a/packages/extension/src/popup/popup.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { BackgroundMessage, MessageType, PopupData } from '../types'; - -const popupView = document.getElementById('popup-view') as HTMLElement; -const blockedView = document.getElementById('blocked-view') as HTMLElement; - -const protectionToggle = document.getElementById('protection-toggle') as HTMLElement; -const statusText = document.getElementById('status-text') as HTMLElement; -const accountStatus = document.getElementById('account-status') as HTMLElement; -const tierBadge = document.getElementById('tier-badge') as HTMLElement; -const threatsCount = document.getElementById('threats-count') as HTMLElement; -const urlsCount = document.getElementById('urls-count') as HTMLElement; -const lastThreat = document.getElementById('last-threat') as HTMLElement; -const threatDescription = document.getElementById('threat-description') as HTMLElement; -const blockingBadge = document.getElementById('blocking-badge') as HTMLElement; -const darkwatchBadge = document.getElementById('darkwatch-badge') as HTMLElement; -const realtimeBadge = document.getElementById('realtime-badge') as HTMLElement; - -const reportBtn = document.getElementById('report-btn') as HTMLButtonElement; -const optionsBtn = document.getElementById('options-btn') as HTMLButtonElement; -const loginBtn = document.getElementById('login-btn') as HTMLButtonElement; -const continueBtn = document.getElementById('continue-btn') as HTMLButtonElement; -const backBtn = document.getElementById('back-btn') as HTMLButtonElement; - -checkBlockedUrl(); -loadPopupData(); - -function checkBlockedUrl(): void { - const params = new URLSearchParams(window.location.search); - const blockedUrl = params.get('blocked'); - if (blockedUrl) { - popupView.classList.add('hidden'); - blockedView.classList.remove('hidden'); - document.getElementById('blocked-url')!.textContent = blockedUrl; - - continueBtn.onclick = () => { - chrome.tabs.update({ url: blockedUrl }); - }; - backBtn.onclick = () => { - chrome.tabs.goBack(); - }; - } -} - -function loadPopupData(): void { - chrome.runtime.sendMessage({ type: MessageType.GET_POPUP_DATA }, (response) => { - const data = response as PopupData; - updateUI(data); - }); -} - -function updateUI(data: PopupData): void { - statusText.textContent = data.protectionEnabled ? 'Active' : 'Paused'; - statusText.className = `status-value ${data.protectionEnabled ? 'safe' : 'warning'}`; - protectionToggle.className = `toggle ${data.protectionEnabled ? 'active' : ''}`; - - accountStatus.textContent = data.isLoggedIn ? 'Connected' : 'Guest'; - accountStatus.className = `status-value ${data.isLoggedIn ? 'safe' : ''}`; - - const tier = data.tier || 'basic'; - tierBadge.textContent = tier.charAt(0).toUpperCase() + tier.slice(1); - tierBadge.className = `tier-badge ${tier}`; - - threatsCount.textContent = data.threatsBlockedToday.toLocaleString(); - urlsCount.textContent = data.urlsCheckedToday.toLocaleString(); - - if (data.lastThreat) { - lastThreat.classList.remove('hidden'); - threatDescription.textContent = data.lastThreat.description; - } - - if (data.tier === 'plus' || data.tier === 'premium') { - blockingBadge.textContent = 'Active'; - blockingBadge.className = 'tier-badge plus'; - } - - if (data.tier === 'premium') { - darkwatchBadge.textContent = 'Active'; - darkwatchBadge.className = 'tier-badge plus'; - realtimeBadge.textContent = 'Active'; - realtimeBadge.className = 'tier-badge premium'; - } -} - -protectionToggle.addEventListener('click', () => { - chrome.runtime.sendMessage({ type: MessageType.TOGGLE_PROTECTION }, (response) => { - const enabled = (response as { enabled: boolean }).enabled; - protectionToggle.className = `toggle ${enabled ? 'active' : ''}`; - statusText.textContent = enabled ? 'Active' : 'Paused'; - statusText.className = `status-value ${enabled ? 'safe' : 'warning'}`; - }); -}); - -reportBtn.addEventListener('click', async () => { - const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); - if (!tab?.url) return; - - const title = tab.title || 'Unknown Page'; - const success = await chrome.runtime.sendMessage({ - type: MessageType.REPORT_PHISHING, - payload: { - url: tab.url, - pageTitle: title, - tabId: tab.id, - timestamp: Date.now(), - reason: 'Manual report from popup', - heuristics: {}, - }, - }); - - reportBtn.textContent = (success as { success: boolean })?.success - ? '✓ Reported' - : '⚡ Report Phishing'; - setTimeout(() => { reportBtn.innerHTML = ' Report Phishing'; }, 2000); -}); - -optionsBtn.addEventListener('click', () => { - chrome.tabs.create({ url: chrome.runtime.getURL('options.html') }); -}); - -loginBtn.addEventListener('click', () => { - chrome.tabs.create({ url: 'https://app.shieldai.com/auth/login?extension=true' }); -}); diff --git a/packages/extension/src/types/index.ts b/packages/extension/src/types/index.ts deleted file mode 100644 index 7a3ea9c..0000000 --- a/packages/extension/src/types/index.ts +++ /dev/null @@ -1,138 +0,0 @@ -export interface UrlCheckResult { - url: string; - domain: string; - verdict: UrlVerdict; - confidence: number; - threats: ThreatInfo[]; - cached: boolean; - latencyMs: number; - timestamp: number; -} - -export enum UrlVerdict { - SAFE = 'safe', - SUSPICIOUS = 'suspicious', - PHISHING = 'phishing', - SPAM = 'spam', - EXPOSED_CREDENTIALS = 'exposed_credentials', - UNKNOWN = 'unknown', -} - -export interface ThreatInfo { - type: ThreatType; - severity: number; - source: string; - description: string; -} - -export enum ThreatType { - PHISHING_KNOWN = 'phishing_known', - PHISHING_HEURISTIC = 'phishing_heuristic', - DOMAIN_AGE = 'domain_age', - SSL_ANOMALY = 'ssl_anomaly', - URL_ENTROPY = 'url_entropy', - TYPOSQUAT = 'typosquat', - CREDENTIAL_EXPOSURE = 'credential_exposure', - SPAM_SOURCE = 'spam_source', - REDIRECT_CHAIN = 'redirect_chain', - MIXED_CONTENT = 'mixed_content', -} - -export interface CachedUrlEntry { - url: string; - result: UrlCheckResult; - expiresAt: number; -} - -export interface ExtensionSettings { - apiKey: string; - apiBaseUrl: string; - authToken: string | null; - userId: string | null; - tier: SubscriptionTier | null; - enabled: boolean; - activeBlocking: boolean; - darkWatchEnabled: boolean; - spamProtectionEnabled: boolean; - showNotifications: boolean; - blockedDomains: string[]; - allowedDomains: string[]; - lastSyncAt: number | null; -} - -export enum SubscriptionTier { - BASIC = 'basic', - PLUS = 'plus', - PREMIUM = 'premium', -} - -export interface TierFeatures { - passiveWarnings: boolean; - activeBlocking: boolean; - darkWatchIntegration: boolean; - realTimeScanning: boolean; - maxDailyChecks: number; -} - -export const TIER_FEATURES: Record = { - [SubscriptionTier.BASIC]: { - passiveWarnings: true, - activeBlocking: false, - darkWatchIntegration: false, - realTimeScanning: false, - maxDailyChecks: 100, - }, - [SubscriptionTier.PLUS]: { - passiveWarnings: true, - activeBlocking: true, - darkWatchIntegration: true, - realTimeScanning: false, - maxDailyChecks: 1000, - }, - [SubscriptionTier.PREMIUM]: { - passiveWarnings: true, - activeBlocking: true, - darkWatchIntegration: true, - realTimeScanning: true, - maxDailyChecks: 10000, - }, -}; - -export interface PhishingReport { - url: string; - pageTitle: string; - tabId: number; - timestamp: number; - reason: string; - heuristics: Record; -} - -export interface PopupData { - protectionEnabled: boolean; - tier: SubscriptionTier | null; - threatsBlockedToday: number; - urlsCheckedToday: number; - lastThreat: ThreatInfo | null; - isLoggedIn: boolean; -} - -export interface BackgroundMessage { - type: MessageType; - payload?: Record; -} - -export enum MessageType { - CHECK_URL = 'check_url', - CHECK_URL_RESPONSE = 'check_url_response', - GET_SETTINGS = 'get_settings', - UPDATE_SETTINGS = 'update_settings', - REPORT_PHISHING = 'report_phishing', - AUTH_LOGIN = 'auth_login', - AUTH_LOGOUT = 'auth_logout', - GET_POPUP_DATA = 'get_popup_data', - POPUP_DATA_RESPONSE = 'popup_data_response', - DARKWATCH_CHECK = 'darkwatch_check', - DARKWATCH_RESPONSE = 'darkwatch_response', - TOGGLE_PROTECTION = 'toggle_protection', - REFRESH_TOKEN = 'refresh_token', -} diff --git a/packages/extension/tests/cache.test.ts b/packages/extension/tests/cache.test.ts deleted file mode 100644 index 35b2fb8..0000000 --- a/packages/extension/tests/cache.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, it, expect, beforeEach } from 'vitest'; -import { urlCache } from '../src/lib/cache'; -import { UrlCheckResult, UrlVerdict } from '../src/types'; - -describe('UrlCache', () => { - const sampleResult: UrlCheckResult = { - url: 'https://example.com', - domain: 'example.com', - verdict: UrlVerdict.SAFE, - confidence: 0.95, - threats: [], - cached: false, - latencyMs: 50, - timestamp: Date.now(), - }; - - beforeEach(async () => { - urlCache.clear(); - }); - - it('should return null for missing URL', async () => { - const result = await urlCache.get('https://missing.com'); - expect(result).toBeNull(); - }); - - it('should store and retrieve cached result', async () => { - await urlCache.set('https://example.com', sampleResult); - const cached = await urlCache.get('https://example.com'); - expect(cached).not.toBeNull(); - expect(cached!.cached).toBe(true); - expect(cached!.verdict).toBe(UrlVerdict.SAFE); - }); - - it('should normalize URLs by stripping hash and search', async () => { - await urlCache.set('https://example.com/page?foo=bar#section', sampleResult); - const cached = await urlCache.get('https://example.com/page'); - expect(cached).not.toBeNull(); - }); - - it('should persist and restore from storage', async () => { - await urlCache.set('https://test.com', sampleResult); - await urlCache.persistToStorage(); - urlCache.clear(); - await urlCache.loadFromStorage(); - const cached = await urlCache.get('https://test.com'); - expect(cached).not.toBeNull(); - }); - - it('should evict oldest entry when at max capacity', async () => { - const stats = urlCache.getStats(); - expect(stats.max).toBe(5000); - }); - - it('should handle malformed URLs gracefully', async () => { - await urlCache.set('not-a-url', sampleResult); - const cached = await urlCache.get('not-a-url'); - expect(cached).not.toBeNull(); - }); -}); diff --git a/packages/extension/tests/phishing-detector.test.ts b/packages/extension/tests/phishing-detector.test.ts deleted file mode 100644 index cb1d644..0000000 --- a/packages/extension/tests/phishing-detector.test.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { phishingDetector } from '../src/lib/phishing-detector'; -import { UrlVerdict, ThreatType } from '../src/types'; - -describe('PhishingDetector', () => { - const detector = phishingDetector; - - describe('analyzeUrl', () => { - it('should return SAFE for legitimate URLs', () => { - const result = detector.analyzeUrl('https://www.google.com/search?q=test'); - expect(result.verdict).toBe(UrlVerdict.SAFE); - expect(result.score).toBeLessThan(20); - }); - - it('should detect suspicious TLD', () => { - const result = detector.analyzeUrl('https://free-prize.tk/claim'); - expect(result.threats.some((t) => t.type === ThreatType.DOMAIN_AGE)).toBe(true); - expect(result.score).toBeGreaterThanOrEqual(25); - }); - - it('should detect typosquatting', () => { - const result = detector.analyzeUrl('https://goggle.com/login'); - expect(result.threats.some((t) => t.type === ThreatType.TYPOSQUAT)).toBe(true); - expect(result.score).toBeGreaterThanOrEqual(35); - }); - - it('should detect IP address hostname', () => { - const result = detector.analyzeUrl('http://192.168.1.100/admin'); - expect(result.threats.some((t) => t.type === ThreatType.PHISHING_HEURISTIC)).toBe(true); - expect(result.score).toBeGreaterThanOrEqual(25); - }); - - it('should detect phishing pattern in hostname', () => { - const result = detector.analyzeUrl('https://login-secure-portal.xyz/account'); - expect(result.threats.some((t) => t.type === ThreatType.PHISHING_HEURISTIC)).toBe(true); - }); - - it('should detect HTTP protocol', () => { - const result = detector.analyzeUrl('http://example.com/login'); - expect(result.threats.some((t) => t.type === ThreatType.MIXED_CONTENT)).toBe(true); - }); - - it('should detect deep subdomain nesting', () => { - const result = detector.analyzeUrl('https://a.b.c.d.e.f.example.com/login'); - expect(result.threats.some((t) => t.type === ThreatType.PHISHING_HEURISTIC)).toBe(true); - }); - - it('should detect multiple redirect parameters', () => { - const result = detector.analyzeUrl('https://example.com/page?redirect=/login&next=/dashboard&return=/home'); - expect(result.threats.some((t) => t.type === ThreatType.REDIRECT_CHAIN)).toBe(true); - }); - - it('should detect excessive URL encoding', () => { - const result = detector.analyzeUrl('https://example.com/%3f%3d%26%23%40%24%5e'); - expect(result.threats.some((t) => t.type === ThreatType.URL_ENTROPY)).toBe(true); - }); - - it('should detect high URL path entropy', () => { - const result = detector.analyzeUrl('https://example.com/a8f3k2m9x7q1w4e6r5t0y2u8i3o7p'); - expect(result.threats.some((t) => t.type === ThreatType.URL_ENTROPY)).toBe(true); - }); - - it('should return SUSPICIOUS for moderate score', () => { - const result = detector.analyzeUrl('http://goggle.com/login-secure'); - expect(result.verdict).toBe(UrlVerdict.SUSPICIOUS); - }); - - it('should return PHISHING for high score', () => { - const result = detector.analyzeUrl('http://goggle.tk/login-secure-portal?redirect=/a&next=/b'); - expect(result.verdict).toBe(UrlVerdict.PHISHING); - }); - - it('should handle malformed URLs', () => { - const result = detector.analyzeUrl('not-a-real-url'); - expect(result.verdict).toBe(UrlVerdict.UNKNOWN); - }); - - it('should detect brand impersonation patterns', () => { - const result = detector.analyzeUrl('https://account-verify-now.com/paypal'); - expect(result.threats.some((t) => t.type === ThreatType.PHISHING_HEURISTIC)).toBe(true); - }); - }); - - describe('verdict thresholds', () => { - it('should classify score < 20 as SAFE', () => { - const result = detector.analyzeUrl('https://www.microsoft.com'); - expect(result.verdict).toBe(UrlVerdict.SAFE); - }); - - it('should classify score >= 20 as SPAM', () => { - const result = detector.analyzeUrl('https://example.tk/page'); - if (result.score >= 20 && result.score < 40) { - expect(result.verdict).toBe(UrlVerdict.SPAM); - } - }); - - it('should classify score >= 40 as SUSPICIOUS', () => { - const result = detector.analyzeUrl('http://g00gle.tk/login'); - if (result.score >= 40 && result.score < 70) { - expect(result.verdict).toBe(UrlVerdict.SUSPICIOUS); - } - }); - - it('should classify score >= 70 as PHISHING', () => { - const result = detector.analyzeUrl('http://g00gle.tk/login-secure-portal?redirect=/a&next=/b'); - if (result.score >= 70) { - expect(result.verdict).toBe(UrlVerdict.PHISHING); - } - }); - }); -}); diff --git a/packages/extension/tests/setup.ts b/packages/extension/tests/setup.ts deleted file mode 100644 index 4a61441..0000000 --- a/packages/extension/tests/setup.ts +++ /dev/null @@ -1,28 +0,0 @@ -const mockStorage: Record = {}; - -const chromeMock = { - storage: { - local: { - set: async (data: Record) => { - Object.assign(mockStorage, data); - }, - get: async (key: string | string[]) => { - if (Array.isArray(key)) { - const result: Record = {}; - for (const k of key) result[k] = mockStorage[k]; - return result; - } - return { [key]: mockStorage[key] }; - }, - remove: async (key: string | string[]) => { - const keys = Array.isArray(key) ? key : [key]; - for (const k of keys) delete mockStorage[k]; - }, - clear: async () => { - Object.keys(mockStorage).forEach((k) => delete mockStorage[k]); - }, - }, - }, -}; - -(global as any).chrome = chromeMock; diff --git a/packages/extension/tsconfig.json b/packages/extension/tsconfig.json deleted file mode 100644 index feb565a..0000000 --- a/packages/extension/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "jsx": "preserve", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "outDir": "dist", - "rootDir": "src", - "types": ["chrome"] - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/extension/vite.config.ts b/packages/extension/vite.config.ts deleted file mode 100644 index 29c6d37..0000000 --- a/packages/extension/vite.config.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; - -export default defineConfig(({ mode }) => { - const isFirefox = mode === 'firefox'; - const targetDir = isFirefox ? 'dist/firefox' : 'dist/chrome'; - - return { - root: '.', - build: { - outDir: targetDir, - emptyOutDir: true, - rollupOptions: { - input: { - background: resolve(__dirname, 'src/background/index.ts'), - content: resolve(__dirname, 'src/content/index.ts'), - popup: resolve(__dirname, 'src/popup/popup.ts'), - options: resolve(__dirname, 'src/options/options.ts'), - }, - output: { - entryFileNames: '[name].js', - }, - }, - copyPublicDir: true, - }, - resolve: { - alias: { - '@': resolve(__dirname, 'src'), - }, - }, - }; -}); diff --git a/packages/extension/vitest.config.ts b/packages/extension/vitest.config.ts deleted file mode 100644 index ba55739..0000000 --- a/packages/extension/vitest.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['tests/**/*.test.ts'], - setupFiles: ['./tests/setup.ts'], - }, -}); diff --git a/packages/integration-tests/README.md b/packages/integration-tests/README.md deleted file mode 100644 index 9d53864..0000000 --- a/packages/integration-tests/README.md +++ /dev/null @@ -1,131 +0,0 @@ -# Notification Service Integration Tests - -This directory contains integration tests for all notification services in the ShieldAI system. - -## Test Files - -### Individual Service Tests - -- `email.service.integration.test.ts` - Integration tests for EmailService (Resend) -- `sms.service.integration.test.ts` - Integration tests for SMSService (Twilio) -- `push.service.integration.test.ts` - Integration tests for PushService (FCM/APNs) - -### Orchestration Tests - -- `notification.service.integration.test.ts` - Integration tests for NotificationService - - Tests rate limiting across all channels - - Tests deduplication logic - - Tests user preferences - - Tests template-based notifications - -### End-to-End Tests - -- `notifications.integration.test.ts` - Basic E2E tests for notification flow -- `notifications.benchmark.ts` - Performance benchmarks - -## External Provider Mocks - -All external provider API calls are mocked: - -- **Resend (Email)**: Mocked via `vi.mock('resend')` -- **Twilio (SMS)**: Mocked via `vi.mock('twilio')` -- **Firebase Admin (Push)**: Mocked via `vi.mock('firebase-admin')` - -## Test Coverage - -### Email Service -- ✅ Email validation -- ✅ Rate limiting per user -- ✅ Template-based sending -- ✅ Batch sending -- ✅ Attachment handling -- ✅ Metadata handling -- ✅ Error handling (API errors, network timeouts, invalid emails) - -### SMS Service -- ✅ Phone number validation -- ✅ Rate limiting per user -- ✅ Batch sending -- ✅ Metadata handling -- ✅ Error handling (API errors, network timeouts, invalid numbers) - -### Push Service -- ✅ FCM notification sending -- ✅ APNs configuration -- ✅ Data payload handling -- ✅ Badge/sound/category settings -- ✅ Rate limiting per user -- ✅ Batch sending -- ✅ Error handling - -### Notification Service -- ✅ Multi-channel routing -- ✅ Deduplication logic -- ✅ User preferences -- ✅ Rate limiting -- ✅ Template resolution -- ✅ Error handling and retry logic - -## Running Tests - -```bash -# Run all integration tests -npm run test:e2e - -# Run specific test file -npm test -- email.service.integration.test.ts - -# Run with coverage -npm run test:coverage -``` - -## CI Integration - -Tests are configured to run in CI with the following setup: - -1. Environment variables must be set for all providers -2. Redis must be available for rate limiting and deduplication -3. Tests use mocked external APIs for reliability - -### Required Environment Variables - -```bash -# Resend (Email) -RESEND_API_KEY=your_api_key - -# Twilio (SMS) -TWILIO_ACCOUNT_SID=your_account_sid -TWILIO_AUTH_TOKEN=your_auth_token -TWILIO_MESSAGING_SERVICE_SID=your_service_sid - -# Firebase (Push) -FCM_PRIVATE_KEY=your_private_key -FCM_PROJECT_ID=your_project_id -FCM_CLIENT_EMAIL=your_client_email - -# Redis -REDIS_URL=redis://localhost:6379 -DEDUP_WINDOW_SECONDS=300 - -# Rate Limits -EMAIL_RATE_LIMIT=60 -SMS_RATE_LIMIT=30 -PUSH_RATE_LIMIT=100 -RATE_LIMIT_WINDOW_SECONDS=60 -``` - -## Test Strategy - -1. **Unit Tests**: Test individual service methods with mocked dependencies -2. **Integration Tests**: Test service interactions and external API mocks -3. **E2E Tests**: Test complete notification flows -4. **Benchmark Tests**: Measure performance under load - -## Error Scenarios Tested - -- Network timeouts -- API rate limits -- Invalid input validation -- Missing configuration -- Provider authentication failures -- Partial batch failures diff --git a/packages/integration-tests/REVIEW_STATUS.md b/packages/integration-tests/REVIEW_STATUS.md deleted file mode 100644 index efa4546..0000000 --- a/packages/integration-tests/REVIEW_STATUS.md +++ /dev/null @@ -1 +0,0 @@ -FRE-4501: Code Review Complete - Assigned to Security Reviewer diff --git a/packages/integration-tests/jest.config.ts b/packages/integration-tests/jest.config.ts deleted file mode 100644 index cc093b4..0000000 --- a/packages/integration-tests/jest.config.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { JestConfigWithTsJest } from 'ts-jest'; - -const config: JestConfigWithTsJest = { - preset: 'ts-jest', - testEnvironment: 'node', - roots: ['/src'], - testMatch: ['**/*.test.ts', '**/*.spec.ts'], - setupFilesAfterEnv: ['/src/setup.ts'], - moduleNameMapper: { - '^@shieldai/(.*)$': '/../$1/src/index.ts', - }, - collectCoverageFrom: [ - 'src/**/*.ts', - '!src/**/*.d.ts', - '!src/setup.ts', - ], - coverageThreshold: { - global: { - branches: 80, - functions: 80, - lines: 80, - statements: 80, - }, - }, - testTimeout: 30000, -}; - -export default config; diff --git a/packages/integration-tests/package.json b/packages/integration-tests/package.json deleted file mode 100644 index 2faf6c6..0000000 --- a/packages/integration-tests/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@shieldai/integration-tests", - "version": "1.0.0", - "main": "src/index.ts", - "scripts": { - "test": "jest", - "test:e2e": "jest src/e2e", - "test:unit": "jest src/unit", - "test:bench": "jest src/benchmarks", - "test:coverage": "jest --coverage", - "lint": "eslint src/" - }, - "dependencies": { - "@shieldai/db": "workspace:*", - "@shieldai/shared-billing": "workspace:*", - "@shieldai/shared-notifications": "workspace:*", - "jest": "^29.7.0", - "@types/jest": "^29.5.0", - "@jest/globals": "^29.7.0", - "ts-jest": "^29.1.0", - "typescript": "^5.0.0" - }, - "devDependencies": { - "@types/node": "^20.0.0", - "ts-node": "^10.9.0" - }, - "peerDependencies": { - "typescript": "^5.0.0" - } -} diff --git a/packages/integration-tests/src/benchmarks/billing.benchmark.ts b/packages/integration-tests/src/benchmarks/billing.benchmark.ts deleted file mode 100644 index 52ec48e..0000000 --- a/packages/integration-tests/src/benchmarks/billing.benchmark.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { describe, it, expect, beforeAll } from '@jest/globals'; -import { BillingService } from '@shieldai/shared-billing'; -import { SubscriptionTier } from '@shieldai/shared-billing'; - -describe('Billing Performance Benchmarks', () => { - let billingService: BillingService; - const iterations = 1000; - - beforeAll(() => { - billingService = BillingService.getInstance(); - }); - - describe('Tier Limit Checks', () => { - it('should check tier limits within 1ms', async () => { - const startTime = performance.now(); - - for (let i = 0; i < iterations; i++) { - await billingService.getTierLimits('plus' as SubscriptionTier); - } - - const endTime = performance.now(); - const avgTime = (endTime - startTime) / iterations; - - expect(avgTime).toBeLessThan(1); - }); - - it('should check usage against limit within 1ms', async () => { - const startTime = performance.now(); - - for (let i = 0; i < iterations; i++) { - await billingService.checkUsageAgainstLimit( - `user_${i}`, - 'plus' as SubscriptionTier, - 1000 - ); - } - - const endTime = performance.now(); - const avgTime = (endTime - startTime) / iterations; - - expect(avgTime).toBeLessThan(1); - }); - }); - - describe('Concurrency', () => { - it('should handle 100 concurrent limit checks', async () => { - const promises = Array.from({ length: 100 }, (_, i) => - billingService.checkUsageAgainstLimit( - `user_${i}`, - 'plus' as SubscriptionTier, - 1000 + i - ) - ); - - const startTime = performance.now(); - const results = await Promise.all(promises); - const endTime = performance.now(); - - expect(results).toHaveLength(100); - expect(endTime - startTime).toBeLessThan(100); - }); - }); -}); diff --git a/packages/integration-tests/src/benchmarks/notifications.benchmark.ts b/packages/integration-tests/src/benchmarks/notifications.benchmark.ts deleted file mode 100644 index 85311f1..0000000 --- a/packages/integration-tests/src/benchmarks/notifications.benchmark.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { describe, it, expect, beforeAll } from '@jest/globals'; -import { EmailService, SMSService, PushService } from '@shieldai/shared-notifications'; - -describe('Notification Performance Benchmarks', () => { - let emailService: EmailService; - let smsService: SMSService; - let pushService: PushService; - - beforeAll(() => { - emailService = EmailService.getInstance(); - smsService = SMSService.getInstance(); - pushService = PushService.getInstance(); - }); - - describe('Rate Limit Checks', () => { - it('should check email rate limit within 1ms', async () => { - const iterations = 1000; - const startTime = performance.now(); - - for (let i = 0; i < iterations; i++) { - emailService.getRateLimitStatus(); - } - - const endTime = performance.now(); - const avgTime = (endTime - startTime) / iterations; - - expect(avgTime).toBeLessThan(1); - }); - - it('should check SMS rate limit within 1ms', async () => { - const iterations = 1000; - const startTime = performance.now(); - - for (let i = 0; i < iterations; i++) { - smsService.getRateLimitStatus(); - } - - const endTime = performance.now(); - const avgTime = (endTime - startTime) / iterations; - - expect(avgTime).toBeLessThan(1); - }); - - it('should check push rate limit within 1ms', async () => { - const iterations = 1000; - const startTime = performance.now(); - - for (let i = 0; i < iterations; i++) { - pushService.getRateLimitStatus(); - } - - const endTime = performance.now(); - const avgTime = (endTime - startTime) / iterations; - - expect(avgTime).toBeLessThan(1); - }); - }); - - describe('Concurrency', () => { - it('should handle 100 concurrent rate limit checks', async () => { - const promises = Array.from({ length: 100 }, () => - emailService.getRateLimitStatus() - ); - - const startTime = performance.now(); - const results = await Promise.all(promises); - const endTime = performance.now(); - - expect(results).toHaveLength(100); - expect(endTime - startTime).toBeLessThan(50); - }); - }); -}); diff --git a/packages/integration-tests/src/e2e/billing.integration.test.ts b/packages/integration-tests/src/e2e/billing.integration.test.ts deleted file mode 100644 index b98403f..0000000 --- a/packages/integration-tests/src/e2e/billing.integration.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { describe, it, expect, beforeAll } from '@jest/globals'; -import { BillingService } from '@shieldai/shared-billing'; -import { loadBillingConfig, SubscriptionTier } from '@shieldai/shared-billing'; - -describe('Billing Integration Tests', () => { - let billingService: BillingService; - let testCustomerId: string; - - beforeAll(() => { - billingService = BillingService.getInstance(); - }); - - describe('Tier Configuration', () => { - it('should load tier configurations correctly', () => { - const config = loadBillingConfig(); - - expect(config.tiers.free.callMinutesLimit).toBe(100); - expect(config.tiers.basic.callMinutesLimit).toBe(500); - expect(config.tiers.plus.callMinutesLimit).toBe(2000); - expect(config.tiers.premium.callMinutesLimit).toBe(10000); - }); - - it('should have increasing limits across tiers', () => { - const config = loadBillingConfig(); - - expect(config.tiers.free.callMinutesLimit).toBeLessThan( - config.tiers.basic.callMinutesLimit - ); - expect(config.tiers.basic.callMinutesLimit).toBeLessThan( - config.tiers.plus.callMinutesLimit - ); - expect(config.tiers.plus.callMinutesLimit).toBeLessThan( - config.tiers.premium.callMinutesLimit - ); - }); - }); - - describe('Usage Limits', () => { - it('should check usage within limit', async () => { - const result = await billingService.checkUsageAgainstLimit( - 'user_test', - 'plus' as SubscriptionTier, - 1000 - ); - - expect(result.withinLimit).toBe(true); - expect(result.limit).toBe(2000); - expect(result.remaining).toBe(1000); - }); - - it('should detect usage exceeding limit', async () => { - const result = await billingService.checkUsageAgainstLimit( - 'user_test', - 'basic' as SubscriptionTier, - 600 - ); - - expect(result.withinLimit).toBe(false); - expect(result.remaining).toBe(0); - expect(result.limit).toBe(500); - }); - - it('should return correct remaining minutes', async () => { - const result = await billingService.checkUsageAgainstLimit( - 'user_test', - 'plus' as SubscriptionTier, - 1500 - ); - - expect(result.remaining).toBe(500); - }); - }); - - describe('Tier Limits', () => { - it('should return correct limits for each tier', async () => { - const free = await billingService.getTierLimits('free' as SubscriptionTier); - const basic = await billingService.getTierLimits('basic' as SubscriptionTier); - const plus = await billingService.getTierLimits('plus' as SubscriptionTier); - const premium = await billingService.getTierLimits('premium' as SubscriptionTier); - - expect(free.callMinutesLimit).toBe(100); - expect(basic.callMinutesLimit).toBe(500); - expect(plus.callMinutesLimit).toBe(2000); - expect(premium.callMinutesLimit).toBe(10000); - - expect(free.smsCountLimit).toBe(500); - expect(basic.smsCountLimit).toBe(2000); - expect(plus.smsCountLimit).toBe(10000); - expect(premium.smsCountLimit).toBe(50000); - }); - }); -}); diff --git a/packages/integration-tests/src/e2e/email.service.integration.test.ts b/packages/integration-tests/src/e2e/email.service.integration.test.ts deleted file mode 100644 index dbac03f..0000000 --- a/packages/integration-tests/src/e2e/email.service.integration.test.ts +++ /dev/null @@ -1,401 +0,0 @@ -import { describe, it, expect, beforeAll, beforeEach, afterAll, vi } from '@jest/globals'; -import { EmailService } from '@shieldai/shared-notifications'; -import type { EmailNotification } from '@shieldai/shared-notifications'; - -// Mock Resend -vi.mock('resend', () => { - return { - Resend: vi.fn().mockImplementation(() => ({ - emails: { - send: vi.fn().mockResolvedValue({ - data: { id: 'resend-mock-123' }, - error: undefined, - }), - }, - })), - }; -}); - -describe('EmailService Integration Tests', () => { - let emailService: EmailService; - let mockResend: any; - - beforeAll(() => { - emailService = EmailService.getInstance(); - mockResend = (require('resend').Resend as any).mock.instances[0]; - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('send', () => { - it('should successfully send email notification', async () => { - mockResend.emails.send.mockResolvedValueOnce({ - data: { id: 'test-email-123' }, - error: undefined, - }); - - const notification: EmailNotification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test Subject', - htmlBody: '

          Test

          ', - textBody: 'Test', - }; - - const result = await emailService.send(notification); - - expect(result.status).toBe('sent'); - expect(result.channel).toBe('email'); - expect(result.externalId).toBe('test-email-123'); - expect(result.notificationId).toContain('email-'); - expect(result.deliveredAt).toBeInstanceOf(Date); - }); - - it('should handle invalid email format', async () => { - const notification: EmailNotification = { - channel: 'email', - to: 'invalid-email', - subject: 'Test', - htmlBody: '

          Test

          ', - }; - - const result = await emailService.send(notification); - - expect(result.status).toBe('failed'); - expect(result.error).toContain('Invalid email format'); - }); - - it('should handle Resend API error', async () => { - mockResend.emails.send.mockResolvedValueOnce({ - data: { id: 'error-email-456' }, - error: { message: 'API rate limit exceeded' }, - }); - - const notification: EmailNotification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test', - htmlBody: '

          Test

          ', - }; - - const result = await emailService.send(notification); - - expect(result.status).toBe('failed'); - expect(result.error).toBe('API rate limit exceeded'); - }); - - it('should handle network error', async () => { - mockResend.emails.send.mockRejectedValueOnce( - new Error('Network timeout') - ); - - const notification: EmailNotification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test', - htmlBody: '

          Test

          ', - }; - - const result = await emailService.send(notification); - - expect(result.status).toBe('failed'); - expect(result.error).toBe('Network timeout'); - }); - - it('should include metadata in email', async () => { - mockResend.emails.send.mockResolvedValueOnce({ - data: { id: 'meta-email-789' }, - error: undefined, - }); - - const notification: EmailNotification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test', - htmlBody: '

          Test

          ', - metadata: { userId: 'user-123', campaign: 'welcome' }, - }; - - await emailService.send(notification); - - expect(mockResend.emails.send).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: { userId: 'user-123', campaign: 'welcome' }, - }) - ); - }); - - it('should include attachments in email', async () => { - mockResend.emails.send.mockResolvedValueOnce({ - data: { id: 'attach-email-101' }, - error: undefined, - }); - - const notification: EmailNotification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test', - htmlBody: '

          Test

          ', - attachments: [ - { - filename: 'report.pdf', - content: Buffer.from('PDF content'), - mimeType: 'application/pdf', - }, - ], - }; - - await emailService.send(notification); - - expect(mockResend.emails.send).toHaveBeenCalledWith( - expect.objectContaining({ - attachments: expect.arrayContaining([ - expect.objectContaining({ - filename: 'report.pdf', - contentType: 'application/pdf', - }), - ]), - }) - ); - }); - - it('should use default from address when not provided', async () => { - mockResend.emails.send.mockResolvedValueOnce({ - data: { id: 'default-from-202' }, - error: undefined, - }); - - const notification: EmailNotification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test', - htmlBody: '

          Test

          ', - }; - - await emailService.send(notification); - - expect(mockResend.emails.send).toHaveBeenCalledWith( - expect.objectContaining({ - from: 'ShieldAI ', - }) - ); - }); - - it('should use custom from address when provided', async () => { - mockResend.emails.send.mockResolvedValueOnce({ - data: { id: 'custom-from-303' }, - error: undefined, - }); - - const notification: EmailNotification = { - channel: 'email', - to: 'test@example.com', - from: 'custom@shieldai.com', - subject: 'Test', - htmlBody: '

          Test

          ', - }; - - await emailService.send(notification); - - expect(mockResend.emails.send).toHaveBeenCalledWith( - expect.objectContaining({ - from: 'custom@shieldai.com', - }) - ); - }); - - it('should handle both html and text body', async () => { - mockResend.emails.send.mockResolvedValueOnce({ - data: { id: 'both-body-404' }, - error: undefined, - }); - - const notification: EmailNotification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test', - htmlBody: '

          HTML

          ', - textBody: 'Plain text', - }; - - await emailService.send(notification); - - expect(mockResend.emails.send).toHaveBeenCalledWith( - expect.objectContaining({ - html: '

          HTML

          ', - text: 'Plain text', - }) - ); - }); - - it('should enforce email rate limiting', async () => { - // Set rate limit to 2 for testing - process.env.EMAIL_RATE_LIMIT = '2'; - - // Clear the service instance to pick up new config - vi.clearAllMocks(); - emailService = EmailService.getInstance(); - - const notification: EmailNotification = { - channel: 'email', - to: 'rate-test@example.com', - subject: 'Test', - htmlBody: '

          Test

          ', - }; - - // First two should succeed - const result1 = await emailService.send(notification); - const result2 = await emailService.send(notification); - - expect(result1.status).toBe('sent'); - expect(result2.status).toBe('sent'); - - // Third should throw due to rate limit - await expect(emailService.send(notification)).rejects.toThrow( - 'Email rate limit exceeded' - ); - }); - }); - - describe('sendWithTemplate', () => { - beforeEach(() => { - vi.clearAllMocks(); - emailService = EmailService.getInstance(); - }); - - it('should send email with resolved template', async () => { - mockResend.emails.send.mockResolvedValueOnce({ - data: { id: 'template-email-505' }, - error: undefined, - }); - - const result = await emailService.sendWithTemplate('test@example.com', { - templateId: 'welcome-email', - locale: 'en', - variables: { name: 'John' }, - }); - - expect(result.status).toBe('sent'); - expect(result.channel).toBe('email'); - }); - - it('should handle missing template', async () => { - mockResend.emails.send.mockResolvedValueOnce({ - data: { id: 'missing-template-606' }, - error: undefined, - }); - - const result = await emailService.sendWithTemplate('test@example.com', { - templateId: 'non-existent-template', - locale: 'en', - variables: {}, - }); - - expect(result.status).toBe('failed'); - expect(result.error).toContain('Template not found'); - }); - - it('should handle template channel mismatch', async () => { - mockResend.emails.send.mockResolvedValueOnce({ - data: { id: 'channel-mismatch-707' }, - error: undefined, - }); - - const result = await emailService.sendWithTemplate('test@example.com', { - templateId: 'sms-template', - locale: 'en', - variables: {}, - channel: 'email', - }); - - expect(result.status).toBe('failed'); - expect(result.error).toContain('is for channel'); - }); - }); - - describe('sendBatch', () => { - beforeEach(() => { - vi.clearAllMocks(); - emailService = EmailService.getInstance(); - }); - - it('should send multiple emails successfully', async () => { - mockResend.emails.send - .mockResolvedValueOnce({ data: { id: 'batch-1' }, error: undefined }) - .mockResolvedValueOnce({ data: { id: 'batch-2' }, error: undefined }) - .mockResolvedValueOnce({ data: { id: 'batch-3' }, error: undefined }); - - const notifications: EmailNotification[] = [ - { - channel: 'email', - to: 'user1@example.com', - subject: 'Batch 1', - htmlBody: '

          Test 1

          ', - }, - { - channel: 'email', - to: 'user2@example.com', - subject: 'Batch 2', - htmlBody: '

          Test 2

          ', - }, - { - channel: 'email', - to: 'user3@example.com', - subject: 'Batch 3', - htmlBody: '

          Test 3

          ', - }, - ]; - - const results = await emailService.sendBatch(notifications); - - expect(results).toHaveLength(3); - expect(results.every(r => r.status === 'sent')).toBe(true); - expect(results.map(r => r.externalId)).toEqual(['batch-1', 'batch-2', 'batch-3']); - }); - - it('should handle partial failures in batch', async () => { - mockResend.emails.send - .mockResolvedValueOnce({ data: { id: 'partial-1' }, error: undefined }) - .mockResolvedValueOnce({ data: { id: 'partial-2' }, error: undefined }); - - const notifications: EmailNotification[] = [ - { - channel: 'email', - to: 'valid@example.com', - subject: 'Valid', - htmlBody: '

          Valid

          ', - }, - { - channel: 'email', - to: 'invalid-email', - subject: 'Invalid', - htmlBody: '

          Invalid

          ', - }, - ]; - - const results = await emailService.sendBatch(notifications); - - expect(results).toHaveLength(2); - expect(results[0].status).toBe('sent'); - expect(results[1].status).toBe('failed'); - }); - }); - - describe('getRateLimitStatus', () => { - beforeEach(() => { - vi.clearAllMocks(); - emailService = EmailService.getInstance(); - }); - - it('should return rate limit status', () => { - const status = emailService.getRateLimitStatus(); - - expect(status).toHaveProperty('remaining'); - expect(status).toHaveProperty('limit'); - expect(status.limit).toBeGreaterThan(0); - expect(status.remaining).toBeLessThanOrEqual(status.limit); - }); - }); -}); diff --git a/packages/integration-tests/src/e2e/notification.service.integration.test.ts b/packages/integration-tests/src/e2e/notification.service.integration.test.ts deleted file mode 100644 index 74c7000..0000000 --- a/packages/integration-tests/src/e2e/notification.service.integration.test.ts +++ /dev/null @@ -1,513 +0,0 @@ -import { describe, it, expect, beforeAll, beforeEach, vi } from '@jest/globals'; -import { NotificationService } from '@shieldai/shared-notifications'; -import { EmailService } from '@shieldai/shared-notifications'; -import { SMSService } from '@shieldai/shared-notifications'; -import { PushService } from '@shieldai/shared-notifications'; -import type { Notification, DeduplicationKey } from '@shieldai/shared-notifications'; - -// Mock individual services -vi.mock('@shieldai/shared-notifications', async () => { - const actual = await vi.importActual('@shieldai/shared-notifications'); - - return { - ...(actual as object), - EmailService: { - getInstance: vi.fn(() => ({ - send: vi.fn(async (notification: any) => ({ - notificationId: `email-${Date.now()}`, - channel: 'email', - status: 'sent', - externalId: 'resend-mock-id', - })), - })), - }, - SMSService: { - getInstance: vi.fn(() => ({ - send: vi.fn(async (notification: any) => ({ - notificationId: `sms-${Date.now()}`, - channel: 'sms', - status: 'sent', - externalId: 'twilio-mock-id', - })), - })), - }, - PushService: { - getInstance: vi.fn(() => ({ - send: vi.fn(async (notification: any) => ({ - notificationId: `push-${Date.now()}`, - channel: 'push', - status: 'sent', - externalId: 'fcm-mock-id', - })), - })), - }, - }; -}); - -describe('NotificationService Integration Tests', () => { - let notificationService: NotificationService; - - beforeAll(() => { - notificationService = NotificationService.getInstance(); - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('send', () => { - it('should send email notification', async () => { - const notification: Notification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test', - htmlBody: '

          Test

          ', - }; - - const result = await notificationService.send(notification); - - expect(result.channel).toBe('email'); - expect(result.status).toBe('sent'); - expect(result.notificationId).toContain('email-'); - }); - - it('should send SMS notification', async () => { - const notification: Notification = { - channel: 'sms', - to: '+14155552672', - body: 'Test SMS', - }; - - const result = await notificationService.send(notification); - - expect(result.channel).toBe('sms'); - expect(result.status).toBe('sent'); - expect(result.notificationId).toContain('sms-'); - }); - - it('should send push notification', async () => { - const notification: Notification = { - channel: 'push', - userId: 'user-device-token', - title: 'Test Title', - body: 'Test Body', - }; - - const result = await notificationService.send(notification); - - expect(result.channel).toBe('push'); - expect(result.status).toBe('sent'); - expect(result.notificationId).toContain('push-'); - }); - - it('should throw for unknown channel', async () => { - const notification = { - channel: 'unknown' as any, - to: 'test@example.com', - } as Notification; - - await expect(notificationService.send(notification)).rejects.toThrow( - 'Unknown notification channel' - ); - }); - }); - - describe('sendWithDeduplication', () => { - beforeEach(() => { - vi.clearAllMocks(); - notificationService = NotificationService.getInstance(); - }); - - it('should allow first notification', async () => { - const notification: Notification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test', - htmlBody: '

          Test

          ', - }; - - const dedupKey: DeduplicationKey = { - userId: 'user-123', - templateId: 'welcome-email', - key: 'initial', - }; - - const result = await notificationService.sendWithDeduplication( - notification, - dedupKey - ); - - expect(result.status).toBe('sent'); - }); - - it('should mark duplicate as pending', async () => { - const notification: Notification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test', - htmlBody: '

          Test

          ', - }; - - const dedupKey: DeduplicationKey = { - userId: 'user-456', - templateId: 'alert-email', - key: 'same-key', - }; - - // First call - await notificationService.sendWithDeduplication(notification, dedupKey); - - // Second call with same key - should be pending - const result = await notificationService.sendWithDeduplication( - { ...notification, subject: 'Updated' }, - dedupKey - ); - - expect(result.status).toBe('pending'); - expect(result.error).toContain('Duplicate notification'); - }); - - it('should use custom deduplication window', async () => { - const notification: Notification = { - channel: 'sms', - to: '+14155552672', - body: 'Test', - }; - - const dedupKey: DeduplicationKey = { - userId: 'user-789', - templateId: 'sms-template', - key: 'custom-window', - windowSeconds: 60, - }; - - const result = await notificationService.sendWithDeduplication( - notification, - dedupKey - ); - - expect(result.status).toBe('sent'); - }); - }); - - describe('setPreference and getPreference', () => { - beforeEach(() => { - vi.clearAllMocks(); - notificationService = NotificationService.getInstance(); - }); - - it('should set notification preference', async () => { - const userId = 'user-pref-123'; - const channel = 'email' as const; - const enabled = true; - const categories = ['alerts', 'updates']; - - const preference = await notificationService.setPreference( - userId, - channel, - enabled, - categories - ); - - expect(preference.userId).toBe(userId); - expect(preference.channel).toBe(channel); - expect(preference.enabled).toBe(enabled); - expect(preference.categories).toEqual(categories); - }); - - it('should get notification preference', async () => { - const userId = 'user-pref-456'; - const channel = 'push' as const; - - await notificationService.setPreference(userId, channel, true, ['notifications']); - - const preference = await notificationService.getPreference(userId, channel); - - expect(preference).not.toBeNull(); - expect(preference?.userId).toBe(userId); - expect(preference?.enabled).toBe(true); - }); - - it('should return null for non-existent preference', async () => { - const preference = await notificationService.getPreference( - 'non-existent-user', - 'email' - ); - - expect(preference).toBeNull(); - }); - }); - - describe('shouldSend', () => { - beforeEach(() => { - vi.clearAllMocks(); - notificationService = NotificationService.getInstance(); - }); - - it('should allow send when no preference set', async () => { - const result = await notificationService.shouldSend( - 'new-user', - 'email', - 'alerts' - ); - - expect(result).toBe(true); - }); - - it('should allow send when preference enabled', async () => { - await notificationService.setPreference('enabled-user', 'sms', true, ['marketing']); - - const result = await notificationService.shouldSend( - 'enabled-user', - 'sms', - 'marketing' - ); - - expect(result).toBe(true); - }); - - it('should block send when preference disabled', async () => { - await notificationService.setPreference('disabled-user', 'push', false, ['alerts']); - - const result = await notificationService.shouldSend( - 'disabled-user', - 'push', - 'alerts' - ); - - expect(result).toBe(false); - }); - - it('should block send when category not in allowed list', async () => { - await notificationService.setPreference( - 'category-user', - 'email', - true, - ['alerts', 'updates'] - ); - - const result = await notificationService.shouldSend( - 'category-user', - 'email', - 'marketing' - ); - - expect(result).toBe(false); - }); - - it('should allow send when categories list is empty', async () => { - await notificationService.setPreference('empty-cats-user', 'sms', true, []); - - const result = await notificationService.shouldSend( - 'empty-cats-user', - 'sms', - 'any-category' - ); - - expect(result).toBe(true); - }); - }); - - describe('sendWithPreferences', () => { - beforeEach(() => { - vi.clearAllMocks(); - notificationService = NotificationService.getInstance(); - }); - - it('should send when preference allows', async () => { - await notificationService.setPreference( - 'pref-user-1', - 'email', - true, - ['alerts'] - ); - - const notification: Notification = { - channel: 'email', - to: 'test@example.com', - subject: 'Test', - htmlBody: '

          Test

          ', - }; - - const result = await notificationService.sendWithPreferences( - notification, - 'alerts' - ); - - expect(result?.status).toBe('sent'); - }); - - it('should return pending when preference disabled', async () => { - await notificationService.setPreference( - 'pref-user-2', - 'sms', - false, - ['marketing'] - ); - - const notification: Notification = { - channel: 'sms', - to: '+14155552672', - body: 'Test', - }; - - const result = await notificationService.sendWithPreferences( - notification, - 'marketing' - ); - - expect(result?.status).toBe('pending'); - expect(result?.error).toContain('Notification disabled'); - }); - }); - - describe('checkRateLimit', () => { - beforeEach(async () => { - vi.clearAllMocks(); - notificationService = NotificationService.getInstance(); - }); - - it('should allow within rate limit', async () => { - const result = await notificationService.checkRateLimit( - 'rate-user-1', - 'email' - ); - - expect(result.allowed).toBe(true); - expect(result.limit).toBeGreaterThan(0); - expect(result.remaining).toBeLessThan(result.limit); - }); - - it('should track multiple identifiers independently', async () => { - await notificationService.checkRateLimit('rate-user-2a', 'email'); - await notificationService.checkRateLimit('rate-user-2b', 'email'); - - const resultA = await notificationService.checkRateLimit('rate-user-2a', 'email'); - const resultB = await notificationService.checkRateLimit('rate-user-2b', 'email'); - - expect(resultA.currentCount).toBe(2); - expect(resultB.currentCount).toBe(2); - }); - - it('should track different channels independently', async () => { - await notificationService.checkRateLimit('rate-user-3', 'email'); - await notificationService.checkRateLimit('rate-user-3', 'sms'); - - const emailResult = await notificationService.checkRateLimit('rate-user-3', 'email'); - const smsResult = await notificationService.checkRateLimit('rate-user-3', 'sms'); - - expect(emailResult.currentCount).toBe(2); - expect(smsResult.currentCount).toBe(2); - }); - - it('should use custom limit', async () => { - const result = await notificationService.checkRateLimit( - 'rate-user-4', - 'email', - 5 - ); - - expect(result.limit).toBe(5); - }); - - it('should use custom window', async () => { - const result = await notificationService.checkRateLimit( - 'rate-user-5', - 'email', - 10, - 120 - ); - - expect(result.resetInSeconds).toBeLessThanOrEqual(120); - }); - }); - - describe('deduplicateNotification', () => { - beforeEach(async () => { - vi.clearAllMocks(); - notificationService = NotificationService.getInstance(); - }); - - it('should return true for first notification', async () => { - const wasSet = await notificationService.deduplicateNotification({ - userId: 'dedup-user-1', - templateId: 'test-template', - key: 'unique-key', - }); - - expect(wasSet).toBe(true); - }); - - it('should return false for duplicate', async () => { - await notificationService.deduplicateNotification({ - userId: 'dedup-user-2', - templateId: 'test-template', - key: 'duplicate-key', - }); - - const wasSet = await notificationService.deduplicateNotification({ - userId: 'dedup-user-2', - templateId: 'test-template', - key: 'duplicate-key', - }); - - expect(wasSet).toBe(false); - }); - - it('should use custom window', async () => { - const wasSet = await notificationService.deduplicateNotification( - { - userId: 'dedup-user-3', - templateId: 'test-template', - key: 'custom-window-key', - }, - 60 - ); - - expect(wasSet).toBe(true); - }); - - it('should use windowSeconds from key', async () => { - const wasSet = await notificationService.deduplicateNotification({ - userId: 'dedup-user-4', - templateId: 'test-template', - key: 'key-window', - windowSeconds: 120, - }); - - expect(wasSet).toBe(true); - }); - }); - - describe('getRateLimitConfig', () => { - beforeEach(() => { - vi.clearAllMocks(); - notificationService = NotificationService.getInstance(); - }); - - it('should return rate limit configuration', () => { - const config = notificationService.getRateLimitConfig(); - - expect(config).toHaveProperty('emailPerMinute'); - expect(config).toHaveProperty('smsPerMinute'); - expect(config).toHaveProperty('pushPerMinute'); - expect(config).toHaveProperty('windowSeconds'); - expect(config.emailPerMinute).toBeGreaterThan(0); - expect(config.smsPerMinute).toBeGreaterThan(0); - expect(config.pushPerMinute).toBeGreaterThan(0); - }); - }); - - describe('getTemplateService', () => { - beforeEach(() => { - vi.clearAllMocks(); - notificationService = NotificationService.getInstance(); - }); - - it('should return template service instance', () => { - const templateService = notificationService.getTemplateService(); - - expect(templateService).toBeDefined(); - }); - }); -}); diff --git a/packages/integration-tests/src/e2e/notifications.integration.test.ts b/packages/integration-tests/src/e2e/notifications.integration.test.ts deleted file mode 100644 index 1be9efa..0000000 --- a/packages/integration-tests/src/e2e/notifications.integration.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, it, expect, beforeAll } from '@jest/globals'; -import { EmailService, SMSService, PushService } from '@shieldai/shared-notifications'; - -describe('Notification Integration Tests', () => { - let emailService: EmailService; - let smsService: SMSService; - let pushService: PushService; - - beforeAll(() => { - emailService = EmailService.getInstance(); - smsService = SMSService.getInstance(); - pushService = PushService.getInstance(); - }); - - describe('Email Service', () => { - it('should validate email notification structure', () => { - const notification = { - channel: 'email' as const, - to: 'test@example.com', - subject: 'Test Subject', - htmlBody: '

          Test

          ', - textBody: 'Test', - }; - - expect(notification.channel).toBe('email'); - expect(notification.to).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/); - expect(notification.subject).toBeTruthy(); - expect(notification.htmlBody).toBeTruthy(); - }); - - it('should handle rate limiting', async () => { - const rateLimit = emailService.getRateLimitStatus(); - - expect(rateLimit.limit).toBeGreaterThan(0); - expect(rateLimit.remaining).toBeLessThanOrEqual(rateLimit.limit); - }); - }); - - describe('SMS Service', () => { - it('should validate SMS notification structure', () => { - const notification = { - channel: 'sms' as const, - to: '+1234567890', - body: 'Test message', - }; - - expect(notification.channel).toBe('sms'); - expect(notification.to).toMatch(/^\+?\d{10,15}$/); - expect(notification.body).toBeTruthy(); - }); - - it('should handle rate limiting', async () => { - const rateLimit = smsService.getRateLimitStatus(); - - expect(rateLimit.limit).toBeGreaterThan(0); - expect(rateLimit.remaining).toBeLessThanOrEqual(rateLimit.limit); - }); - }); - - describe('Push Service', () => { - it('should validate push notification structure', () => { - const notification = { - channel: 'push' as const, - userId: 'user_123', - title: 'Test Title', - body: 'Test Body', - data: { key: 'value' }, - }; - - expect(notification.channel).toBe('push'); - expect(notification.userId).toBeTruthy(); - expect(notification.title).toBeTruthy(); - expect(notification.body).toBeTruthy(); - }); - - it('should handle rate limiting', async () => { - const rateLimit = pushService.getRateLimitStatus(); - - expect(rateLimit.limit).toBeGreaterThan(0); - expect(rateLimit.remaining).toBeLessThanOrEqual(rateLimit.limit); - }); - }); - - describe('Multi-Channel Notifications', () => { - it('should support different channels for same user', async () => { - const emailResult = await emailService.send({ - channel: 'email' as const, - to: 'test@example.com', - subject: 'Alert', - htmlBody: '

          Alert message

          ', - }); - - expect(emailResult.channel).toBe('email'); - expect(emailResult.notificationId).toBeTruthy(); - }); - }); -}); diff --git a/packages/integration-tests/src/e2e/push.service.integration.test.ts b/packages/integration-tests/src/e2e/push.service.integration.test.ts deleted file mode 100644 index f41485d..0000000 --- a/packages/integration-tests/src/e2e/push.service.integration.test.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { describe, it, expect, beforeAll, beforeEach, vi } from '@jest/globals'; -import { PushService } from '@shieldai/shared-notifications'; -import type { PushNotification } from '@shieldai/shared-notifications'; -import * as admin from 'firebase-admin'; - -// Mock firebase-admin -vi.mock('firebase-admin', () => { - const mockMessaging = { - send: vi.fn().mockResolvedValue('push-token-123'), - }; - - const mockCredential = { - cert: vi.fn().mockReturnValue({ - projectId: 'test-project', - clientEmail: 'test@test-project.iam.gserviceaccount.com', - privateKey: 'test-key', - }), - }; - - const mockApp = { - options: {}, - }; - - return { - default: { - initializeApp: vi.fn().mockReturnValue(mockApp), - credential: { - cert: mockCredential, - }, - messaging: vi.fn().mockReturnValue(mockMessaging), - }, - messaging: vi.fn().mockReturnValue(mockMessaging), - app: { - App: Object, - }, - credential: { - cert: mockCredential, - }, - }; -}); - -describe('PushService Integration Tests', () => { - let pushService: PushService; - let mockMessaging: any; - - beforeAll(() => { - pushService = PushService.getInstance(); - mockMessaging = (require('firebase-admin').messaging as any).mock.instances[0]; - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('send', () => { - it('should successfully send push notification', async () => { - const mockResponse = 'fcm-message-id-123'; - (require('firebase-admin').messaging as any).mock.instances[0].send.mockResolvedValueOnce(mockResponse); - - const notification: PushNotification = { - channel: 'push', - userId: 'user-device-token-123', - title: 'Test Title', - body: 'Test Body', - }; - - const result = await pushService.send(notification); - - expect(result.status).toBe('sent'); - expect(result.channel).toBe('push'); - expect(result.externalId).toBe(mockResponse); - expect(result.notificationId).toContain('push-'); - expect(result.deliveredAt).toBeInstanceOf(Date); - }); - - it('should include notification data', async () => { - const mockResponse = 'data-push-456'; - (require('firebase-admin').messaging as any).mock.instances[0].send.mockResolvedValueOnce(mockResponse); - - const notification: PushNotification = { - channel: 'push', - userId: 'user-device-token-456', - title: 'Test', - body: 'Test', - data: { key1: 'value1', key2: 'value2' }, - }; - - await pushService.send(notification); - - expect((require('firebase-admin').messaging as any).mock.instances[0].send).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - key1: 'value1', - key2: 'value2', - }), - }) - ); - }); - - it('should include badge and sound settings', async () => { - const mockResponse = 'apns-push-789'; - (require('firebase-admin').messaging as any).mock.instances[0].send.mockResolvedValueOnce(mockResponse); - - const notification: PushNotification = { - channel: 'push', - userId: 'user-device-token-789', - title: 'Test', - body: 'Test', - badge: 5, - sound: 'custom-sound.caf', - category: 'ALERT_CATEGORY', - }; - - await pushService.send(notification); - - expect((require('firebase-admin').messaging as any).mock.instances[0].send).toHaveBeenCalledWith( - expect.objectContaining({ - apns: expect.objectContaining({ - payload: expect.objectContaining({ - aps: expect.objectContaining({ - badge: 5, - sound: 'custom-sound.caf', - category: 'ALERT_CATEGORY', - }), - }), - }), - }) - ); - }); - - it('should handle undefined data gracefully', async () => { - const mockResponse = 'no-data-push-101'; - (require('firebase-admin').messaging as any).mock.instances[0].send.mockResolvedValueOnce(mockResponse); - - const notification: PushNotification = { - channel: 'push', - userId: 'user-device-token-101', - title: 'Test', - body: 'Test', - }; - - await pushService.send(notification); - - expect((require('firebase-admin').messaging as any).mock.instances[0].send).toHaveBeenCalledWith( - expect.objectContaining({ - data: undefined, - }) - ); - }); - - it('should handle FCM API error', async () => { - (require('firebase-admin').messaging as any).mock.instances[0].send.mockRejectedValueOnce( - new Error('Invalid registration token') - ); - - const notification: PushNotification = { - channel: 'push', - userId: 'invalid-token', - title: 'Test', - body: 'Test', - }; - - const result = await pushService.send(notification); - - expect(result.status).toBe('failed'); - expect(result.error).toBe('Invalid registration token'); - }); - - it('should enforce push rate limiting', async () => { - process.env.PUSH_RATE_LIMIT = '2'; - vi.clearAllMocks(); - pushService = PushService.getInstance(); - - const notification: PushNotification = { - channel: 'push', - userId: 'rate-test-user', - title: 'Test', - body: 'Test', - }; - - // First two should succeed - const result1 = await pushService.send(notification); - const result2 = await pushService.send(notification); - - expect(result1.status).toBe('sent'); - expect(result2.status).toBe('sent'); - - // Third should throw due to rate limit - await expect(pushService.send(notification)).rejects.toThrow( - 'Push rate limit exceeded' - ); - }); - - it('should handle network timeout', async () => { - (require('firebase-admin').messaging as any).mock.instances[0].send.mockRejectedValueOnce( - new Error('Network timeout') - ); - - const notification: PushNotification = { - channel: 'push', - userId: 'timeout-user', - title: 'Test', - body: 'Test', - }; - - const result = await pushService.send(notification); - - expect(result.status).toBe('failed'); - expect(result.error).toBe('Network timeout'); - }); - }); - - describe('sendBatch', () => { - beforeEach(() => { - vi.clearAllMocks(); - pushService = PushService.getInstance(); - }); - - it('should send multiple push notifications successfully', async () => { - (require('firebase-admin').messaging as any).mock.instances[0].send - .mockResolvedValueOnce('batch-push-1') - .mockResolvedValueOnce('batch-push-2') - .mockResolvedValueOnce('batch-push-3'); - - const notifications: PushNotification[] = [ - { - channel: 'push', - userId: 'user-token-1', - title: 'Batch 1', - body: 'Test 1', - }, - { - channel: 'push', - userId: 'user-token-2', - title: 'Batch 2', - body: 'Test 2', - }, - { - channel: 'push', - userId: 'user-token-3', - title: 'Batch 3', - body: 'Test 3', - }, - ]; - - const results = await pushService.sendBatch(notifications); - - expect(results).toHaveLength(3); - expect(results.every(r => r.status === 'sent')).toBe(true); - expect(results.map(r => r.externalId)).toEqual(['batch-push-1', 'batch-push-2', 'batch-push-3']); - }); - - it('should handle partial failures in batch', async () => { - (require('firebase-admin').messaging as any).mock.instances[0].send - .mockResolvedValueOnce('partial-push-1') - .mockRejectedValueOnce(new Error('Invalid token')); - - const notifications: PushNotification[] = [ - { - channel: 'push', - userId: 'valid-token', - title: 'Valid', - body: 'Valid', - }, - { - channel: 'push', - userId: 'invalid-token', - title: 'Invalid', - body: 'Invalid', - }, - ]; - - const results = await pushService.sendBatch(notifications); - - expect(results).toHaveLength(2); - expect(results[0].status).toBe('sent'); - expect(results[1].status).toBe('failed'); - }); - }); - - describe('getRateLimitStatus', () => { - beforeEach(() => { - vi.clearAllMocks(); - pushService = PushService.getInstance(); - }); - - it('should return rate limit status', () => { - const status = pushService.getRateLimitStatus(); - - expect(status).toHaveProperty('remaining'); - expect(status).toHaveProperty('limit'); - expect(status.limit).toBeGreaterThan(0); - expect(status.remaining).toBeLessThanOrEqual(status.limit); - }); - }); - - describe('APNs configuration', () => { - beforeEach(() => { - vi.clearAllMocks(); - pushService = PushService.getInstance(); - }); - - it('should configure APNs payload correctly', async () => { - const mockResponse = 'apns-config-test'; - (require('firebase-admin').messaging as any).mock.instances[0].send.mockResolvedValueOnce(mockResponse); - - const notification: PushNotification = { - channel: 'push', - userId: 'ios-device-token', - title: 'iOS Test', - body: 'iOS Body', - badge: 10, - sound: 'notification.caf', - category: 'MESSAGE_CATEGORY', - }; - - await pushService.send(notification); - - const callArg = (require('firebase-admin').messaging as any).mock.instances[0].send.mock.calls[0][0]; - - expect(call_arg.apns).toEqual( - expect.objectContaining({ - payload: expect.objectContaining({ - aps: expect.objectContaining({ - badge: 10, - sound: 'notification.caf', - category: 'MESSAGE_CATEGORY', - }), - }), - }) - ); - }); - }); -}); diff --git a/packages/integration-tests/src/e2e/sms.service.integration.test.ts b/packages/integration-tests/src/e2e/sms.service.integration.test.ts deleted file mode 100644 index e377ec3..0000000 --- a/packages/integration-tests/src/e2e/sms.service.integration.test.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { describe, it, expect, beforeAll, beforeEach, vi } from '@jest/globals'; -import { SMSService } from '@shieldai/shared-notifications'; -import type { SMSNotification } from '@shieldai/shared-notifications'; -import twilio from 'twilio'; - -// Mock twilio -vi.mock('twilio', () => { - const mockMessages = { - create: vi.fn().mockResolvedValue({ - sid: 'SM1234567890abcdef1234567890abcdef', - from: '+14155552671', - to: '+14155552672', - body: 'Test message', - status: 'sent', - }), - }; - - return vi.fn().mockReturnValue({ - messages: mockMessages, - }); -}); - -describe('SMSService Integration Tests', () => { - let smsService: SMSService; - let mockTwilio: any; - - beforeAll(() => { - smsService = SMSService.getInstance(); - mockTwilio = (require('twilio') as any).mock.results[0].value; - }); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - describe('send', () => { - it('should successfully send SMS notification', async () => { - mockTwilio.messages.create.mockResolvedValueOnce({ - sid: 'SM1234567890abcdef', - from: '+14155552671', - to: '+14155552672', - body: 'Test SMS', - status: 'sent', - }); - - const notification: SMSNotification = { - channel: 'sms', - to: '+14155552672', - body: 'Test SMS', - }; - - const result = await smsService.send(notification); - - expect(result.status).toBe('sent'); - expect(result.channel).toBe('sms'); - expect(result.externalId).toBe('SM1234567890abcdef'); - expect(result.notificationId).toContain('sms-'); - expect(result.deliveredAt).toBeInstanceOf(Date); - }); - - it('should use default from number when not provided', async () => { - mockTwilio.messages.create.mockResolvedValueOnce({ - sid: 'SM-default-from', - from: '+14155552671', - to: '+14155552672', - body: 'Test', - status: 'sent', - }); - - const notification: SMSNotification = { - channel: 'sms', - to: '+14155552672', - body: 'Test', - }; - - await smsService.send(notification); - - expect(mockTwilio.messages.create).toHaveBeenCalledWith( - expect.objectContaining({ - from: expect.any(String), - }) - ); - }); - - it('should use custom from number when provided', async () => { - mockTwilio.messages.create.mockResolvedValueOnce({ - sid: 'SM-custom-from', - from: '+14155559999', - to: '+14155552672', - body: 'Test', - status: 'sent', - }); - - const notification: SMSNotification = { - channel: 'sms', - to: '+14155552672', - from: '+14155559999', - body: 'Test', - }; - - await smsService.send(notification); - - expect(mockTwilio.messages.create).toHaveBeenCalledWith( - expect.objectContaining({ - from: '+14155559999', - }) - ); - }); - - it('should include metadata in SMS', async () => { - mockTwilio.messages.create.mockResolvedValueOnce({ - sid: 'SM-metadata-test', - from: '+14155552671', - to: '+14155552672', - body: 'Test', - status: 'sent', - }); - - const notification: SMSNotification = { - channel: 'sms', - to: '+14155552672', - body: 'Test', - metadata: { userId: 'user-123', campaign: 'promo' }, - }; - - await smsService.send(notification); - - expect(mockTwilio.messages.create).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: { userId: 'user-123', campaign: 'promo' }, - }) - ); - }); - - it('should handle Twilio API error', async () => { - mockTwilio.messages.create.mockRejectedValueOnce( - new Error('Invalid phone number') - ); - - const notification: SMSNotification = { - channel: 'sms', - to: 'invalid-number', - body: 'Test', - }; - - const result = await smsService.send(notification); - - expect(result.status).toBe('failed'); - expect(result.error).toBe('Invalid phone number'); - }); - - it('should handle rate limiting', async () => { - process.env.SMS_RATE_LIMIT = '2'; - vi.clearAllMocks(); - smsService = SMSService.getInstance(); - - mockTwilio.messages.create - .mockResolvedValueOnce({ - sid: 'SM-rate-1', - from: '+14155552671', - to: '+14155552672', - body: 'Test', - status: 'sent', - }) - .mockResolvedValueOnce({ - sid: 'SM-rate-2', - from: '+14155552671', - to: '+14155552672', - body: 'Test', - status: 'sent', - }); - - const notification: SMSNotification = { - channel: 'sms', - to: '+14155552672', - body: 'Test', - }; - - // First two should succeed - const result1 = await smsService.send(notification); - const result2 = await smsService.send(notification); - - expect(result1.status).toBe('sent'); - expect(result2.status).toBe('sent'); - - // Third should throw due to rate limit - await expect(smsService.send(notification)).rejects.toThrow( - 'SMS rate limit exceeded' - ); - }); - - it('should handle network timeout', async () => { - mockTwilio.messages.create.mockRejectedValueOnce( - new Error('Network timeout') - ); - - const notification: SMSNotification = { - channel: 'sms', - to: '+14155552672', - body: 'Test', - }; - - const result = await smsService.send(notification); - - expect(result.status).toBe('failed'); - expect(result.error).toBe('Network timeout'); - }); - - it('should handle invalid phone number format', async () => { - mockTwilio.messages.create.mockRejectedValueOnce( - new Error('Number not in E.164 format') - ); - - const notification: SMSNotification = { - channel: 'sms', - to: '12345', - body: 'Test', - }; - - const result = await smsService.send(notification); - - expect(result.status).toBe('failed'); - expect(result.error).toContain('Number not in E.164 format'); - }); - }); - - describe('sendBatch', () => { - beforeEach(() => { - vi.clearAllMocks(); - smsService = SMSService.getInstance(); - mockTwilio.messages.create - .mockResolvedValueOnce({ - sid: 'batch-sms-1', - from: '+14155552671', - to: '+14155552672', - body: 'Test 1', - status: 'sent', - }) - .mockResolvedValueOnce({ - sid: 'batch-sms-2', - from: '+14155552671', - to: '+14155552673', - body: 'Test 2', - status: 'sent', - }) - .mockResolvedValueOnce({ - sid: 'batch-sms-3', - from: '+14155552671', - to: '+14155552674', - body: 'Test 3', - status: 'sent', - }); - }); - - it('should send multiple SMS successfully', async () => { - const notifications: SMSNotification[] = [ - { - channel: 'sms', - to: '+14155552672', - body: 'Batch 1', - }, - { - channel: 'sms', - to: '+14155552673', - body: 'Batch 2', - }, - { - channel: 'sms', - to: '+14155552674', - body: 'Batch 3', - }, - ]; - - const results = await smsService.sendBatch(notifications); - - expect(results).toHaveLength(3); - expect(results.every(r => r.status === 'sent')).toBe(true); - expect(results.map(r => r.externalId)).toEqual(['batch-sms-1', 'batch-sms-2', 'batch-sms-3']); - }); - - it('should handle partial failures in batch', async () => { - mockTwilio.messages.create - .mockResolvedValueOnce({ - sid: 'partial-sms-1', - from: '+14155552671', - to: '+14155552672', - body: 'Valid', - status: 'sent', - }) - .mockRejectedValueOnce(new Error('Invalid number')); - - const notifications: SMSNotification[] = [ - { - channel: 'sms', - to: '+14155552672', - body: 'Valid', - }, - { - channel: 'sms', - to: 'invalid', - body: 'Invalid', - }, - ]; - - const results = await smsService.sendBatch(notifications); - - expect(results).toHaveLength(2); - expect(results[0].status).toBe('sent'); - expect(results[1].status).toBe('failed'); - }); - }); - - describe('getRateLimitStatus', () => { - beforeEach(() => { - vi.clearAllMocks(); - smsService = SMSService.getInstance(); - }); - - it('should return rate limit status', () => { - const status = smsService.getRateLimitStatus(); - - expect(status).toHaveProperty('remaining'); - expect(status).toHaveProperty('limit'); - expect(status.limit).toBeGreaterThan(0); - expect(status.remaining).toBeLessThanOrEqual(status.limit); - }); - }); - - describe('Twilio configuration', () => { - beforeEach(() => { - vi.clearAllMocks(); - smsService = SMSService.getInstance(); - }); - - it('should use configured account SID and auth token', () => { - expect(require('twilio')).toHaveBeenCalledWith( - expect.any(String), - expect.any(String) - ); - }); - - it('should use configured messaging service SID', async () => { - mockTwilio.messages.create.mockResolvedValueOnce({ - sid: 'SM-config-test', - from: '+14155552671', - to: '+14155552672', - body: 'Test', - status: 'sent', - }); - - const notification: SMSNotification = { - channel: 'sms', - to: '+14155552672', - body: 'Test', - }; - - await smsService.send(notification); - - expect(mockTwilio.messages.create).toHaveBeenCalledWith( - expect.objectContaining({ - from: expect.any(String), - }) - ); - }); - }); -}); diff --git a/packages/integration-tests/src/fixtures/test-db.ts b/packages/integration-tests/src/fixtures/test-db.ts deleted file mode 100644 index 8a4c780..0000000 --- a/packages/integration-tests/src/fixtures/test-db.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { PrismaClient } from '@shieldai/db'; - -let prisma: PrismaClient | null = null; - -export async function initializeTestDB(): Promise { - if (!prisma) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - process.env.DATABASE_URL = process.env.DATABASE_URL || 'postgresql://test:test@localhost:5432/test'; - const db = await import('@shieldai/db'); - const PC = (db as unknown as { PrismaClient: new () => PrismaClient }).PrismaClient; - prisma = new PC(); - } - return prisma; -} - -export async function cleanupTestDB(): Promise { - if (prisma) { - await prisma.$disconnect(); - prisma = null; - } -} - -export function getTestDB(): PrismaClient { - if (!prisma) { - throw new Error('Test database not initialized. Call initializeTestDB() first.'); - } - return prisma; -} diff --git a/packages/integration-tests/src/fixtures/test-fixtures.ts b/packages/integration-tests/src/fixtures/test-fixtures.ts deleted file mode 100644 index 38c938c..0000000 --- a/packages/integration-tests/src/fixtures/test-fixtures.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Subscription, SubscriptionTier } from '@shieldai/shared-billing'; -import type { EmailNotification, SMSNotification, PushNotification } from '@shieldai/shared-notifications'; - -export const TestFixtures = { - users: { - free: { id: 'user_free', email: 'free@test.com', tier: 'free' as SubscriptionTier }, - basic: { id: 'user_basic', email: 'basic@test.com', tier: 'basic' as SubscriptionTier }, - plus: { id: 'user_plus', email: 'plus@test.com', tier: 'plus' as SubscriptionTier }, - premium: { id: 'user_premium', email: 'premium@test.com', tier: 'premium' as SubscriptionTier }, - }, - - subscriptions: { - basic: { - id: 'sub_basic_1', - userId: 'user_basic', - stripeSubscriptionId: 'sub_123', - stripeCustomerId: 'cus_123', - tier: 'basic' as SubscriptionTier, - status: 'active' as const, - currentPeriodStart: new Date('2026-04-01'), - currentPeriodEnd: new Date('2026-05-01'), - cancelAtPeriodEnd: false, - createdAt: new Date('2026-04-01'), - updatedAt: new Date('2026-04-01'), - } as Subscription, - plus: { - id: 'sub_plus_1', - userId: 'user_plus', - stripeSubscriptionId: 'sub_456', - stripeCustomerId: 'cus_456', - tier: 'plus' as SubscriptionTier, - status: 'active' as const, - currentPeriodStart: new Date('2026-04-01'), - currentPeriodEnd: new Date('2026-05-01'), - cancelAtPeriodEnd: false, - createdAt: new Date('2026-04-01'), - updatedAt: new Date('2026-04-01'), - } as Subscription, - }, - - notifications: { - email: { - channel: 'email' as const, - to: 'test@example.com', - subject: 'Test Email', - htmlBody: '

          Test

          ', - textBody: 'Test', - metadata: { source: 'integration-test' }, - } as EmailNotification, - sms: { - channel: 'sms' as const, - to: '+1234567890', - body: 'Test SMS', - metadata: { source: 'integration-test' }, - } as SMSNotification, - push: { - channel: 'push' as const, - userId: 'user_plus', - title: 'Test Push', - body: 'Test notification', - data: { type: 'test' }, - badge: 1, - } as PushNotification, - }, -}; diff --git a/packages/integration-tests/src/setup.ts b/packages/integration-tests/src/setup.ts deleted file mode 100644 index 36b2612..0000000 --- a/packages/integration-tests/src/setup.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { beforeAll, afterAll, beforeEach } from '@jest/globals'; -import { PrismaClient } from '@shieldai/db'; -import { BillingService } from '@shieldai/shared-billing'; -import { EmailService, SMSService, PushService } from '@shieldai/shared-notifications'; - -// Global test setup -beforeAll(async () => { - // Initialize test database - await import('./fixtures/test-db'); - - // Initialize services with test config - process.env.STRIPE_API_KEY = 'sk_test_123'; - process.env.STRIPE_WEBHOOK_SECRET = 'whsec_123'; - process.env.RESEND_API_KEY = 're_123'; - process.env.TWILIO_ACCOUNT_SID = 'AC123'; - process.env.TWILIO_AUTH_TOKEN = 'token123'; - process.env.TWILIO_MESSAGING_SERVICE_SID = 'MG123'; - process.env.FCM_PROJECT_ID = 'test-project'; - process.env.FCM_CLIENT_EMAIL = 'test@test-project.iam.gserviceaccount.com'; - process.env.FCM_PRIVATE_KEY = '"-----BEGIN PRIVATE KEY-----\\ntest\\n-----END PRIVATE KEY-----\\n"'; - process.env.APNS_KEY = 'apns_key'; - process.env.APNS_KEY_ID = 'key_id'; - process.env.APNS_TEAM_ID = 'team_id'; - process.env.APNS_BUNDLE_ID = 'com.shieldai.app'; -}); - -beforeEach(async () => { - // Reset service state between tests - const prisma = new PrismaClient(); - await prisma.$transaction([ - prisma.subscription.deleteMany(), - prisma.notification.deleteMany(), - prisma.spamFeedback.deleteMany(), - ]); -}); - -afterAll(async () => { - // Cleanup - const prisma = new PrismaClient(); - await prisma.$disconnect(); -}); diff --git a/packages/integration-tests/tsconfig.json b/packages/integration-tests/tsconfig.json deleted file mode 100644 index f3883ae..0000000 --- a/packages/integration-tests/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "types": ["jest", "node"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/jobs/package.json b/packages/jobs/package.json deleted file mode 100644 index 868646b..0000000 --- a/packages/jobs/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@shieldai/jobs", - "version": "0.1.0", - "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsc", - "start": "node dist/index.js", - "test": "vitest run", - "test:coverage": "vitest run --coverage", - "lint": "eslint src/" - }, - "dependencies": { - "bullmq": "^5.24.0", - "@shieldai/db": "workspace:*", - "@shieldai/types": "workspace:*", - "@shieldai/darkwatch": "workspace:*", - "@shieldai/report": "workspace:*", - "@shieldai/shared-notifications": "workspace:*", - "ioredis": "^5.4.0" - }, - "devDependencies": { - "vitest": "^4.1.5", - "@vitest/coverage-v8": "^4.1.5" - } -} diff --git a/packages/jobs/src/darkwatch.jobs.ts b/packages/jobs/src/darkwatch.jobs.ts deleted file mode 100644 index 1f26a0c..0000000 --- a/packages/jobs/src/darkwatch.jobs.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { prisma, SubscriptionTier } from '@shieldai/db'; -import { Queue, Worker, Job } from 'bullmq'; -import { Redis } from 'ioredis'; -import { tierConfig, getTierFeatures } from '@shieldsai/shared-billing'; -import { mixpanelService, EventType } from '@shieldsai/shared-analytics'; - -const redisHost = process.env.REDIS_HOST || 'localhost'; -const redisPort = parseInt(process.env.REDIS_PORT || '6379', 10); - -const connection = new Redis({ - host: redisHost, - port: redisPort, - retryStrategy: (times: number) => Math.min(times * 50, 2000), -}); - -const QUEUE_CONFIG = { - darkwatchScan: { - name: 'darkwatch-scan', - concurrency: parseInt(process.env.DARKWATCH_CONCURRENCY || '5', 10), - defaultJobTimeout: parseInt(process.env.DARKWATCH_JOB_TIMEOUT || '120000', 10), - maxAttempts: parseInt(process.env.DARKWATCH_MAX_ATTEMPTS || '3', 10), - }, -}; - -export const darkwatchScanQueue = new Queue( - QUEUE_CONFIG.darkwatchScan.name, - { connection } -); - -async function processDarkwatchScan( - job: Job<{ - subscriptionId: string; - tier: string; - scanType: 'scheduled' | 'on-demand' | 'realtime'; - sourceData?: Record; - }> -) { - const { subscriptionId, tier, scanType, sourceData } = job.data; - - const { scanService } = await import( - '../../../apps/api/src/services/darkwatch/scan.service' - ); - const { alertPipeline } = await import( - '../../../apps/api/src/services/darkwatch/alert.pipeline' - ); - - job.updateProgress(10); - console.log( - `[DarkWatch:Scan] Starting ${scanType} scan for subscription ${subscriptionId} (tier: ${tier})` - ); - - try { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { userId: true, tier: true }, - }); - - if (!subscription) { - job.updateProgress(100); - return { status: 'skipped', reason: 'subscription_not_found' }; - } - - await mixpanelService.track( - EventType.DARK_WEB_SCAN_STARTED, - subscription.userId, - { - scanType, - subscriptionTier: subscription.tier, - } - ); - - job.updateProgress(25); - - const watchlistItems = await scanService.getWatchlistItems(subscriptionId); - - if (watchlistItems.length === 0) { - job.updateProgress(100); - return { status: 'completed', exposuresCreated: 0, exposuresUpdated: 0 }; - } - - job.updateProgress(50); - - const { exposuresCreated, exposuresUpdated } = - await scanService.processSubscriptionScan(subscriptionId, watchlistItems); - - job.updateProgress(80); - - const newExposureIds = await prisma.exposure.findMany({ - where: { - subscriptionId, - isFirstTime: true, - detectedAt: { gte: new Date(Date.now() - 5 * 60 * 1000) }, - }, - select: { id: true }, - }); - - if (newExposureIds.length > 0) { - await alertPipeline.processNewExposures(newExposureIds.map((e) => e.id)); - } - - await alertPipeline.dispatchScanCompleteAlert( - subscriptionId, - subscription.userId, - exposuresCreated - ); - - job.updateProgress(95); - - await mixpanelService.track( - EventType.DARK_WEB_SCAN_COMPLETED, - subscription.userId, - { - scanType, - subscriptionTier: subscription.tier, - exposuresCreated, - exposuresUpdated, - watchlistItemsScanned: watchlistItems.length, - } - ); - - job.updateProgress(100); - - return { - status: 'completed', - exposuresCreated, - exposuresUpdated, - watchlistItemsScanned: watchlistItems.length, - }; - } catch (error) { - const message = error instanceof Error ? error.message : 'Scan failed'; - console.error(`[DarkWatch:Scan] Job ${job.id} failed:`, message); - job.updateProgress(100); - throw new Error(message); - } -} - -export const darkwatchScanWorker = new Worker( - QUEUE_CONFIG.darkwatchScan.name, - processDarkwatchScan, - { - connection, - concurrency: QUEUE_CONFIG.darkwatchScan.concurrency, - limiter: { - max: 20, - duration: 1000, - }, - removeOnComplete: { - age: 7 * 24 * 60 * 60, - count: 1000, - }, - removeOnFail: { - age: 30 * 24 * 60 * 60, - count: 100, - }, - } -); - -darkwatchScanWorker.on('completed', (job, result) => { - console.log(`[DarkWatch:Scan] Job ${job.id} completed:`, result); -}); - -darkwatchScanWorker.on('failed', (job, err) => { - console.error(`[DarkWatch:Scan] Job ${job?.id} failed:`, err.message); -}); - -darkwatchScanWorker.on('error', (err) => { - console.error('[DarkWatch:Scan] Worker error:', err.message); -}); - -export default { - darkwatchScanQueue, - darkwatchScanWorker, -}; diff --git a/packages/jobs/src/index.ts b/packages/jobs/src/index.ts deleted file mode 100644 index 8221da5..0000000 --- a/packages/jobs/src/index.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { Queue, Worker } from "bullmq"; -import { Redis } from "ioredis"; -import { ScanService, ScanScheduler, WebhookHandler } from "@shieldai/darkwatch"; -import { AlertPipeline } from "@shieldai/darkwatch"; - -const redisUrl = process.env.REDIS_URL || "redis://localhost:6379"; -const connection = new Redis(redisUrl); - -const scanQueue = new Queue("darkwatch-scans", { connection }); -const alertQueue = new Queue("darkwatch-alerts", { connection }); -const scheduleQueue = new Queue("darkwatch-scheduler", { connection }); - -const scanWorker = new Worker( - "darkwatch-scans", - async (job) => { - const { userId, source } = job.data; - const scanService = new ScanService(); - const resultCount = await scanService.runScan(userId, source); - return { resultCount, completedAt: new Date().toISOString() }; - }, - { connection, concurrency: 3 } -); - -const alertWorker = new Worker( - "darkwatch-alerts", - async () => { - const pipeline = new AlertPipeline(); - const sent = await pipeline.sendPendingAlerts(); - return { sent, processedAt: new Date().toISOString() }; - }, - { connection, concurrency: 1 } -); - -const scheduleWorker = new Worker( - "darkwatch-scheduler", - async () => { - const scheduler = new ScanScheduler(); - const dueSchedules = await scheduler.getDueSchedules(); - const results: Array<{ userId: string; queued: boolean }> = []; - - for (const schedule of dueSchedules) { - try { - await scanQueue.add("scheduled-scan", { - userId: schedule.userId, - source: undefined, - }, { - attempts: 3, - backoff: { type: "exponential", delay: 5000 }, - jobId: `scheduled-scan-${schedule.userId}-${Date.now()}`, - }); - - await scheduler.markScanned(schedule.userId); - results.push({ userId: schedule.userId, queued: true }); - } catch (err) { - console.error(`[Scheduler] Failed to queue scan for ${schedule.userId}:`, err); - results.push({ userId: schedule.userId, queued: false }); - } - } - - return { processed: results.length, completedAt: new Date().toISOString() }; - }, - { connection, concurrency: 1 } -); - -const webhookWorker = new Worker( - "darkwatch-webhooks", - async () => { - const handler = new WebhookHandler(); - const processed = await handler.processPendingEvents(); - return { processed, completedAt: new Date().toISOString() }; - }, - { connection, concurrency: 1 } -); - -scanWorker.on("completed", (job) => { - console.log(`[Scan] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`); -}); - -scanWorker.on("failed", (job, err) => { - console.error(`[Scan] Job ${job?.id} failed: ${err.message}`); -}); - -alertWorker.on("completed", (job) => { - console.log(`[Alert] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`); -}); - -alertWorker.on("failed", (job, err) => { - console.error(`[Alert] Job ${job?.id} failed: ${err.message}`); -}); - -scheduleWorker.on("completed", (job) => { - console.log(`[Scheduler] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`); -}); - -scheduleWorker.on("failed", (job, err) => { - console.error(`[Scheduler] Job ${job?.id} failed: ${err.message}`); -}); - -webhookWorker.on("completed", (job) => { - console.log(`[Webhook] Job ${job?.id} completed: ${JSON.stringify(job?.returnvalue)}`); -}); - -webhookWorker.on("failed", (job, err) => { - console.error(`[Webhook] Job ${job?.id} failed: ${err.message}`); -}); - -export async function addScanJob(userId: string, source?: string) { - return scanQueue.add("scan", { userId, source }, { - attempts: 3, - backoff: { type: "exponential", delay: 5000 }, - jobId: `scan-${userId}-${Date.now()}`, - }); -} - -export async function scheduleAlertProcessing() { - return alertQueue.add("process-alerts", {}, { - repeat: { pattern: "*/5 * * * *" }, - jobId: "alert-processor-recurring", - }); -} - -export async function schedulePeriodicScanCheck() { - return scheduleQueue.add("check-due-scans", {}, { - repeat: { pattern: "*/10 * * * *" }, - jobId: "scheduler-recurring", - }); -} - -export async function scheduleWebhookProcessor() { - const webhookQueue = new Queue("darkwatch-webhooks", { connection }); - return webhookQueue.add("process-pending-webhooks", {}, { - repeat: { pattern: "*/2 * * * *" }, - jobId: "webhook-processor-recurring", - }); -} - -// Waitlist email worker -import { EmailService } from '@shieldai/shared-notifications'; - -const waitlistEmailWorker = new Worker( - "waitlist-emails", - async (job) => { - const { email, name, entryId } = job.data; - const templateIdMap: Record = { - 'send-waitlist-intro': 'waitlist_intro', - 'send-waitlist-features': 'waitlist_features', - 'send-waitlist-launch-teaser': 'waitlist_launch_teaser', - }; - - const templateId = templateIdMap[job.name]; - if (!templateId) { - throw new Error(`Unknown waitlist email job: ${job.name}`); - } - - const emailService = EmailService.getInstance(); - const result = await emailService.sendWithTemplate(email, { - templateId, - variables: { name, entryId }, - }); - - if (result.status === 'failed') { - throw new Error(`Failed to send ${templateId} to ${email}: ${result.error}`); - } - - return { templateId, email, deliveredAt: result.deliveredAt }; - }, - { connection, concurrency: 5 } -); - -waitlistEmailWorker.on("completed", (job) => { - console.log(`[WaitlistEmail] Job ${job?.id} (${job?.name}) completed for ${job?.data?.email}`); -}); - -waitlistEmailWorker.on("failed", (job, err) => { - console.error(`[WaitlistEmail] Job ${job?.id} (${job?.name}) failed: ${err.message}`); -}); - -console.log("Job workers started"); - -// Report generation workers -import { - reportGenerationWorker, - reportSchedulerWorker, - scheduleReportProcessor, - scheduleMonthlyReportTrigger, - scheduleAnnualReportTrigger, -} from './report.jobs'; - -scheduleReportProcessor().catch(console.error); -scheduleMonthlyReportTrigger().catch(console.error); -scheduleAnnualReportTrigger().catch(console.error); diff --git a/packages/jobs/src/report.jobs.ts b/packages/jobs/src/report.jobs.ts deleted file mode 100644 index d43adcf..0000000 --- a/packages/jobs/src/report.jobs.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { prisma } from '@shieldai/db'; -import { Queue, Worker, Job } from 'bullmq'; -import { Redis } from 'ioredis'; -import { reportService } from '@shieldai/report'; - -const redisHost = process.env.REDIS_HOST || 'localhost'; -const redisPort = parseInt(process.env.REDIS_PORT || '6379', 10); - -const connection = new Redis({ - host: redisHost, - port: redisPort, - retryStrategy: (times: number) => Math.min(times * 50, 2000), -}); - -const QUEUE_CONFIG = { - reportGeneration: { - name: 'report-generation', - concurrency: parseInt(process.env.REPORT_CONCURRENCY || '3', 10), - defaultJobTimeout: parseInt(process.env.REPORT_JOB_TIMEOUT || '30000', 10), - maxAttempts: parseInt(process.env.REPORT_MAX_ATTEMPTS || '2', 10), - }, - reportScheduler: { - name: 'report-scheduler', - concurrency: 1, - }, -}; - -export const reportGenerationQueue = new Queue( - QUEUE_CONFIG.reportGeneration.name, - { connection } -); - -export const reportSchedulerQueue = new Queue( - QUEUE_CONFIG.reportScheduler.name, - { connection } -); - -async function processReportGeneration( - job: Job<{ - reportId: string; - userId: string; - subscriptionId: string; - reportType: string; - periodStart?: string; - periodEnd?: string; - notifyEmail?: string; - }> -) { - const { reportId, userId, subscriptionId, reportType, periodStart, periodEnd, notifyEmail } = job.data; - - job.updateProgress(10); - console.log(`[Report:Generate] Starting report ${reportId} for user ${userId}`); - - try { - const report = await reportService.generateReport({ - userId, - subscriptionId, - reportType, - periodStart: periodStart ? new Date(periodStart) : undefined, - periodEnd: periodEnd ? new Date(periodEnd) : undefined, - }); - - job.updateProgress(80); - - if (notifyEmail && report.status === 'COMPLETED') { - const { EmailService } = await import('@shieldai/shared-notifications'); - const emailService = EmailService.getInstance(); - - const dashboardUrl = process.env.DASHBOARD_URL || 'https://app.shieldai.com'; - - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { name: true, email: true }, - }); - - const userName = user?.name || notifyEmail.split('@')[0]; - - const templateId = report.reportType === 'WEEKLY_DIGEST' ? 'weekly_digest' : 'report_ready'; - - if (report.reportType === 'WEEKLY_DIGEST' && report.dataPayload) { - const payload = typeof report.dataPayload === 'string' ? JSON.parse(report.dataPayload) : report.dataPayload; - - await emailService.sendWithTemplate(notifyEmail, { - templateId: 'weekly_digest', - variables: { - name: userName, - period_start: new Date(report.periodStart).toLocaleDateString('en-US', { month: 'long', day: 'numeric' }), - period_end: new Date(report.periodEnd).toLocaleDateString('en-US', { month: 'long', day: 'numeric' }), - protection_score: payload.protectionScore || 0, - new_exposures: payload.exposureSummary?.newExposures || 0, - critical_exposures: payload.exposureSummary?.criticalExposures || 0, - spam_events_blocked: payload.spamStats?.totalSpamEvents || 0, - calls_blocked: payload.spamStats?.callsBlocked || 0, - texts_blocked: payload.spamStats?.textsBlocked || 0, - voice_threats: payload.voiceStats?.threatsDetected || 0, - enrollments_active: payload.voiceStats?.enrollmentsActive || 0, - properties_monitored: payload.homeTitleStats?.propertiesMonitored || 0, - changes_detected: payload.homeTitleStats?.changesDetected || 0, - report_url: `${dashboardUrl}/reports/${report.id}`, - }, - }); - } else { - await emailService.sendWithTemplate(notifyEmail, { - templateId: 'report_ready', - variables: { - name: userName, - report_title: report.title, - report_summary: report.summary || 'Your protection report contains detailed statistics and recommendations.', - report_url: `${dashboardUrl}/reports/${report.id}`, - pdf_url: report.pdfUrl || `${dashboardUrl}/api/v1/reports/${report.id}/pdf`, - }, - }); - } - - await prisma.securityReport.update({ - where: { id: report.id }, - data: { - status: 'DELIVERED', - deliveredAt: new Date(), - }, - }); - - job.updateProgress(95); - } - - job.updateProgress(100); - - return { - status: report.status, - reportId: report.id, - title: report.title, - }; - } catch (error) { - const message = error instanceof Error ? error.message : 'Report generation failed'; - console.error(`[Report:Generate] Job ${job.id} failed:`, message); - - await prisma.securityReport.update({ - where: { id: reportId }, - data: { - status: 'FAILED', - error: message, - }, - }); - - job.updateProgress(100); - throw new Error(message); - } -} - -async function processReportScheduler(job: Job) { - console.log('[Report:Scheduler] Running scheduled report check'); - - try { - const pendingReports = await prisma.securityReport.findMany({ - where: { - status: 'PENDING', - scheduledFor: { - lte: new Date(), - }, - }, - include: { - user: { select: { email: true } }, - }, - }); - - const results: Array<{ reportId: string; queued: boolean }> = []; - - for (const report of pendingReports) { - try { - await reportGenerationQueue.add('generate-report', { - reportId: report.id, - userId: report.userId, - subscriptionId: report.subscriptionId, - reportType: report.reportType, - periodStart: report.periodStart.toISOString(), - periodEnd: report.periodEnd.toISOString(), - notifyEmail: report.user?.email, - }, { - attempts: QUEUE_CONFIG.reportGeneration.maxAttempts, - backoff: { type: 'exponential', delay: 5000 }, - jobId: `report-gen-${report.id}`, - }); - - results.push({ reportId: report.id, queued: true }); - } catch (err) { - console.error(`[Report:Scheduler] Failed to queue report ${report.id}:`, err); - results.push({ reportId: report.id, queued: false }); - } - } - - return { processed: results.length, completedAt: new Date().toISOString() }; - } catch (error) { - console.error('[Report:Scheduler] Error:', error); - throw error; - } -} - -export const reportGenerationWorker = new Worker( - QUEUE_CONFIG.reportGeneration.name, - processReportGeneration, - { - connection, - concurrency: QUEUE_CONFIG.reportGeneration.concurrency, - removeOnComplete: { - age: 7 * 24 * 60 * 60, - count: 500, - }, - removeOnFail: { - age: 30 * 24 * 60 * 60, - count: 100, - }, - } -); - -export const reportSchedulerWorker = new Worker( - QUEUE_CONFIG.reportScheduler.name, - processReportScheduler, - { - connection, - concurrency: QUEUE_CONFIG.reportScheduler.concurrency, - } -); - -reportGenerationWorker.on('completed', (job, result) => { - console.log(`[Report:Generate] Job ${job.id} completed:`, result); -}); - -reportGenerationWorker.on('failed', (job, err) => { - console.error(`[Report:Generate] Job ${job?.id} failed:`, err.message); -}); - -reportGenerationWorker.on('error', (err) => { - console.error('[Report:Generate] Worker error:', err.message); -}); - -reportSchedulerWorker.on('completed', (job, result) => { - console.log(`[Report:Scheduler] Job ${job.id} completed:`, result); -}); - -reportSchedulerWorker.on('failed', (job, err) => { - console.error(`[Report:Scheduler] Job ${job?.id} failed:`, err.message); -}); - -export async function queueReportGeneration(data: { - reportId: string; - userId: string; - subscriptionId: string; - reportType: string; - periodStart?: string; - periodEnd?: string; - notifyEmail?: string; -}) { - return reportGenerationQueue.add('generate-report', data, { - attempts: QUEUE_CONFIG.reportGeneration.maxAttempts, - backoff: { type: 'exponential', delay: 5000 }, - jobId: `report-gen-${data.reportId}-${Date.now()}`, - }); -} - -export async function scheduleReportProcessor() { - return reportSchedulerQueue.add('check-pending-reports', {}, { - repeat: { pattern: '0 */6 * * *' }, - jobId: 'report-scheduler-recurring', - }); -} - -export async function scheduleMonthlyReportTrigger() { - return reportSchedulerQueue.add('trigger-monthly-reports', {}, { - repeat: { pattern: '0 0 1 * *' }, - jobId: 'monthly-report-trigger', - }); -} - -export async function scheduleWeeklyDigestTrigger() { - return reportSchedulerQueue.add('trigger-weekly-digest', {}, { - repeat: { pattern: '0 8 * * 1' }, - jobId: 'weekly-digest-trigger', - }); -} - -export async function scheduleAnnualReportTrigger() { - return reportSchedulerQueue.add('trigger-annual-reports', {}, { - repeat: { pattern: '0 0 1 1 *' }, - jobId: 'annual-report-trigger', - }); -} - -export default { - reportGenerationQueue, - reportGenerationWorker, - reportSchedulerQueue, - reportSchedulerWorker, -}; diff --git a/packages/jobs/src/voiceprint.jobs.ts b/packages/jobs/src/voiceprint.jobs.ts deleted file mode 100644 index 9398a63..0000000 --- a/packages/jobs/src/voiceprint.jobs.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Queue, Worker, Job } from "bullmq"; -import { Redis } from "ioredis"; - -const redisUrl = process.env.REDIS_URL || "redis://localhost:6379"; -const { host, port } = new URL(redisUrl); -const connection = new Redis({ - host, - port: parseInt(port, 10), - retryStrategy: (times: number) => { - const maxAttempts = parseInt(process.env.VOICEPRINT_MAX_RETRIES || "5", 10); - const delay = Math.min(times * 1000, 5000); - return times < maxAttempts ? delay : null; - }, -}); - -const analysisQueue = new Queue("voiceprint-analysis", { connection }); - -const VOICEPRINT_CONFIG = { - concurrency: parseInt(process.env.VOICEPRINT_CONCURRENCY || "2", 10), - maxAttempts: parseInt(process.env.VOICEPRINT_MAX_ATTEMPTS || "3", 10), - defaultBackoffDelay: parseInt(process.env.VOICEPRINT_BACKOFF_DELAY || "5000", 10), -}; - -export function createAnalysisWorker(): Worker { - const analysisWorker = new Worker( - "voiceprint-analysis", - async (job: Job) => { - const { userId, audioBuffer, sampleRate, analysisType } = job.data; - - // Import AnalysisService within job handler to avoid circular deps - const { analysisService: { analyze } } = await import("@shieldai/voiceprint"); - - const decodedAudio = Buffer.from(audioBuffer, "base64"); - const result = await analyze(userId, decodedAudio, { - enrollmentId: undefined, - audioUrl: undefined, - }); - - return { jobId: result.id, completedAt: new Date().toISOString() }; - }, - { - connection, - concurrency: VOICEPRINT_CONFIG.concurrency, - removeOnComplete: { - age: 7 * 24 * 60 * 60 * 1000, // 7 days - count: 1000, - }, - removeOnFail: { - age: 30 * 24 * 60 * 60 * 1000, // 30 days - count: 500, - }, - } - ); - - analysisWorker.on("completed", (job: Job | undefined) => { - if (job) { - console.log(`[VoicePrint] Job ${job.id} completed: ${JSON.stringify(job.returnvalue)}`); - } - }); - - analysisWorker.on("failed", (job: Job | undefined, err: Error) => { - if (job) { - console.error(`[VoicePrint] Job ${job.id} failed: ${err.message}`); - } - }); - - analysisWorker.on("error", (err: Error) => { - console.error("[VoicePrint] Worker error:", err.message); - }); - - return analysisWorker; -} - -export async function addAnalysisJob( - userId: string, - audioBuffer: Buffer, - sampleRate?: number, - analysisType?: string -) { - return analysisQueue.add("analyze", { - userId, - audioBuffer: audioBuffer.toString("base64"), - sampleRate, - analysisType, - }, { - attempts: 3, - backoff: { type: "exponential", delay: 5000 }, - jobId: `vp-${userId}-${Date.now()}`, - }); -} - diff --git a/packages/jobs/tsconfig.json b/packages/jobs/tsconfig.json deleted file mode 100644 index e459899..0000000 --- a/packages/jobs/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/jobs/vitest.config.ts b/packages/jobs/vitest.config.ts deleted file mode 100644 index fe01a0a..0000000 --- a/packages/jobs/vitest.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['src/**/*.test.ts', 'test/**/*.test.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - reportsDirectory: './coverage', - include: ['src/**/*.ts'], - exclude: [ - 'src/**/*.d.ts', - '**/node_modules/**', - '**/test/**', - ], - thresholds: { - statements: 80, - branches: 80, - functions: 80, - lines: 80, - }, - }, - }, -}); diff --git a/packages/mobile-api-client/README.md b/packages/mobile-api-client/README.md deleted file mode 100644 index 32f16aa..0000000 --- a/packages/mobile-api-client/README.md +++ /dev/null @@ -1,237 +0,0 @@ -# @shieldai/mobile-api-client - -React Native API client library for ShieldAI services. Provides type-safe access to all API endpoints with built-in authentication, offline support, and error handling. - -## Installation - -```bash -npm install @shieldai/mobile-api-client -# or -yarn add @shieldai/mobile-api-client -``` - -## Setup - -### Initialize the client - -```typescript -import { createApiClient } from '@shieldai/mobile-api-client'; - -createApiClient({ - baseURL: 'https://api.shieldai.freno.me/api/v1', - timeout: 30000, - debug: __DEV__, // Enable debug logging in development -}); -``` - -### Authentication - -```typescript -import { authService } from '@shieldai/mobile-api-client'; - -// Login -const { user, tokens } = await authService.login({ - email: 'user@example.com', - password: 'password123', -}); - -// Register -const { user: newUser } = await authService.register({ - email: 'user@example.com', - password: 'password123', - firstName: 'John', - lastName: 'Doe', -}); - -// Get current user -const currentUser = await authService.getCurrentUser(); - -// Logout -await authService.logout(); - -// Check authentication status -const isAuthenticated = await authService.isAuthenticated(); -``` - -### Device Management - -```typescript -import { deviceService } from '@shieldai/mobile-api-client'; -import * as Notifications from 'expo-notifications'; - -// Register device for push notifications -async function registerForPushNotifications() { - const token = (await Notifications.getExpoPushTokenAsync({ - projectId: 'your-project-id', - })).data; - - await deviceService.registerDevice({ - platform: Platform.OS === 'ios' ? 'ios' : 'android', - pushToken: token, - modelName: Platform.OS === 'ios' ? 'iPhone' : 'Android', - osVersion: Platform.Version.toString(), - appVersion: '1.0.0', - }); -} - -// Get all user devices -const { devices } = await deviceService.getDevices(); - -// Update push token -await deviceService.updatePushToken('new-token'); -``` - -### Subscriptions - -```typescript -import { subscriptionService } from '@shieldai/mobile-api-client'; - -// Get current subscription -const { subscription, tier, usage } = await subscriptionService.getSubscription(); - -// Get available tiers -const tiers = await subscriptionService.getTiers(); - -// Create subscription -const newSubscription = await subscriptionService.createSubscription({ - tier: 'premium', -}); - -// Update subscription -await subscriptionService.updateSubscription({ - tier: 'enterprise', -}); - -// Cancel subscription -await subscriptionService.cancelSubscription(); - -// Create checkout session -const { url } = await subscriptionService.createCheckoutSession('premium'); -Linking.openURL(url); - -// Create customer portal session -const { url: portalUrl } = await subscriptionService.createCustomerPortalSession(); -Linking.openURL(portalUrl); -``` - -### Notifications - -```typescript -import { notificationService } from '@shieldai/mobile-api-client'; - -// Get notifications -const { notifications, unreadCount } = await notificationService.getNotifications({ - page: 1, - limit: 20, - unreadOnly: false, -}); - -// Mark as read -await notificationService.markAsRead(notificationId); - -// Mark all as read -await notificationService.markAllAsRead(); - -// Get unread count -const count = await notificationService.getUnreadCount(); - -// Update preferences -await notificationService.updatePreferences({ - emailNotifications: true, - pushNotifications: true, - notificationTypes: { - darkwatch_alert: true, - spam_blocked: true, - voiceprint_analysis: true, - }, -}); -``` - -## Features - -### Automatic Token Refresh - -The client automatically handles JWT token refresh when access tokens expire: - -```typescript -// No manual handling needed - just make the request -const user = await authService.getCurrentUser(); -// If token expired, it will be refreshed automatically -``` - -### Offline Support - -Requests are automatically queued when offline and replayed when connection is restored: - -```typescript -import { requestQueue } from '@shieldai/mobile-api-client'; - -// Subscribe to queue status changes -const unsubscribe = requestQueue.subscribe(() => { - const status = requestQueue.getStatus(); - console.log(`Queued requests: ${status.size}`); -}); - -// Cleanup -unsubscribe(); -``` - -### Error Handling - -```typescript -import { authService } from '@shieldai/mobile-api-client'; - -try { - await authService.login({ email, password }); -} catch (error) { - if (error.response?.status === 401) { - // Invalid credentials - } else if (error.response?.status === 422) { - // Validation error - } else if (error.offline) { - // Offline mode - request queued - } else { - // Network error - } -} -``` - -## API Reference - -### Services - -- `authService` - Authentication and user management -- `deviceService` - Device registration and push tokens -- `subscriptionService` - Billing and subscription management -- `notificationService` - Push notifications and preferences - -### Types - -All TypeScript types are exported for type-safe development: - -```typescript -import type { User, Device, Subscription, Notification } from '@shieldai/mobile-api-client'; -``` - -## Development - -```bash -# Install dependencies -npm install - -# Build -npm run build - -# Watch mode -npm run dev - -# Type check -npx tsc --noEmit - -# Lint -npm run lint -``` - -## License - -MIT diff --git a/packages/mobile-api-client/package.json b/packages/mobile-api-client/package.json deleted file mode 100644 index 04bb47b..0000000 --- a/packages/mobile-api-client/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@shieldai/mobile-api-client", - "version": "1.0.0", - "description": "React Native API client library for ShieldAI services", - "main": "dist/index.js", - "types": "dist/index.d.ts", - "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "lint": "eslint src/", - "test": "jest" - }, - "keywords": ["react-native", "api-client", "shieldai"], - "author": "ShieldAI Team", - "license": "MIT", - "peerDependencies": { - "react-native": ">=0.72.0", - "expo": ">=49.0.0" - }, - "dependencies": { - "expo-secure-store": "^12.8.0", - "@react-native-async-storage/async-storage": "1.23.1", - "axios": "^1.6.0" - }, - "devDependencies": { - "@types/react": "^18.2.0", - "typescript": "^5.3.0" - } -} diff --git a/packages/mobile-api-client/src/api/api-client.ts b/packages/mobile-api-client/src/api/api-client.ts deleted file mode 100644 index bf095a6..0000000 --- a/packages/mobile-api-client/src/api/api-client.ts +++ /dev/null @@ -1,249 +0,0 @@ -/** - * API Client for ShieldAI services - * Handles authentication, request/response interception, and error handling - */ - -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios'; -import { tokenStorage } from '../storage/token-storage'; -import { requestQueue } from '../utils/request-queue'; -import type { AuthTokens, AuthResponse, RefreshTokenRequest } from '../types'; - -export interface ApiClientConfig { - baseURL: string; - timeout?: number; - debug?: boolean; -} - -export class ApiClient { - private client: AxiosInstance; - private config: ApiClientConfig; - private isRefreshing = false; - private refreshSubscribers: Set<(token: string) => void> = new Set(); - - constructor(config: ApiClientConfig) { - this.config = { - baseURL: config.baseURL, - timeout: config.timeout ?? 30000, - debug: config.debug ?? false, - }; - - this.client = axios.create({ - baseURL: this.config.baseURL, - timeout: this.config.timeout, - headers: { - 'Content-Type': 'application/json', - }, - }); - - this.setupInterceptors(); - } - - private setupInterceptors(): void { - // Request interceptor - add auth token - this.client.interceptors.request.use( - async (config) => { - if (this.config.debug) { - console.log('[API] Request:', config.method?.toUpperCase(), config.url); - } - - // Add auth token if available - const token = await tokenStorage.getAccessToken(); - if (token && config.headers) { - config.headers.Authorization = `Bearer ${token}`; - } - - // Queue request if offline - if (!requestQueue.isOnline() && this.requiresNetwork(config)) { - await requestQueue.enqueue(config); - throw new Error('OFFLINE'); - } - - return config; - }, - (error) => { - if (error.message === 'OFFLINE') { - return Promise.reject({ offline: true, config: error.config }); - } - return Promise.reject(error); - } - ); - - // Response interceptor - handle errors and token refresh - this.client.interceptors.response.use( - (response) => { - if (this.config.debug) { - console.log('[API] Response:', response.status, response.config.url); - } - return response; - }, - async (error: AxiosError) => { - const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }; - - // Handle 401 - unauthorized - if (error.response?.status === 401 && !originalRequest._retry) { - if (this.isRefreshing) { - // Wait for refresh to complete - return new Promise((resolve) => { - this.refreshSubscribers.add((token) => { - originalRequest.headers = originalRequest.headers || {}; - originalRequest.headers.Authorization = `Bearer ${token}`; - resolve(this.client(originalRequest)); - }); - }); - } - - originalRequest._retry = true; - this.isRefreshing = true; - - try { - const refreshToken = await tokenStorage.getRefreshToken(); - if (!refreshToken) { - throw new Error('No refresh token'); - } - - const newTokens = await this.refreshAccessToken(refreshToken); - await tokenStorage.saveTokens(newTokens.accessToken, newTokens.refreshToken); - - // Retry failed requests - this.refreshSubscribers.forEach((callback) => callback(newTokens.accessToken)); - this.refreshSubscribers.clear(); - - originalRequest.headers = originalRequest.headers || {}; - originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`; - - return this.client(originalRequest); - } catch (refreshError) { - // Refresh failed - clear tokens and redirect to login - await tokenStorage.clearTokens(); - this.refreshSubscribers.clear(); - return Promise.reject(refreshError); - } finally { - this.isRefreshing = false; - } - } - - return Promise.reject(error); - } - ); - } - - private requiresNetwork(config: AxiosRequestConfig): boolean { - // Don't queue GET requests that can be cached - const method = (config.method || 'get').toLowerCase(); - return method !== 'get'; - } - - private async refreshAccessToken(refreshToken: string): Promise { - const response = await this.client.post('/auth/refresh', { - refreshToken, - }); - - return { - accessToken: response.data.tokens.accessToken, - refreshToken: response.data.tokens.refreshToken, - expiresIn: response.data.tokens.expiresIn, - tokenType: response.data.tokens.tokenType, - }; - } - - // Subscribe to token refresh - onTokenRefresh(callback: (token: string) => void): () => void { - this.refreshSubscribers.add(callback); - return () => this.refreshSubscribers.delete(callback); - } - - // Public API methods - async get(url: string, config?: AxiosRequestConfig): Promise> { - return this.client.get(url, config); - } - - async post(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { - return this.client.post(url, data, config); - } - - async put(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { - return this.client.put(url, data, config); - } - - async patch(url: string, data?: unknown, config?: AxiosRequestConfig): Promise> { - return this.client.patch(url, data, config); - } - - async delete(url: string, config?: AxiosRequestConfig): Promise> { - return this.client.delete(url, config); - } - - // Auth methods - async login(email: string, password: string): Promise { - const response = await this.post('/auth/login', { - email, - password, - }); - - if (response.data.tokens) { - await tokenStorage.saveTokens( - response.data.tokens.accessToken, - response.data.tokens.refreshToken - ); - } - - return response.data; - } - - async register(data: { - email: string; - password: string; - firstName?: string; - lastName?: string; - }): Promise { - const response = await this.post('/auth/register', data); - - if (response.data.tokens) { - await tokenStorage.saveTokens( - response.data.tokens.accessToken, - response.data.tokens.refreshToken - ); - } - - return response.data; - } - - async logout(): Promise { - try { - await this.post('/auth/logout'); - } finally { - await tokenStorage.clearTokens(); - } - } - - async isAuthenticated(): Promise { - const token = await tokenStorage.getAccessToken(); - return !!token; - } - - // Health check - async healthCheck(): Promise<{ status: string; version: string }> { - const response = await this.get<{ status: string; version: string }>('/health'); - return response.data; - } - - // Get the underlying axios instance for advanced usage - getClient(): AxiosInstance { - return this.client; - } -} - -// Default client instance -let defaultClient: ApiClient | null = null; - -export const createApiClient = (config: ApiClientConfig): ApiClient => { - defaultClient = new ApiClient(config); - return defaultClient; -}; - -export const getApiClient = (): ApiClient => { - if (!defaultClient) { - throw new Error('API Client not initialized. Call createApiClient first.'); - } - return defaultClient; -}; diff --git a/packages/mobile-api-client/src/api/auth.service.ts b/packages/mobile-api-client/src/api/auth.service.ts deleted file mode 100644 index 36b7c23..0000000 --- a/packages/mobile-api-client/src/api/auth.service.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Authentication API service - */ - -import { getApiClient } from './api-client'; -import type { - User, - AuthResponse, - LoginCredentials, - RegisterData, - AuthTokens -} from '../types'; - -export class AuthService { - private api = getApiClient(); - - async login(credentials: LoginCredentials): Promise { - const response = await this.api.login(credentials.email, credentials.password); - return response; - } - - async register(data: RegisterData): Promise { - const response = await this.api.register(data); - return response; - } - - async logout(): Promise { - await this.api.logout(); - } - - async getCurrentUser(): Promise { - const response = await this.api.get<{ user: User; authType: string }>('/auth/user/me'); - return response.data.user; - } - - async refreshToken(): Promise { - const response = await this.api.get('/auth/refresh-token'); - return response.data; - } - - async isAuthenticated(): Promise { - return await this.api.isAuthenticated(); - } -} - -export const authService = new AuthService(); diff --git a/packages/mobile-api-client/src/api/device.service.ts b/packages/mobile-api-client/src/api/device.service.ts deleted file mode 100644 index d06173f..0000000 --- a/packages/mobile-api-client/src/api/device.service.ts +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Device API service - */ - -import { getApiClient } from './api-client'; -import type { Device, DeviceRegistration, DeviceListResponse } from '../types'; - -export class DeviceService { - private api = getApiClient(); - - async registerDevice(data: DeviceRegistration): Promise { - const response = await this.api.post('/api/v1/devices/register', data); - return response.data; - } - - async updatePushToken(pushToken: string): Promise { - const response = await this.api.patch('/api/v1/devices/push-token', { - pushToken, - }); - return response.data; - } - - async getDevices(): Promise { - const response = await this.api.get('/api/v1/devices'); - return response.data; - } - - async getDevice(deviceId: string): Promise { - const response = await this.api.get(`/api/v1/devices/${deviceId}`); - return response.data; - } - - async deleteDevice(deviceId: string): Promise { - await this.api.delete(`/api/v1/devices/${deviceId}`); - } - - async getCurrentDevice(): Promise { - try { - const response = await this.api.get('/api/v1/devices/current'); - return response.data; - } catch { - return null; - } - } -} - -export const deviceService = new DeviceService(); diff --git a/packages/mobile-api-client/src/api/notification.service.ts b/packages/mobile-api-client/src/api/notification.service.ts deleted file mode 100644 index 0cc2cfa..0000000 --- a/packages/mobile-api-client/src/api/notification.service.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Notification API service - */ - -import { getApiClient } from './api-client'; -import type { Notification, NotificationListResponse, NotificationPreferences } from '../types'; - -export class NotificationService { - private api = getApiClient(); - - async getNotifications(params?: { - page?: number; - limit?: number; - unreadOnly?: boolean; - }): Promise { - const response = await this.api.get('/notifications', { params }); - return response.data; - } - - async getNotification(notificationId: string): Promise { - const response = await this.api.get(`/notifications/${notificationId}`); - return response.data; - } - - async markAsRead(notificationId: string): Promise { - await this.api.patch(`/notifications/${notificationId}/read`); - } - - async markAllAsRead(): Promise { - await this.api.post('/notifications/read-all'); - } - - async deleteNotification(notificationId: string): Promise { - await this.api.delete(`/notifications/${notificationId}`); - } - - async getPreferences(): Promise { - const response = await this.api.get('/notifications/preferences'); - return response.data; - } - - async updatePreferences(preferences: NotificationPreferences): Promise { - const response = await this.api.put('/notifications/preferences', preferences); - return response.data; - } - - async getUnreadCount(): Promise { - const response = await this.api.get<{ count: number }>('/notifications/unread-count'); - return response.data.count; - } -} - -export const notificationService = new NotificationService(); diff --git a/packages/mobile-api-client/src/api/subscription.service.ts b/packages/mobile-api-client/src/api/subscription.service.ts deleted file mode 100644 index ff46c1c..0000000 --- a/packages/mobile-api-client/src/api/subscription.service.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Subscription API service - */ - -import { getApiClient } from './api-client'; -import type { - Subscription, - SubscriptionTier, - SubscriptionStatusResponse, - CreateSubscriptionRequest, - UpdateSubscriptionRequest, -} from '../types'; - -export class SubscriptionService { - private api = getApiClient(); - - async getSubscription(): Promise { - const response = await this.api.get('/billing/subscription'); - return response.data; - } - - async createSubscription(data: CreateSubscriptionRequest): Promise { - const response = await this.api.post('/billing/subscription', data); - return response.data; - } - - async updateSubscription(data: UpdateSubscriptionRequest): Promise { - const response = await this.api.patch('/billing/subscription', data); - return response.data; - } - - async cancelSubscription(): Promise { - const response = await this.api.delete('/billing/subscription'); - return response.data; - } - - async getTiers(): Promise { - const response = await this.api.get('/billing/tiers'); - return response.data; - } - - async createCheckoutSession(tier: string): Promise<{ url: string }> { - const response = await this.api.post<{ url: string }>('/billing/checkout', { tier }); - return response.data; - } - - async createCustomerPortalSession(): Promise<{ url: string }> { - const response = await this.api.post<{ url: string }>('/billing/customer-portal'); - return response.data; - } -} - -export const subscriptionService = new SubscriptionService(); diff --git a/packages/mobile-api-client/src/index.ts b/packages/mobile-api-client/src/index.ts deleted file mode 100644 index df57f8d..0000000 --- a/packages/mobile-api-client/src/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * ShieldAI Mobile API Client - * - * A comprehensive TypeScript API client library for React Native apps - * to interact with ShieldAI backend services. - * - * @example - * ```typescript - * import { createApiClient, authService, deviceService } from '@shieldai/mobile-api-client'; - * - * // Initialize the client - * createApiClient({ - * baseURL: 'https://api.shieldai.freno.me/api/v1', - * timeout: 30000, - * debug: __DEV__, - * }); - * - * // Login - * const { user, tokens } = await authService.login({ - * email: 'user@example.com', - * password: 'password123', - * }); - * - * // Register device for push notifications - * await deviceService.registerDevice({ - * platform: 'ios', - * pushToken: '...', - * }); - * ``` - */ - -// Core API client -export { - createApiClient, - getApiClient, - ApiClient, - ApiClientConfig, -} from './api/api-client'; - -// Services -export { authService, AuthService } from './api/auth.service'; -export { deviceService, DeviceService } from './api/device.service'; -export { subscriptionService, SubscriptionService } from './api/subscription.service'; -export { notificationService, NotificationService } from './api/notification.service'; - -// Types -export * from './types'; - -// Storage -export { storage, tokenStorage, StorageAdapter } from './storage/token-storage'; - -// Utils -export { requestQueue, RequestQueue } from './utils/request-queue'; diff --git a/packages/mobile-api-client/src/react-native.d.ts b/packages/mobile-api-client/src/react-native.d.ts deleted file mode 100644 index d685f04..0000000 --- a/packages/mobile-api-client/src/react-native.d.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Type declarations for React Native and Expo packages -declare module 'expo-secure-store' { - export function getItemAsync(key: string): Promise; - export function setItemAsync(key: string, value: string): Promise; - export function deleteItemAsync(key: string): Promise; -} - -declare module '@react-native-async-storage/async-storage' { - export function getItem(key: string): Promise; - export function setItem(key: string, value: string): Promise; - export function removeItem(key: string): Promise; - export function clear(): Promise; -} - -declare module 'react-native' { - export import Platform = require('react-native/Libraries/Utilities/Platform'); - export const Platform: { - OS: 'ios' | 'android' | 'web' | 'windows' | 'macos'; - Version: number; - }; -} diff --git a/packages/mobile-api-client/src/storage/token-storage.ts b/packages/mobile-api-client/src/storage/token-storage.ts deleted file mode 100644 index 1679d56..0000000 --- a/packages/mobile-api-client/src/storage/token-storage.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Secure storage for authentication tokens - * Uses expo-secure-store for production, AsyncStorage for fallback - */ - -import * as SecureStore from 'expo-secure-store'; -import AsyncStorage from '@react-native-async-storage/async-storage'; - -const ACCESS_TOKEN_KEY = '@shieldai:access_token'; -const REFRESH_TOKEN_KEY = '@shieldai:refresh_token'; - -export interface StorageAdapter { - getItem: (key: string) => Promise; - setItem: (key: string, value: string) => Promise; - removeItem: (key: string) => Promise; -} - -class SecureStorageAdapter implements StorageAdapter { - async getItem(key: string): Promise { - try { - return await SecureStore.getItemAsync(key); - } catch { - // Fallback to AsyncStorage if SecureStore fails - return await AsyncStorage.getItem(key); - } - } - - async setItem(key: string, value: string): Promise { - try { - await SecureStore.setItemAsync(key, value); - } catch { - await AsyncStorage.setItem(key, value); - } - } - - async removeItem(key: string): Promise { - try { - await SecureStore.deleteItemAsync(key); - } catch { - await AsyncStorage.removeItem(key); - } - } -} - -class InMemoryStorageAdapter implements StorageAdapter { - private store: Map = new Map(); - - async getItem(key: string): Promise { - return this.store.get(key) || null; - } - - async setItem(key: string, value: string): Promise { - this.store.set(key, value); - } - - async removeItem(key: string): Promise { - this.store.delete(key); - } -} - -// Detect environment and choose appropriate storage -const getStorageAdapter = (): StorageAdapter => { - if (process.env.NODE_ENV === 'test') { - return new InMemoryStorageAdapter(); - } - return new SecureStorageAdapter(); -}; - -export const storage = getStorageAdapter(); - -export const tokenStorage = { - async getAccessToken(): Promise { - return await storage.getItem(ACCESS_TOKEN_KEY); - }, - - async getRefreshToken(): Promise { - return await storage.getItem(REFRESH_TOKEN_KEY); - }, - - async saveTokens(accessToken: string, refreshToken: string): Promise { - await Promise.all([ - storage.setItem(ACCESS_TOKEN_KEY, accessToken), - storage.setItem(REFRESH_TOKEN_KEY, refreshToken), - ]); - }, - - async clearTokens(): Promise { - await Promise.all([ - storage.removeItem(ACCESS_TOKEN_KEY), - storage.removeItem(REFRESH_TOKEN_KEY), - ]); - }, -}; diff --git a/packages/mobile-api-client/src/types/auth.types.ts b/packages/mobile-api-client/src/types/auth.types.ts deleted file mode 100644 index 47ebe12..0000000 --- a/packages/mobile-api-client/src/types/auth.types.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * Authentication types for ShieldAI API - */ - -export interface User { - id: string; - email: string; - firstName?: string; - lastName?: string; - createdAt: string; - updatedAt: string; -} - -export interface AuthTokens { - accessToken: string; - refreshToken: string; - expiresIn: number; - tokenType: 'Bearer'; -} - -export interface LoginCredentials { - email: string; - password: string; -} - -export interface RegisterData { - email: string; - password: string; - firstName?: string; - lastName?: string; -} - -export interface AuthResponse { - user: User; - tokens: AuthTokens; -} - -export interface RefreshTokenRequest { - refreshToken: string; -} - -export interface AuthError { - code: string; - message: string; - statusCode: number; -} diff --git a/packages/mobile-api-client/src/types/common.types.ts b/packages/mobile-api-client/src/types/common.types.ts deleted file mode 100644 index aae8a02..0000000 --- a/packages/mobile-api-client/src/types/common.types.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Shared API types - */ - -export interface ApiResponse { - data: T; - success: boolean; - message?: string; -} - -export interface PaginatedResponse { - data: T[]; - total: number; - page: number; - pageSize: number; - totalPages: number; -} - -export interface ErrorResponse { - code: string; - message: string; - details?: Record; - statusCode: number; -} - -export interface HealthStatus { - status: 'healthy' | 'degraded' | 'unhealthy'; - timestamp: string; - version: string; - services?: Record; -} - -export interface VersionInfo { - version: string; - environment: string; - build: string; -} diff --git a/packages/mobile-api-client/src/types/device.types.ts b/packages/mobile-api-client/src/types/device.types.ts deleted file mode 100644 index a5ae19d..0000000 --- a/packages/mobile-api-client/src/types/device.types.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * Device types for push notification and device management - */ - -export interface Device { - id: string; - userId: string; - platform: 'ios' | 'android'; - pushToken?: string; - modelName?: string; - osVersion?: string; - appVersion?: string; - isActive: boolean; - lastActiveAt: string; - createdAt: string; - updatedAt: string; -} - -export interface DeviceRegistration { - platform: 'ios' | 'android'; - pushToken: string; - modelName?: string; - osVersion?: string; - appVersion?: string; -} - -export interface DeviceListResponse { - devices: Device[]; - total: number; -} diff --git a/packages/mobile-api-client/src/types/index.ts b/packages/mobile-api-client/src/types/index.ts deleted file mode 100644 index 0a18a19..0000000 --- a/packages/mobile-api-client/src/types/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './auth.types'; -export * from './device.types'; -export * from './subscription.types'; -export * from './notification.types'; -export * from './common.types'; diff --git a/packages/mobile-api-client/src/types/notification.types.ts b/packages/mobile-api-client/src/types/notification.types.ts deleted file mode 100644 index f274db6..0000000 --- a/packages/mobile-api-client/src/types/notification.types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Notification types - */ - -export interface Notification { - id: string; - userId: string; - type: 'darkwatch_alert' | 'spam_blocked' | 'voiceprint_analysis' | 'subscription' | 'system'; - title: string; - message: string; - data?: Record; - isRead: boolean; - createdAt: string; - readAt?: string; -} - -export interface NotificationListResponse { - notifications: Notification[]; - total: number; - unreadCount: number; -} - -export interface NotificationPreferences { - emailNotifications: boolean; - pushNotifications: boolean; - notificationTypes: Record; -} diff --git a/packages/mobile-api-client/src/types/subscription.types.ts b/packages/mobile-api-client/src/types/subscription.types.ts deleted file mode 100644 index 9ddf59b..0000000 --- a/packages/mobile-api-client/src/types/subscription.types.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Subscription and billing types - */ - -export interface Subscription { - id: string; - userId: string; - tier: 'free' | 'basic' | 'premium' | 'enterprise'; - status: 'active' | 'canceled' | 'past_due' | 'trialing'; - stripeCustomerId: string; - stripeSubscriptionId?: string; - currentPeriodStart: string; - currentPeriodEnd: string; - cancelAtPeriodEnd: boolean; - createdAt: string; - updatedAt: string; -} - -export interface SubscriptionTier { - id: string; - name: string; - description: string; - price: number; - currency: string; - interval: 'month' | 'year'; - features: string[]; -} - -export interface CreateSubscriptionRequest { - tier: 'free' | 'basic' | 'premium' | 'enterprise'; - paymentMethodId?: string; -} - -export interface UpdateSubscriptionRequest { - tier?: 'free' | 'basic' | 'premium' | 'enterprise'; - cancelAtPeriodEnd?: boolean; -} - -export interface SubscriptionStatusResponse { - subscription: Subscription; - tier: SubscriptionTier; - usage: { - currentPeriod: { - start: string; - end: string; - }; - features: Record; - }; -} diff --git a/packages/mobile-api-client/src/utils/request-queue.ts b/packages/mobile-api-client/src/utils/request-queue.ts deleted file mode 100644 index 354c7d9..0000000 --- a/packages/mobile-api-client/src/utils/request-queue.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Request queue for offline support - * Queues API requests when offline and replays when online - */ - -import AsyncStorage from '@react-native-async-storage/async-storage'; -import type { AxiosRequestConfig, AxiosResponse } from 'axios'; - -const QUEUE_KEY = '@shieldai:api_queue'; -const MAX_QUEUE_SIZE = 100; - -export interface QueuedRequest { - id: string; - config: AxiosRequestConfig; - timestamp: number; - retryCount: number; - maxRetries: number; -} - -export interface QueueStatus { - size: number; - oldestRequest: number | null; - newestRequest: number | null; -} - -export class RequestQueue { - private queue: QueuedRequest[] = []; - private isProcessing = false; - private listeners: Set<() => void> = new Set(); - - constructor() { - this.loadFromStorage(); - } - - private notifyListeners(): void { - this.listeners.forEach((listener) => listener()); - } - - subscribe(listener: () => void): () => void { - this.listeners.add(listener); - return () => this.listeners.delete(listener); - } - - async loadFromStorage(): Promise { - try { - const data = await AsyncStorage.getItem(QUEUE_KEY); - if (data) { - this.queue = JSON.parse(data); - } - } catch (error) { - console.error('Failed to load request queue:', error); - this.queue = []; - } - } - - private async saveToStorage(): Promise { - try { - await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(this.queue)); - } catch (error) { - console.error('Failed to save request queue:', error); - } - } - - async enqueue(config: AxiosRequestConfig): Promise { - const id = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; - - const queuedRequest: QueuedRequest = { - id, - config, - timestamp: Date.now(), - retryCount: 0, - maxRetries: 3, - }; - - // Limit queue size - if (this.queue.length >= MAX_QUEUE_SIZE) { - this.queue.shift(); // Remove oldest - } - - this.queue.push(queuedRequest); - await this.saveToStorage(); - this.notifyListeners(); - - return id; - } - - async dequeue(): Promise { - if (this.queue.length === 0) return null; - - const request = this.queue.shift(); - if (request) { - await this.saveToStorage(); - this.notifyListeners(); - } - return request ?? null; - } - - async remove(id: string): Promise { - const index = this.queue.findIndex((r) => r.id === id); - if (index !== -1) { - this.queue.splice(index, 1); - await this.saveToStorage(); - this.notifyListeners(); - } - } - - async retry(id: string): Promise { - const index = this.queue.findIndex((r) => r.id === id); - if (index !== -1) { - this.queue[index].retryCount += 1; - await this.saveToStorage(); - this.notifyListeners(); - } - } - - async clear(): Promise { - this.queue = []; - await AsyncStorage.removeItem(QUEUE_KEY); - this.notifyListeners(); - } - - getStatus(): QueueStatus { - if (this.queue.length === 0) { - return { size: 0, oldestRequest: null, newestRequest: null }; - } - - return { - size: this.queue.length, - oldestRequest: this.queue[0]?.timestamp ?? null, - newestRequest: this.queue[this.queue.length - 1]?.timestamp ?? null, - }; - } - - isOnline(): boolean { - // In React Native, you'd use NetInfo here - // For now, return true (assume online) - return true; - } -} - -export const requestQueue = new RequestQueue(); diff --git a/packages/mobile-api-client/tsconfig.json b/packages/mobile-api-client/tsconfig.json deleted file mode 100644 index 1afcbfc..0000000 --- a/packages/mobile-api-client/tsconfig.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "commonjs", - "lib": ["ES2020", "DOM"], - "declaration": true, - "strict": true, - "noImplicitAny": true, - "strictNullChecks": true, - "noImplicitThis": true, - "alwaysStrict": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "noImplicitReturns": true, - "noFallthroughCasesInSwitch": false, - "inlineSourceMap": true, - "inlineSources": true, - "experimentalDecorators": true, - "strictPropertyInitialization": false, - "skipLibCheck": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "moduleResolution": "node", - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["./src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/mobile/.gitignore b/packages/mobile/.gitignore deleted file mode 100644 index 2799b87..0000000 --- a/packages/mobile/.gitignore +++ /dev/null @@ -1,20 +0,0 @@ -node_modules/ -.expo/ -.expo-shared/ -dist/ -ios/ -android/ -*.jks -*.p8 -*.p12 -*.mobileprovision -*.orig.* -*.pub -.jscache -*.log -*.pid -*.tgz -*.npm -*.lock -.DS_Store -.env diff --git a/packages/mobile/App.tsx b/packages/mobile/App.tsx deleted file mode 100644 index 29c53ab..0000000 --- a/packages/mobile/App.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useEffect } from 'react'; -import { StatusBar } from 'expo-status-bar'; -import { NavigationContainer } from '@react-navigation/native'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; -import { useAuthStore } from '@/store/authStore'; -import { AuthNavigator } from '@/navigation/AuthNavigator'; -import { MainTabNavigator } from '@/navigation/MainTabNavigator'; -import { usePushNotifications } from '@/hooks'; -import '@/services/api'; - -export default function App() { - const { isAuthenticated } = useAuthStore(); - const { registerForPushNotifications } = usePushNotifications(); - - useEffect(() => { - if (isAuthenticated) { - registerForPushNotifications(); - } - }, [isAuthenticated, registerForPushNotifications]); - - return ( - - - - {isAuthenticated ? : } - - - ); -} diff --git a/packages/mobile/app.json b/packages/mobile/app.json deleted file mode 100644 index 5615752..0000000 --- a/packages/mobile/app.json +++ /dev/null @@ -1,66 +0,0 @@ -{ - "expo": { - "name": "ShieldAI", - "slug": "shieldai", - "version": "1.0.0", - "orientation": "portrait", - "icon": "./assets/icon.png", - "scheme": "shieldai", - "userInterfaceStyle": "automatic", - "splash": { - "image": "./assets/splash.png", - "resizeMode": "contain", - "backgroundColor": "#0a0e1a" - }, - "ios": { - "supportsTablet": true, - "bundleIdentifier": "com.frenocorp.shieldai", - "infoPlist": { - "NSFaceIDUsageDescription": "ShieldAI uses Face ID to securely access your account.", - "NSCameraUsageDescription": "ShieldAI needs camera access for VoicePrint enrollment.", - "NSMicrophoneUsageDescription": "ShieldAI needs microphone access for voice analysis." - }, - "config": { - "usesNonExemptEncryption": false - } - }, - "android": { - "adaptiveIcon": { - "foregroundImage": "./assets/adaptive-icon.png", - "backgroundColor": "#0a0e1a" - }, - "package": "com.frenocorp.shieldai", - "permissions": [ - "RECEIVE_BOOT_COMPLETED", - "VIBRATE", - "android.permission.CAMERA", - "android.permission.RECORD_AUDIO" - ] - }, - "web": { - "bundler": "metro", - "output": "static", - "favicon": "./assets/favicon.png" - }, - "plugins": [ - "expo-secure-store", - "expo-local-authentication", - [ - "expo-notifications", - { - "icon": "./assets/notification-icon.png", - "color": "#4f8cff", - "sounds": [] - } - ] - ], - "experiments": { - "typedRoutes": true - }, - "extra": { - "eas": { - "projectId": "shieldai-project-id" - } - } - } -} diff --git a/packages/mobile/assets/adaptive-icon.png b/packages/mobile/assets/adaptive-icon.png deleted file mode 100644 index 08cd6f2..0000000 Binary files a/packages/mobile/assets/adaptive-icon.png and /dev/null differ diff --git a/packages/mobile/assets/favicon.png b/packages/mobile/assets/favicon.png deleted file mode 100644 index 08cd6f2..0000000 Binary files a/packages/mobile/assets/favicon.png and /dev/null differ diff --git a/packages/mobile/assets/icon.png b/packages/mobile/assets/icon.png deleted file mode 100644 index 08cd6f2..0000000 Binary files a/packages/mobile/assets/icon.png and /dev/null differ diff --git a/packages/mobile/assets/notification-icon.png b/packages/mobile/assets/notification-icon.png deleted file mode 100644 index 08cd6f2..0000000 Binary files a/packages/mobile/assets/notification-icon.png and /dev/null differ diff --git a/packages/mobile/assets/splash.png b/packages/mobile/assets/splash.png deleted file mode 100644 index 08cd6f2..0000000 Binary files a/packages/mobile/assets/splash.png and /dev/null differ diff --git a/packages/mobile/assets/store/README.md b/packages/mobile/assets/store/README.md deleted file mode 100644 index f991c14..0000000 --- a/packages/mobile/assets/store/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# ShieldAI - App Store Metadata - -## App Information -- **App Name:** ShieldAI -- **Subtitle:** Your Digital Identity Shield -- **Description:** ShieldAI protects your digital identity in real-time. Monitor data breaches, block spam calls and texts, and verify voices with AI-powered protection. Get instant alerts when your personal information appears online, manage your watch list for new threats, and keep your family safe with VoicePrint verification. -- **Keyword:** privacy,security,spam,identity,breach,protection,voice,ai -- **Support URL:** https://shieldai.frenocorp.com/support -- **Privacy URL:** https://shieldai.frenocorp.com/privacy -- **Marketing URL:** https://shieldai.frenocorp.com - -## iOS App Store Requirements - -### Screenshots (Required) -- iPhone 6.7" (1290x2796): 3-5 screenshots -- iPhone 6.1" (1170x2532): 3-5 screenshots -- iPad 10.9" (2048x2732): 3-5 screenshots (if tablet supported) -- Format: PNG, no rounded corners, no device frame overlays - -### App Icon -- 1024x1024 PNG, no transparency, no rounded corners -- Place at: `assets/store/ios/app-icon-1024.png` - -### Preview Video (Optional) -- 9:16 or 16:9, 20-90 seconds -- MP4 format, under 100MB - -## Google Play Store Requirements - -### Screenshots (Required) -- Phone (1080x1920 recommended): 2-8 screenshots -- Tablet (1600x2560 recommended): 2-8 screenshots (if tablet supported) -- Format: PNG or JPEG, under 8MB each - -### App Icon -- 512x512 PNG, 32-bit with transparency, 1MB or less -- Place at: `assets/store/android/app-icon-512.png` - -### Feature Graphic -- 1024x500 PNG, 1MB or less, no transparency -- Place at: `assets/store/android/feature-graphic.png` - -### Video (Optional) -- YouTube URL or uploaded video - -## Tier-Based Feature Highlights for Screenshots - -### Free Tier Screenshots -1. Dashboard - Basic exposure summary -2. SpamShield - Call/text spam detection -3. Settings - Account and notifications - -### Plus Tier Screenshots -1. DarkWatch - Watch list and exposure feed -2. Alert notification preview -3. Real-time breach monitoring - -### Premium Tier Screenshots -1. VoicePrint - Family enrollment -2. Voice analysis results -3. Deepfake detection status - -## Screenshot Notes -- Screenshots should show the app in both light and dark mode -- Highlight key differentiators: real-time alerts, AI voice verification, family protection -- Use consistent device mockups across stores -- Localization-ready: avoid hardcoded text in screenshot UI diff --git a/packages/mobile/assets/store/android/screenshots/README.md b/packages/mobile/assets/store/android/screenshots/README.md deleted file mode 100644 index 4de10af..0000000 --- a/packages/mobile/assets/store/android/screenshots/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Android Screenshots - -Place screenshots in this directory before submission. - -## Required Sizes -- `phone-1080x1920.png` - 1080x1920 (standard phone) -- `tablet-1600x2560.png` - 1600x2560 (if tablet supported) - -## Screens to Capture -1. Dashboard - exposure summary cards -2. DarkWatch - watch list with entities -3. SpamShield - call/text history -4. VoicePrint - family member enrollment -5. Settings - tier and notifications - -## Notes -- Use Android emulator or connected device for captures -- Minimum 2 screenshots required, up to 8 allowed -- Landscape screenshots optional for productivity apps diff --git a/packages/mobile/assets/store/ios/screenshots/README.md b/packages/mobile/assets/store/ios/screenshots/README.md deleted file mode 100644 index 3b38608..0000000 --- a/packages/mobile/assets/store/ios/screenshots/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# iOS Screenshots - -Place screenshots in this directory before submission. - -## Required Sizes -- `iphone-6.7in.png` - 1290x2796 (iPhone 14 Pro Max, 15 Pro Max) -- `iphone-6.1in.png` - 1170x2532 (iPhone 14, 15) - -## Screens to Capture -1. Dashboard - exposure summary cards -2. DarkWatch - watch list with entities -3. SpamShield - call/text history -4. VoicePrint - family member enrollment -5. Settings - tier and notifications - -## Notes -- Use Xcode simulator or `xcrun simctl io booted screenshot` for captures -- No device frame overlays in screenshots -- Ensure dark mode and light mode are represented diff --git a/packages/mobile/assets/store/marketing/README.md b/packages/mobile/assets/store/marketing/README.md deleted file mode 100644 index 1ca61d7..0000000 --- a/packages/mobile/assets/store/marketing/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Marketing Assets - -Place marketing materials here. - -## Required -- `app-store-preview.png` - 1200x628 OG image for social sharing -- `feature-graphic.png` - 1024x500 for Google Play feature graphic - -## Optional -- `promo-video.mp4` - 30s promo for app store video preview -- `press-kit.zip` - Press kit with logos, screenshots, and brand guidelines diff --git a/packages/mobile/babel.config.js b/packages/mobile/babel.config.js deleted file mode 100644 index fbaf1f9..0000000 --- a/packages/mobile/babel.config.js +++ /dev/null @@ -1,9 +0,0 @@ -module.exports = function (api) { - api.cache(true); - return { - presets: ['babel-preset-expo'], - plugins: [ - 'react-native-reanimated/plugin', - ], - }; -}; diff --git a/packages/mobile/eas.json b/packages/mobile/eas.json deleted file mode 100644 index 42896be..0000000 --- a/packages/mobile/eas.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "cli": { - "version": ">= 10.0.0", - "appVersionSource": "remote" - }, - "build": { - "development": { - "developmentClient": true, - "distribution": "internal", - "ios": { - "resourceClass": "m1-medium" - }, - "android": { - "buildType": "apk" - }, - "env": { - "NODE_ENV": "development" - } - }, - "preview": { - "distribution": "internal", - "ios": { - "simulator": true, - "resourceClass": "m1-medium" - }, - "android": { - "buildType": "apk" - }, - "env": { - "NODE_ENV": "development" - } - }, - "production": { - "ios": { - "resourceClass": "m1-medium" - }, - "android": { - "buildType": "app-bundle" - }, - "env": { - "NODE_ENV": "production" - } - } - }, - "submit": { - "production": { - "ios": { - "ascAppId": "shieldai-app-id", - "appleId": "apple@frenocorp.com", - "appleTeamId": "FRENOCORP-TEAM-ID" - }, - "android": { - "serviceAccountKeyPath": "./google-play-service-account.json", - "track": "internal" - } - } - } -} diff --git a/packages/mobile/metro.config.js b/packages/mobile/metro.config.js deleted file mode 100644 index 3974b8f..0000000 --- a/packages/mobile/metro.config.js +++ /dev/null @@ -1,8 +0,0 @@ -const { getDefaultConfig } = require('expo/metro-config'); - -const config = getDefaultConfig(__dirname); - -config.resolver.sourceExts = ['js', 'jsx', 'ts', 'tsx', 'json']; -config.watchFolders = [__dirname]; - -module.exports = config; diff --git a/packages/mobile/package.json b/packages/mobile/package.json deleted file mode 100644 index 03bf68d..0000000 --- a/packages/mobile/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@shieldai/mobile", - "version": "1.0.0", - "private": true, - "main": "node_modules/expo/AppEntry.js", - "scripts": { - "dev": "expo start", - "dev:ios": "expo run:ios", - "dev:android": "expo run:android", - "build": "expo build", - "build:ios": "expo build:ios", - "build:android": "expo build:android", - "lint": "eslint src/", - "typecheck": "tsc --noEmit", - "test": "jest", - "clean": "rm -rf node_modules .expo .expo-shared" - }, - "dependencies": { - "@shieldai/mobile-api-client": "workspace:*", - "@expo/vector-icons": "^14.0.0", - "@react-native-async-storage/async-storage": "1.23.1", - "@react-native-community/netinfo": "11.4.1", - "@react-navigation/bottom-tabs": "^6.5.0", - "@react-navigation/native": "^6.1.0", - "@react-navigation/stack": "^6.3.0", - "expo": "~51.0.0", - "expo-av": "~14.0.0", - - "expo-constants": "~16.0.0", - "expo-crypto": "~13.0.0", - "expo-device": "~6.0.0", - "expo-file-system": "~17.0.0", - "expo-image-picker": "~15.0.0", - "expo-linking": "~6.3.0", - "expo-local-authentication": "~14.0.0", - "expo-notifications": "~0.28.0", - "expo-secure-store": "~13.0.0", - "expo-status-bar": "~1.12.0", - "expo-updates": "~0.18.0", - "react": "18.2.0", - "react-native": "0.74.5", - "react-native-gesture-handler": "~2.16.0", - "react-native-reanimated": "~3.10.0", - "react-native-safe-area-context": "4.10.5", - "react-native-screens": "3.31.0", - "react-native-svg": "15.2.0", - "zustand": "^4.4.0" - }, - "devDependencies": { - "@babel/core": "^7.24.0", - "@types/react": "^18.2.0", - "@types/react-test-renderer": "^18.0.0", - "babel-preset-expo": "^11.0.0", - "eslint": "^8.57.0", - "eslint-plugin-react": "^7.34.0", - "eslint-plugin-react-native": "^4.1.0", - "jest": "^29.7.0", - "react-test-renderer": "18.2.0", - "typescript": "^5.3.0" - } -} diff --git a/packages/mobile/src/components/Button.tsx b/packages/mobile/src/components/Button.tsx deleted file mode 100644 index ce0915f..0000000 --- a/packages/mobile/src/components/Button.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from 'react'; -import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import { COLORS, BORDER_RADIUS, FONT_SIZES } from '@/constants/theme'; - -interface ButtonProps { - title: string; - onPress: () => void; - variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; - disabled?: boolean; - loading?: boolean; - fullWidth?: boolean; -} - -export function Button({ - title, - onPress, - variant = 'primary', - disabled = false, - loading = false, - fullWidth = false, -}: ButtonProps) { - const variantColors = { - primary: { bg: COLORS.primary, text: '#fff' }, - secondary: { bg: COLORS.secondary, text: '#fff' }, - danger: { bg: COLORS.danger, text: '#fff' }, - ghost: { bg: 'transparent', text: COLORS.primary }, - }; - - const colors = variantColors[variant]; - - return ( - - - {loading ? '...' : title} - - - ); -} - -const styles = StyleSheet.create({ - button: { - borderRadius: BORDER_RADIUS.md, - paddingVertical: 12, - paddingHorizontal: 24, - alignItems: 'center', - justifyContent: 'center', - marginVertical: 4, - }, - fullWidth: { - width: '100%', - }, - text: { - fontSize: FONT_SIZES.md, - fontWeight: '600', - }, -}); diff --git a/packages/mobile/src/components/Card.tsx b/packages/mobile/src/components/Card.tsx deleted file mode 100644 index 152a0cb..0000000 --- a/packages/mobile/src/components/Card.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { StyleSheet, Text, View, ViewStyle } from 'react-native'; -import { COLORS, BORDER_RADIUS, FONT_SIZES, SPACING } from '@/constants/theme'; - -interface SectionHeaderProps { - title: string; - action?: string; - onActionPress?: () => void; -} - -export function SectionHeader({ title, action, onActionPress }: SectionHeaderProps) { - return ( - - {title} - {action && onActionPress && ( - - {action} - - )} - - ); -} - -interface CardProps { - title?: string; - children: React.ReactNode; - style?: ViewStyle; -} - -export function Card({ title, children, style }: CardProps) { - return ( - - {title && {title}} - {children} - - ); -} - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingHorizontal: SPACING.md, - paddingVertical: SPACING.sm, - }, - title: { - color: COLORS.text, - fontSize: FONT_SIZES.lg, - fontWeight: '600', - }, - action: { - color: COLORS.primary, - fontSize: FONT_SIZES.sm, - }, - card: { - backgroundColor: COLORS.card, - borderRadius: BORDER_RADIUS.lg, - padding: SPACING.md, - marginHorizontal: SPACING.md, - marginVertical: SPACING.xs, - }, - cardTitle: { - color: COLORS.text, - fontSize: FONT_SIZES.md, - fontWeight: '600', - marginBottom: SPACING.sm, - }, -}); diff --git a/packages/mobile/src/components/Input.tsx b/packages/mobile/src/components/Input.tsx deleted file mode 100644 index 09d18e9..0000000 --- a/packages/mobile/src/components/Input.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from 'react'; -import { StyleSheet, TextInput, TextInputProps, Text, View } from 'react-native'; -import { COLORS, BORDER_RADIUS, FONT_SIZES } from '@/constants/theme'; - -interface InputProps extends Omit { - label?: string; - error?: string; - containerStyle?: object; -} - -export function Input({ label, error, containerStyle, ...props }: InputProps) { - return ( - - {label && {label}} - - {error && {error}} - - ); -} - -const styles = StyleSheet.create({ - container: { - marginBottom: 16, - }, - label: { - color: COLORS.textSecondary, - fontSize: FONT_SIZES.sm, - marginBottom: 6, - }, - input: { - backgroundColor: COLORS.backgroundLight, - borderColor: COLORS.border, - borderWidth: 1, - borderRadius: BORDER_RADIUS.md, - padding: 12, - color: COLORS.text, - fontSize: FONT_SIZES.md, - }, - error: { - color: COLORS.danger, - fontSize: FONT_SIZES.xs, - marginTop: 4, - }, -}); diff --git a/packages/mobile/src/components/Loading.tsx b/packages/mobile/src/components/Loading.tsx deleted file mode 100644 index c59e981..0000000 --- a/packages/mobile/src/components/Loading.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; -import { StyleSheet, View, ActivityIndicator, Text } from 'react-native'; -import { COLORS, SPACING } from '@/constants/theme'; - -interface LoadingOverlayProps { - visible: boolean; -} - -export function LoadingOverlay({ visible }: LoadingOverlayProps) { - if (!visible) return null; - - return ( - - - - ); -} - -interface EmptyStateProps { - title: string; - message?: string; -} - -export function EmptyState({ title, message }: EmptyStateProps) { - return ( - - - - 📭 - - - {title} - {message && {message}} - - - - ); -} - -const styles = StyleSheet.create({ - overlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0, 0, 0, 0.3)', - justifyContent: 'center', - alignItems: 'center', - zIndex: 999, - elevation: 999, - }, - emptyContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: SPACING.xl, - }, - emptyContent: { - alignItems: 'center', - }, - emptyIcon: { - width: 48, - height: 48, - borderRadius: 24, - backgroundColor: COLORS.card, - marginBottom: SPACING.md, - justifyContent: 'center', - alignItems: 'center', - }, - emptyIconText: { - fontSize: 24, - }, - emptyText: { - alignItems: 'center', - }, - emptyTitle: { - color: COLORS.textSecondary, - fontSize: 16, - fontWeight: '600', - }, - emptyMessage: { - color: COLORS.textMuted, - fontSize: 14, - }, -}); diff --git a/packages/mobile/src/components/StatCard.tsx b/packages/mobile/src/components/StatCard.tsx deleted file mode 100644 index 334a73f..0000000 --- a/packages/mobile/src/components/StatCard.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme'; - -interface StatCardProps { - title: string; - value: string | number; - icon?: string; - color?: string; - subtitle?: string; -} - -export function StatCard({ title, value, color = COLORS.primary, subtitle }: StatCardProps) { - return ( - - {title} - {value} - {subtitle && {subtitle}} - - ); -} - -const styles = StyleSheet.create({ - card: { - backgroundColor: COLORS.card, - borderRadius: 8, - padding: SPACING.md, - marginBottom: SPACING.sm, - borderLeftWidth: 3, - }, - title: { - color: COLORS.textSecondary, - fontSize: FONT_SIZES.sm, - marginBottom: SPACING.xs, - }, - value: { - fontSize: FONT_SIZES.xxl, - fontWeight: 'bold', - }, - subtitle: { - color: COLORS.textMuted, - fontSize: FONT_SIZES.xs, - marginTop: SPACING.xs, - }, -}); diff --git a/packages/mobile/src/components/index.ts b/packages/mobile/src/components/index.ts deleted file mode 100644 index b22ae78..0000000 --- a/packages/mobile/src/components/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './Button'; -export * from './Input'; -export * from './StatCard'; -export * from './Card'; -export * from './Loading'; diff --git a/packages/mobile/src/constants/index.ts b/packages/mobile/src/constants/index.ts deleted file mode 100644 index c81f1fd..0000000 --- a/packages/mobile/src/constants/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const TAB_ORDER = ['Dashboard', 'DarkWatch', 'SpamShield', 'VoicePrint', 'Settings'] as const; - -export const AUTH_STORAGE_KEY = '@shieldai_auth'; -export const BIOMETRIC_ENABLED_KEY = '@shieldai_biometric'; -export const ONBOARDING_COMPLETE_KEY = '@shieldai_onboarding'; - -export const NOTIFICATION_TYPES = { - DARKWATCH_ALERT: 'darkwatch_alert', - SPAM_BLOCKED: 'spam_blocked', - VOICEPRINT_ANALYSIS: 'voiceprint_analysis', -} as const; diff --git a/packages/mobile/src/constants/theme.ts b/packages/mobile/src/constants/theme.ts deleted file mode 100644 index 9748830..0000000 --- a/packages/mobile/src/constants/theme.ts +++ /dev/null @@ -1,95 +0,0 @@ -export const COLORS = { - primary: '#4f8cff', - primaryDark: '#3a6fd8', - secondary: '#6c5ce7', - accent: '#00cec9', - success: '#00b894', - warning: '#fdcb6e', - danger: '#ff6b6b', - background: '#0a0e1a', - backgroundLight: '#111827', - card: '#1a2035', - cardLight: '#243049', - text: '#e8eaf0', - textSecondary: '#8b95a8', - textMuted: '#5a6577', - border: '#2a3550', - overlay: 'rgba(0, 0, 0, 0.6)', -} as const; - -export const SPACING = { - xs: 4, - sm: 8, - md: 16, - lg: 24, - xl: 32, - xxl: 48, -} as const; - -export const FONT_SIZES = { - xs: 12, - sm: 14, - md: 16, - lg: 18, - xl: 20, - xxl: 24, - xxxl: 32, -} as const; - -export const BORDER_RADIUS = { - sm: 4, - md: 8, - lg: 12, - xl: 16, - round: 999, -} as const; - -export const SHADOWS = { - sm: { shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2 }, - md: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, elevation: 4 }, - lg: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8, elevation: 8 }, -} as const; - -export const API_URL = process.env.EXPO_PUBLIC_API_URL || 'https://api.shieldai.freno.me/api/v1'; - -export const EAS_PROJECT_ID = process.env.EXPO_PUBLIC_EAS_PROJECT_ID || ''; - -export const getSeverityColor = (severity: string): string => { - switch (severity) { - case 'critical': return COLORS.danger; - case 'high': return COLORS.warning; - case 'medium': return COLORS.accent; - default: return COLORS.textSecondary; - } -}; - -export const TIER_FEATURES = { - free: { - name: 'Free', - maxExposures: 5, - spamProtection: false, - voicePrint: false, - darkWatch: false, - }, - basic: { - name: 'Basic', - maxExposures: 50, - spamProtection: true, - voicePrint: false, - darkWatch: true, - }, - premium: { - name: 'Premium', - maxExposures: 200, - spamProtection: true, - voicePrint: true, - darkWatch: true, - }, - enterprise: { - name: 'Enterprise', - maxExposures: -1, - spamProtection: true, - voicePrint: true, - darkWatch: true, - }, -} as const; diff --git a/packages/mobile/src/global.d.ts b/packages/mobile/src/global.d.ts deleted file mode 100644 index b867229..0000000 --- a/packages/mobile/src/global.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare const __DEV__: boolean; diff --git a/packages/mobile/src/hooks/index.ts b/packages/mobile/src/hooks/index.ts deleted file mode 100644 index b3a55b8..0000000 --- a/packages/mobile/src/hooks/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './usePushNotifications'; -export * from './useBiometricAuth'; -export * from './useNetworkStatus'; diff --git a/packages/mobile/src/hooks/useBiometricAuth.ts b/packages/mobile/src/hooks/useBiometricAuth.ts deleted file mode 100644 index b1ba01e..0000000 --- a/packages/mobile/src/hooks/useBiometricAuth.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { useCallback, useState, useEffect } from 'react'; -import * as LocalAuthentication from 'expo-local-authentication'; -import { Alert } from 'react-native'; -import { useSettingsStore } from '@/store/settingsStore'; - -export function useBiometricAuth() { - const { isBiometricEnabled } = useSettingsStore(); - const [biometricsAvailable, setBiometricsAvailable] = useState(false); - - useEffect(() => { - (async () => { - const hasHardware = await LocalAuthentication.hasHardwareAsync(); - const isEnrolled = await LocalAuthentication.isEnrolledAsync(); - setBiometricsAvailable(hasHardware && isEnrolled); - })(); - }, []); - - const authenticate = useCallback(async (): Promise => { - if (!isBiometricEnabled) return true; - - try { - const isAvailable = await LocalAuthentication.hasHardwareAsync(); - const isEnrolled = await LocalAuthentication.isEnrolledAsync(); - - if (!isAvailable || !isEnrolled) { - Alert.alert( - 'Biometric Unavailable', - 'Biometric authentication requires hardware and enrollment. Please use your password.', - [{ text: 'OK' }] - ); - useSettingsStore.getState().toggleBiometric(false); - return false; - } - - const result = await LocalAuthentication.authenticateAsync({ - promptMessage: 'Authenticate with biometrics', - fallbackLabel: 'Use passcode', - cancelLabel: 'Cancel', - disableDeviceFallback: false, - }); - - return result.success; - } catch { - return false; - } - }, [isBiometricEnabled]); - - const enableBiometric = useCallback(async (): Promise => { - const isAvailable = await LocalAuthentication.hasHardwareAsync(); - const isEnrolled = await LocalAuthentication.isEnrolledAsync(); - - if (!isAvailable || !isEnrolled) { - Alert.alert( - 'Biometric Not Available', - 'Your device does not support biometric authentication or no credentials are enrolled.', - [{ text: 'OK' }] - ); - return false; - } - - const result = await LocalAuthentication.authenticateAsync({ - promptMessage: 'Enable biometric authentication', - fallbackLabel: 'Use passcode', - }); - - if (result.success) { - useSettingsStore.getState().toggleBiometric(true); - } - - return result.success; - }, []); - - const disableBiometric = useCallback(() => { - useSettingsStore.getState().toggleBiometric(false); - }, []); - - return { authenticate, enableBiometric, disableBiometric, isBiometricEnabled, biometricsAvailable }; -} diff --git a/packages/mobile/src/hooks/useNetworkStatus.ts b/packages/mobile/src/hooks/useNetworkStatus.ts deleted file mode 100644 index 8d94b79..0000000 --- a/packages/mobile/src/hooks/useNetworkStatus.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useEffect, useState } from 'react'; -import NetInfo from '@react-native-community/netinfo'; - -export function useNetworkStatus() { - const [isConnected, setIsConnected] = useState(true); - const [isOnline, setIsOnline] = useState(true); - - useEffect(() => { - const unsubscribe = NetInfo.addEventListener((state) => { - setIsConnected(!!state.isConnected); - setIsOnline(!!state.isInternetReachable); - }); - - return unsubscribe; - }, []); - - return { isConnected, isOnline, isOffline: !isOnline }; -} diff --git a/packages/mobile/src/hooks/usePushNotifications.ts b/packages/mobile/src/hooks/usePushNotifications.ts deleted file mode 100644 index 96a4692..0000000 --- a/packages/mobile/src/hooks/usePushNotifications.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { useEffect, useCallback, useRef } from 'react'; -import * as Notifications from 'expo-notifications'; -import { Platform } from 'react-native'; -import * as Device from 'expo-device'; -import { deviceService, notificationService } from '@shieldai/mobile-api-client'; -import { useSettingsStore } from '@/store/settingsStore'; -import { EAS_PROJECT_ID } from '@/constants/theme'; - -export function usePushNotifications() { - const preferencesRef = useRef(useSettingsStore.getState().preferences); - - useEffect(() => { - const subscription = useSettingsStore.subscribe((state) => { - preferencesRef.current = state.preferences; - }); - return subscription; - }, []); - - Notifications.setNotificationHandler({ - handleNotification: async () => { - const prefs = preferencesRef.current; - return { - shouldShowAlert: prefs.pushNotifications, - shouldPlaySound: prefs.pushNotifications, - shouldSetBadge: false, - }; - }, - }); - - const registerForPushNotifications = useCallback(async () => { - try { - const { status: existingStatus } = await Notifications.getPermissionsAsync(); - let finalStatus = existingStatus; - - if (existingStatus !== 'granted') { - const { status } = await Notifications.requestPermissionsAsync(); - finalStatus = status; - } - - if (finalStatus !== 'granted') { - return null; - } - - if (!EAS_PROJECT_ID) { - console.warn('EAS_PROJECT_ID not configured — push notifications disabled'); - return null; - } - - const token = (await Notifications.getExpoPushTokenAsync({ - projectId: EAS_PROJECT_ID, - })).data; - - try { - await deviceService.registerDevice({ - platform: Platform.OS === 'ios' ? 'ios' : 'android', - pushToken: token, - modelName: Platform.OS === 'ios' ? 'iPhone' : 'Android', - osVersion: Device.osVersion || '0', - appVersion: '1.0.0', - }); - } catch (deviceError) { - console.warn('Device registration failed (will retry on next launch):', deviceError); - } - - return token; - } catch (error) { - console.error('Failed to register for push notifications:', error); - return null; - } - }, []); - - useEffect(() => { - const subscription = Notifications.addNotificationReceivedListener((notification) => { - const type = notification.request.content.data?.type; - const prefs = preferencesRef.current; - - if (type === 'darkwatch_alert' && !prefs.darkwatchAlert) return; - if (type === 'spam_blocked' && !prefs.spamBlocked) return; - if (type === 'voiceprint_analysis' && !prefs.voiceprintAnalysis) return; - }); - - return () => subscription.remove(); - }, []); - - return { registerForPushNotifications }; -} diff --git a/packages/mobile/src/navigation/AuthNavigator.tsx b/packages/mobile/src/navigation/AuthNavigator.tsx deleted file mode 100644 index ecac723..0000000 --- a/packages/mobile/src/navigation/AuthNavigator.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import { createStackNavigator } from '@react-navigation/stack'; -import { LoginScreen, RegisterScreen } from '@/screens/auth'; - -type AuthStackParamList = { - Login: undefined; - Register: undefined; -}; - -const Stack = createStackNavigator(); - -export function AuthNavigator() { - return ( - - - - - ); -} diff --git a/packages/mobile/src/navigation/MainTabNavigator.tsx b/packages/mobile/src/navigation/MainTabNavigator.tsx deleted file mode 100644 index 73bc3c8..0000000 --- a/packages/mobile/src/navigation/MainTabNavigator.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React from 'react'; -import { Text, ViewStyle, StyleSheet } from 'react-native'; -import { Ionicons } from '@expo/vector-icons'; -import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import { DashboardScreen } from '@/screens/dashboard'; -import { DarkWatchScreen } from '@/screens/darkwatch'; -import { SpamShieldScreen } from '@/screens/spamshield'; -import { VoicePrintScreen } from '@/screens/voiceprint'; -import { SettingsScreen } from '@/screens/settings'; -import { COLORS, FONT_SIZES } from '@/constants/theme'; - -type MainTabParamList = { - Dashboard: undefined; - DarkWatch: undefined; - SpamShield: undefined; - VoicePrint: undefined; - Settings: undefined; -}; - -const Tab = createBottomTabNavigator(); - -const iconMap: Record = { - Dashboard: 'shield-outline', - DarkWatch: 'eye-outline', - SpamShield: 'ban-outline', - VoicePrint: 'mic-outline', - Settings: 'settings-outline', -}; - -const iconActiveMap: Record = { - Dashboard: 'shield', - DarkWatch: 'eye', - SpamShield: 'ban', - VoicePrint: 'mic', - Settings: 'settings', -}; - -function TabIcon({ routeName, color, focused }: { routeName: string; color: string; focused: boolean }) { - const iconName = focused - ? (iconActiveMap[routeName] as keyof typeof Ionicons.glyphMap) - : (iconMap[routeName] as keyof typeof Ionicons.glyphMap); - - return ; -} - -export function MainTabNavigator() { - return ( - - , - }} - /> - , - }} - /> - , - }} - /> - , - }} - /> - , - }} - /> - - ); -} diff --git a/packages/mobile/src/navigation/index.ts b/packages/mobile/src/navigation/index.ts deleted file mode 100644 index 03db8c5..0000000 --- a/packages/mobile/src/navigation/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './AuthNavigator'; -export * from './MainTabNavigator'; diff --git a/packages/mobile/src/screens/auth/LoginScreen.tsx b/packages/mobile/src/screens/auth/LoginScreen.tsx deleted file mode 100644 index bfdde75..0000000 --- a/packages/mobile/src/screens/auth/LoginScreen.tsx +++ /dev/null @@ -1,179 +0,0 @@ -import React, { useState } from 'react'; -import { StyleSheet, Text, View, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert } from 'react-native'; -import AsyncStorage from '@react-native-async-storage/async-storage'; -import { useNavigation, NavigationProp } from '@react-navigation/native'; -import { useAuthStore } from '@/store/authStore'; -import { Button, Input } from '@/components'; -import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme'; - -type RootStackParamList = { - Login: undefined; - Register: undefined; -}; - -const LOGIN_ATTEMPT_KEY = '@shieldai_login_attempts'; -const MAX_ATTEMPTS = 5; -const LOCKOUT_MS = 5 * 60 * 1000; - -export function LoginScreen() { - const navigation = useNavigation>(); - const { login, isLoading, clearError } = useAuthStore(); - - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [formError, setFormError] = useState(''); - - const handleLogin = async () => { - setFormError(''); - clearError(); - - if (!email || !password) { - setFormError('Please fill in all fields'); - return; - } - - const now = Date.now(); - const stored = await AsyncStorage.getItem(LOGIN_ATTEMPT_KEY); - let attempts = { count: 0, lockedUntil: 0 }; - if (stored) { - try { - attempts = JSON.parse(stored); - } catch { /* ignore corrupt data */ } - } - - if (attempts.lockedUntil > now) { - const remaining = Math.ceil((attempts.lockedUntil - now) / 1000 / 60); - setFormError(`Too many failed attempts. Try again in ${remaining} minute${remaining !== 1 ? 's' : ''}.`); - return; - } - - if (attempts.count >= MAX_ATTEMPTS && attempts.lockedUntil <= now) { - attempts.lockedUntil = now + LOCKOUT_MS; - attempts.count = 0; - await AsyncStorage.setItem(LOGIN_ATTEMPT_KEY, JSON.stringify(attempts)); - const remaining = Math.ceil(LOCKOUT_MS / 1000 / 60); - setFormError(`Too many failed attempts. Locked for ${remaining} minutes.`); - return; - } - - try { - await login(email, password); - await AsyncStorage.removeItem(LOGIN_ATTEMPT_KEY); - } catch (err: any) { - attempts.count = (attempts.count || 0) + 1; - if (attempts.count >= MAX_ATTEMPTS) { - attempts.lockedUntil = Date.now() + LOCKOUT_MS; - } - await AsyncStorage.setItem(LOGIN_ATTEMPT_KEY, JSON.stringify(attempts)); - const remaining = MAX_ATTEMPTS - attempts.count; - const warning = remaining > 0 ? ` ${remaining} attempt${remaining !== 1 ? 's' : ''} remaining.` : ''; - setFormError(err.message || 'Login failed. Please try again.' + warning); - Alert.alert('Login Failed', err.message || 'Please check your credentials and try again.' + warning); - } - }; - - return ( - - - - - ShieldAI - Your digital protection suite - - - - {formError && {formError}} - - - - - - - -
          - setName(e.currentTarget.value)} - placeholder="Your name (optional)" - aria-label="Your name" - /> - -
          - {error() &&

          {error()}

          } - - ); - } - - return ( -
          -
          - setEmail(e.currentTarget.value)} - placeholder={props.placeholder || 'Your email'} - required - aria-label="Email address" - /> - -
          - {error() &&

          {error()}

          } -
          - ); -}; - -export default WaitlistForm; diff --git a/packages/web/src/hooks/useAnalytics.ts b/packages/web/src/hooks/useAnalytics.ts deleted file mode 100644 index 7984e2c..0000000 --- a/packages/web/src/hooks/useAnalytics.ts +++ /dev/null @@ -1,130 +0,0 @@ -type EventParams = Record; - -const GA_MEASUREMENT_ID = import.meta.env.VITE_GA_MEASUREMENT_ID as string | undefined; -const MIXPANEL_TOKEN = import.meta.env.VITE_MIXPANEL_TOKEN as string | undefined; -const META_PIXEL_ID = import.meta.env.VITE_META_PIXEL_ID as string | undefined; -const LINKEDIN_PARTNER_ID = import.meta.env.VITE_LINKEDIN_PARTNER_ID as string | undefined; - -declare global { - interface Window { - gtag?: (command: string, target: string, params?: EventParams) => void; - mixpanel?: { track: (event: string, params?: EventParams) => void; init?: (token: string) => void }; - dataLayer?: unknown[]; - fbq?: (...args: unknown[]) => void; - _fbq?: unknown; - lintrk?: (...args: unknown[]) => void; - } -} - -function initGA() { - if (!GA_MEASUREMENT_ID || typeof window === 'undefined') return; - if (window.gtag) return; - - const script = document.createElement('script'); - script.async = true; - script.src = `https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`; - document.head.appendChild(script); - - window.dataLayer = window.dataLayer || []; - window.gtag = function gtag() { - window.dataLayer!.push(arguments); - }; - window.gtag('js', new Date()); - window.gtag('config', GA_MEASUREMENT_ID); -} - -function initMixpanel() { - if (!MIXPANEL_TOKEN || typeof window === 'undefined') return; - if (window.mixpanel) return; - - const script = document.createElement('script'); - script.async = true; - script.src = 'https://cdn.mxpnl.com/libs/mixpanel-2-latest.min.js'; - document.head.appendChild(script); - - script.onload = () => { - window.mixpanel = window.mixpanel || { track: () => {} }; - window.mixpanel.init?.(MIXPANEL_TOKEN); - }; -} - -function initMetaPixel() { - if (!META_PIXEL_ID || typeof window === 'undefined') return; - if (window.fbq) return; - - window.fbq = function fbq() { window._fbq = window._fbq || []; window._fbq.push(arguments); }; - const script = document.createElement('script'); - script.async = true; - script.src = `https://connect.facebook.net/en_US/fbevents.js`; - document.head.appendChild(script); - - window.fbq('init', META_PIXEL_ID); - window.fbq('track', 'PageView'); -} - -function initLinkedInInsight() { - if (!LINKEDIN_PARTNER_ID || typeof window === 'undefined') return; - if (window.lintrk) return; - - window.lintrk = function lintrk() { window.lintrk.q.push(arguments); }; - window.lintrk.q = []; - const script = document.createElement('script'); - script.async = true; - script.src = `https://snap.licdn.com/li.lms-analytics/insight.min.js`; - document.head.appendChild(script); -} - -export function initAnalytics() { - initGA(); - initMixpanel(); - initMetaPixel(); - initLinkedInInsight(); -} - -export function trackMetaEvent(eventName: string, params?: EventParams) { - if (typeof window === 'undefined' || !window.fbq) return; - window.fbq('track', eventName, params); -} - -export function trackLinkedInEvent() { - if (typeof window === 'undefined' || !window.lintrk) return; - window.lintrk('track', { conversion_id: null }); -} - -export function trackEvent(name: string, params?: EventParams) { - if (typeof window === 'undefined') return; - - if (window.gtag) { - window.gtag('event', name, params); - } - - if (window.mixpanel) { - window.mixpanel.track(name, params); - } -} - -function hashEmail(email: string): string { - let hash = 0; - for (let i = 0; i < email.length; i++) { - const char = email.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; - } - return Math.abs(hash).toString(16); -} - -export function trackWaitlistSignup(email: string, source?: string, tier?: string) { - trackEvent('waitlist_signup', { - email, - source: source || 'landing_page', - tier: tier || 'unknown', - }); - trackMetaEvent('Lead', { value: 5.00, currency: 'USD', eventID: hashEmail(email) }); -} - -export function trackPageView(path: string, title?: string) { - trackEvent('page_view', { - page_path: path, - page_title: title || document.title, - }); -} diff --git a/packages/web/src/index.css b/packages/web/src/index.css deleted file mode 100644 index 53a91af..0000000 --- a/packages/web/src/index.css +++ /dev/null @@ -1,1428 +0,0 @@ -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -:root { - --bg-primary: #0a0f1e; - --bg-secondary: #111827; - --bg-card: #1a2332; - --bg-card-hover: #1f2b3d; - --text-primary: #f1f5f9; - --text-secondary: #94a3b8; - --text-muted: #64748b; - --accent-primary: #3b82f6; - --accent-secondary: #06b6d4; - --accent-gradient: linear-gradient(135deg, #3b82f6, #06b6d4); - --border-color: #1e293b; - --border-light: #334155; - --success: #22c55e; - --error: #ef4444; - --radius: 12px; - --radius-sm: 8px; - --max-width: 1200px; - --font-sans: 'Inter', system-ui, -apple-system, sans-serif; -} - -html { - scroll-behavior: smooth; - font-size: 16px; -} - -body { - font-family: var(--font-sans); - background: var(--bg-primary); - color: var(--text-primary); - line-height: 1.6; - -webkit-font-smoothing: antialiased; -} - -a { - color: var(--accent-primary); - text-decoration: none; -} - -a:hover { - color: var(--accent-secondary); -} - -img { - max-width: 100%; - height: auto; -} - -.container { - max-width: var(--max-width); - margin: 0 auto; - padding: 0 24px; -} - -.section-title { - font-size: 2.5rem; - font-weight: 800; - text-align: center; - margin-bottom: 16px; - line-height: 1.2; -} - -.section-subtitle { - font-size: 1.125rem; - color: var(--text-secondary); - text-align: center; - max-width: 640px; - margin: 0 auto 64px; - line-height: 1.7; -} - -.gradient-text { - background: var(--accent-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -/* Hero Section */ -.hero { - position: relative; - min-height: 100vh; - display: flex; - align-items: center; - overflow: hidden; - padding: 120px 0 80px; -} - -.hero-bg { - position: absolute; - inset: 0; - background: - radial-gradient(ellipse 80% 60% at 50% -20%, rgba(59, 130, 246, 0.15), transparent), - radial-gradient(ellipse 50% 40% at 80% 80%, rgba(6, 182, 212, 0.1), transparent), - radial-gradient(ellipse 40% 30% at 20% 70%, rgba(99, 102, 241, 0.08), transparent); - pointer-events: none; -} - -.hero-badge { - display: inline-block; - padding: 6px 16px; - border-radius: 999px; - background: rgba(59, 130, 246, 0.1); - border: 1px solid rgba(59, 130, 246, 0.2); - color: var(--accent-primary); - font-size: 0.875rem; - font-weight: 500; - margin-bottom: 24px; -} - -.hero-title { - font-size: 4rem; - font-weight: 800; - line-height: 1.1; - margin-bottom: 24px; - letter-spacing: -0.02em; -} - -.hero-subtitle { - font-size: 1.25rem; - color: var(--text-secondary); - max-width: 600px; - margin-bottom: 48px; - line-height: 1.7; -} - -.hero-waitlist h3 { - font-size: 1rem; - font-weight: 500; - color: var(--text-muted); - margin-bottom: 16px; -} - -.hero-stats { - display: flex; - gap: 48px; - margin-top: 80px; - padding-top: 48px; - border-top: 1px solid var(--border-color); -} - -.stat { - display: flex; - flex-direction: column; -} - -.stat-value { - font-size: 2rem; - font-weight: 800; - background: var(--accent-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.stat-label { - font-size: 0.875rem; - color: var(--text-muted); - margin-top: 4px; -} - -/* Waitlist Form */ -.waitlist-form { - max-width: 560px; -} - -.form-row { - display: flex; - gap: 12px; - margin-bottom: 12px; -} - -.form-row.secondary { - gap: 12px; -} - -.form-row.secondary input, -.form-row.secondary select { - flex: 1; -} - -.waitlist-form input[type="email"], -.waitlist-form input[type="text"], -.waitlist-form select { - flex: 1; - padding: 14px 18px; - border-radius: var(--radius-sm); - border: 1px solid var(--border-light); - background: var(--bg-card); - color: var(--text-primary); - font-size: 1rem; - font-family: var(--font-sans); - outline: none; - transition: border-color 0.2s; -} - -.waitlist-form input:focus, -.waitlist-form select:focus { - border-color: var(--accent-primary); -} - -.waitlist-form select { - cursor: pointer; - appearance: none; - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); - background-position: right 12px center; - background-repeat: no-repeat; - padding-right: 40px; -} - -.waitlist-form button { - padding: 14px 28px; - border-radius: var(--radius-sm); - border: none; - background: var(--accent-gradient); - color: white; - font-size: 1rem; - font-weight: 600; - font-family: var(--font-sans); - cursor: pointer; - white-space: nowrap; - transition: opacity 0.2s, transform 0.1s; -} - -.waitlist-form button:hover:not(:disabled) { - opacity: 0.9; - transform: translateY(-1px); -} - -.waitlist-form button:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.form-error { - color: var(--error); - font-size: 0.875rem; - margin-top: 8px; -} - -/* Waitlist Success */ -.waitlist-success { - text-align: center; - padding: 32px; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius); - max-width: 400px; -} - -.success-icon { - width: 48px; - height: 48px; - border-radius: 50%; - background: rgba(34, 197, 94, 0.1); - color: var(--success); - display: flex; - align-items: center; - justify-content: center; - font-size: 1.5rem; - font-weight: 700; - margin: 0 auto 16px; -} - -.waitlist-success h3 { - margin-bottom: 8px; - font-size: 1.25rem; -} - -.waitlist-success p { - color: var(--text-secondary); - font-size: 0.938rem; -} - -/* Features Section */ -.features { - padding: 120px 0; - background: var(--bg-secondary); -} - -.features-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 24px; -} - -.feature-card { - padding: 32px; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius); - transition: border-color 0.2s, transform 0.2s; -} - -.feature-card:hover { - border-color: var(--border-light); - transform: translateY(-2px); -} - -.feature-icon { - font-size: 2rem; - margin-bottom: 16px; -} - -.feature-title { - font-size: 1.125rem; - font-weight: 600; - margin-bottom: 12px; -} - -.feature-desc { - font-size: 0.938rem; - color: var(--text-secondary); - line-height: 1.7; -} - -/* Tiers Section */ -.tiers { - padding: 120px 0; -} - -.tier-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 24px; - margin-bottom: 64px; -} - -.tier-card { - padding: 40px 32px; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius); - position: relative; - display: flex; - flex-direction: column; -} - -.tier-highlighted { - border-color: var(--accent-primary); - box-shadow: 0 0 30px rgba(59, 130, 246, 0.1); - transform: scale(1.05); -} - -.tier-badge { - position: absolute; - top: -12px; - left: 50%; - transform: translateX(-50%); - padding: 4px 16px; - border-radius: 999px; - background: var(--accent-gradient); - color: white; - font-size: 0.813rem; - font-weight: 600; - white-space: nowrap; -} - -.tier-name { - font-size: 1.25rem; - font-weight: 700; - margin-bottom: 12px; -} - -.tier-price { - margin-bottom: 8px; -} - -.price-value { - font-size: 3rem; - font-weight: 800; -} - -.price-period { - font-size: 1rem; - color: var(--text-muted); -} - -.tier-desc { - color: var(--text-secondary); - font-size: 0.938rem; - margin-bottom: 24px; - flex: 1; -} - -.tier-card .waitlist-form { - max-width: 100%; -} - -.tier-card .form-row { - flex-direction: column; -} - -.tier-card .waitlist-form input { - width: 100%; -} - -.tier-card .waitlist-form button { - width: 100%; -} - -/* Tier Table */ -.tier-table-wrapper { - overflow-x: auto; -} - -.tier-table { - width: 100%; - border-collapse: collapse; - font-size: 0.938rem; -} - -.tier-table th, -.tier-table td { - padding: 14px 20px; - text-align: center; - border-bottom: 1px solid var(--border-color); -} - -.tier-table th { - font-weight: 600; - color: var(--text-secondary); - text-transform: uppercase; - font-size: 0.813rem; - letter-spacing: 0.05em; -} - -.tier-table th:first-child, -.tier-table td:first-child { - text-align: left; -} - -.tier-table td.feature-name { - color: var(--text-primary); - font-weight: 500; -} - -.col-highlighted { - background: rgba(59, 130, 246, 0.05); -} - -.check { - color: var(--success); - font-weight: 700; -} - -.cross { - color: var(--text-muted); -} - -/* Blog Preview */ -.blog-preview { - padding: 120px 0; - background: var(--bg-secondary); -} - -.blog-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 24px; - margin-bottom: 48px; -} - -.blog-card { - display: block; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius); - overflow: hidden; - transition: border-color 0.2s, transform 0.2s; -} - -.blog-card:hover { - border-color: var(--border-light); - transform: translateY(-2px); -} - -.blog-card-image { - height: 200px; - overflow: hidden; -} - -.blog-card-image img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.blog-card-body { - padding: 24px; -} - -.blog-card-tags { - display: flex; - gap: 8px; - margin-bottom: 12px; - flex-wrap: wrap; -} - -.tag { - padding: 3px 10px; - border-radius: 999px; - background: rgba(59, 130, 246, 0.1); - color: var(--accent-primary); - font-size: 0.813rem; - font-weight: 500; -} - -.blog-card-body h3 { - font-size: 1.125rem; - font-weight: 600; - margin-bottom: 8px; - line-height: 1.4; -} - -.blog-card-body p { - font-size: 0.875rem; - color: var(--text-secondary); - line-height: 1.6; - margin-bottom: 16px; -} - -.blog-card-meta { - display: flex; - gap: 16px; - font-size: 0.813rem; - color: var(--text-muted); -} - -.blog-placeholder { - text-align: center; - padding: 64px 24px; - background: var(--bg-card); - border: 1px dashed var(--border-color); - border-radius: var(--radius); - margin-bottom: 48px; -} - -.blog-placeholder p { - color: var(--text-secondary); - font-size: 1rem; -} - -.blog-cta { - text-align: center; -} - -.btn-secondary { - display: inline-block; - padding: 12px 28px; - border-radius: var(--radius-sm); - border: 1px solid var(--border-light); - color: var(--text-primary); - font-size: 0.938rem; - font-weight: 500; - font-family: var(--font-sans); - cursor: pointer; - transition: border-color 0.2s, background 0.2s; -} - -.btn-secondary:hover { - border-color: var(--accent-primary); - background: rgba(59, 130, 246, 0.05); -} - -/* Footer */ -.footer { - padding: 80px 0 40px; - background: var(--bg-primary); - border-top: 1px solid var(--border-color); -} - -.footer-grid { - display: grid; - grid-template-columns: 2fr 1fr 1fr 1fr; - gap: 48px; - margin-bottom: 48px; -} - -.footer-brand h3 { - font-size: 1.5rem; - font-weight: 800; - margin-bottom: 8px; - background: var(--accent-gradient); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.footer-brand p { - color: var(--text-muted); - font-size: 0.938rem; - max-width: 300px; -} - -.footer-links h4 { - font-size: 0.875rem; - font-weight: 600; - text-transform: uppercase; - color: var(--text-muted); - letter-spacing: 0.05em; - margin-bottom: 16px; -} - -.footer-links a { - display: block; - color: var(--text-secondary); - font-size: 0.938rem; - padding: 4px 0; - transition: color 0.2s; -} - -.footer-links a:hover { - color: var(--text-primary); -} - -.footer-bottom { - padding-top: 32px; - border-top: 1px solid var(--border-color); - text-align: center; -} - -.footer-bottom p { - color: var(--text-muted); - font-size: 0.875rem; -} - -/* Blog Page */ -.blog-page-header { - padding: 80px 0 48px; - background: var(--bg-secondary); -} - -.back-link { - display: inline-block; - margin-bottom: 24px; - font-size: 0.938rem; -} - -.blog-page-header h1 { - font-size: 3rem; - font-weight: 800; - margin-bottom: 16px; -} - -.blog-page-header p { - color: var(--text-secondary); - font-size: 1.125rem; - max-width: 600px; -} - -.blog-page-content { - padding: 64px 0; -} - -.blog-list { - display: flex; - flex-direction: column; - gap: 24px; -} - -.blog-list-item { - display: flex; - gap: 24px; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius); - overflow: hidden; - transition: border-color 0.2s; -} - -.blog-list-item:hover { - border-color: var(--border-light); -} - -.blog-list-image { - width: 280px; - min-height: 180px; - flex-shrink: 0; - overflow: hidden; -} - -.blog-list-image img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.blog-list-body { - padding: 24px; - flex: 1; -} - -.blog-list-tags { - display: flex; - gap: 8px; - margin-bottom: 12px; - flex-wrap: wrap; -} - -.blog-list-body h2 { - font-size: 1.5rem; - font-weight: 700; - margin-bottom: 12px; -} - -.blog-list-body p { - color: var(--text-secondary); - font-size: 0.938rem; - line-height: 1.7; - margin-bottom: 16px; -} - -.blog-list-meta { - display: flex; - gap: 16px; - font-size: 0.875rem; - color: var(--text-muted); -} - -/* Blog Post */ -.blog-post { - padding: 80px 0; -} - -.blog-post-header { - margin-bottom: 48px; -} - -.blog-post-header h1 { - font-size: 3rem; - font-weight: 800; - line-height: 1.2; - margin-bottom: 16px; -} - -.blog-post-meta { - display: flex; - gap: 16px; - color: var(--text-muted); - font-size: 0.938rem; - margin-bottom: 16px; -} - -.blog-post-tags { - display: flex; - gap: 8px; - flex-wrap: wrap; -} - -.blog-post-cover { - border-radius: var(--radius); - overflow: hidden; - margin-bottom: 48px; -} - -.blog-post-cover img { - width: 100%; - max-height: 500px; - object-fit: cover; -} - -.blog-post-content { - max-width: 720px; - font-size: 1.063rem; - line-height: 1.8; - color: var(--text-secondary); -} - -.blog-post-content h2 { - font-size: 1.75rem; - font-weight: 700; - color: var(--text-primary); - margin: 48px 0 16px; -} - -.blog-post-content h3 { - font-size: 1.25rem; - font-weight: 600; - color: var(--text-primary); - margin: 32px 0 12px; -} - -.blog-post-content p { - margin-bottom: 20px; -} - -.blog-post-content ul, -.blog-post-content ol { - margin-bottom: 20px; - padding-left: 24px; -} - -.blog-post-content li { - margin-bottom: 8px; -} - -.blog-post-content code { - background: var(--bg-card); - padding: 2px 6px; - border-radius: 4px; - font-size: 0.875em; -} - -.blog-post-content pre { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - padding: 20px; - overflow-x: auto; - margin-bottom: 24px; -} - -.blog-post-content pre code { - background: none; - padding: 0; -} - -.blog-post-content blockquote { - border-left: 3px solid var(--accent-primary); - padding-left: 20px; - color: var(--text-secondary); - font-style: italic; - margin: 24px 0; -} - -/* Loading / Empty */ -.loading { - text-align: center; - padding: 64px 0; - color: var(--text-muted); -} - -.empty-state { - text-align: center; - padding: 64px 24px; - background: var(--bg-card); - border: 1px dashed var(--border-color); - border-radius: var(--radius); -} - -.empty-state h2 { - margin-bottom: 12px; -} - -.empty-state p { - color: var(--text-secondary); -} - -/* Blog Waitlist CTA */ -.blog-waitlist-cta { - background: var(--bg-card); - border-top: 1px solid var(--border-color); - padding: 80px 0; - text-align: center; -} - -.blog-waitlist-cta h2 { - font-size: 2rem; - font-weight: 700; - margin-bottom: 12px; -} - -.blog-waitlist-cta p { - color: var(--text-secondary); - margin-bottom: 32px; - font-size: 1.125rem; -} - -.blog-waitlist-cta .hero-form { - max-width: 500px; - margin: 0 auto; -} - -/* Responsive */ -@media (max-width: 1024px) { - .features-grid { - grid-template-columns: repeat(2, 1fr); - } - - .tier-grid { - grid-template-columns: repeat(2, 1fr); - } - - .tier-highlighted { - transform: none; - } - - .blog-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (max-width: 768px) { - .hero-title { - font-size: 2.5rem; - } - - .hero-subtitle { - font-size: 1.063rem; - } - - .hero-stats { - flex-direction: column; - gap: 24px; - margin-top: 48px; - } - - .section-title { - font-size: 2rem; - } - - .features-grid { - grid-template-columns: 1fr; - } - - .tier-grid { - grid-template-columns: 1fr; - max-width: 400px; - margin-left: auto; - margin-right: auto; - } - - .form-row { - flex-direction: column; - } - - .footer-grid { - grid-template-columns: 1fr 1fr; - gap: 32px; - } - - .blog-grid { - grid-template-columns: 1fr; - } - - .blog-list-item { - flex-direction: column; - } - - .blog-list-image { - width: 100%; - height: 200px; - } - - .blog-page-header h1 { - font-size: 2rem; - } - - .blog-post-header h1 { - font-size: 2rem; - } -} - -/* Dashboard Page */ -.dashboard-page { - min-height: 100vh; - padding: 80px 0; - background: var(--bg-primary); -} - -.dashboard-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - margin-bottom: 48px; - gap: 24px; - flex-wrap: wrap; -} - -.dashboard-header h1 { - font-size: 2.5rem; - font-weight: 800; - margin-bottom: 8px; -} - -.dashboard-subtitle { - color: var(--text-secondary); - font-size: 1.063rem; -} - -.dashboard-actions { - display: flex; - align-items: center; - gap: 16px; -} - -.dashboard-stats-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 20px; - margin-bottom: 48px; -} - -.dashboard-stat-card { - display: flex; - align-items: center; - gap: 16px; - padding: 24px; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius); -} - -.dashboard-stat-icon { - width: 48px; - height: 48px; - border-radius: var(--radius-sm); - display: flex; - align-items: center; - justify-content: center; - font-size: 1.5rem; - flex-shrink: 0; -} - -.dashboard-stat-info { - display: flex; - flex-direction: column; -} - -.dashboard-stat-value { - font-size: 1.75rem; - font-weight: 800; - color: var(--text-primary); -} - -.dashboard-stat-label { - font-size: 0.813rem; - color: var(--text-muted); - text-transform: uppercase; - letter-spacing: 0.05em; - font-weight: 500; -} - -.dashboard-grid { - display: flex; - flex-direction: column; - gap: 32px; -} - -.dashboard-section { - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius); - padding: 32px; -} - -.section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 24px; -} - -.section-header h2 { - font-size: 1.25rem; - font-weight: 700; -} - -.btn-link { - background: none; - border: none; - color: var(--accent-primary); - font-size: 0.938rem; - font-weight: 500; - cursor: pointer; - padding: 4px 8px; - border-radius: var(--radius-sm); - transition: background 0.2s; -} - -.btn-link:hover { - background: rgba(59, 130, 246, 0.1); -} - -.btn-primary { - padding: 10px 20px; - border-radius: var(--radius-sm); - border: none; - background: var(--accent-gradient); - color: white; - font-size: 0.938rem; - font-weight: 600; - font-family: var(--font-sans); - cursor: pointer; - transition: opacity 0.2s; -} - -.btn-primary:hover { - opacity: 0.9; -} - -.tier-badge { - display: inline-block; - padding: 4px 12px; - border-radius: 999px; - font-size: 0.813rem; - font-weight: 600; -} - -.tier-free { - background: rgba(148, 163, 184, 0.1); - color: var(--text-muted); -} - -.tier-basic { - background: rgba(59, 130, 246, 0.1); - color: var(--accent-primary); -} - -.tier-plus { - background: rgba(168, 85, 247, 0.1); - color: #a855f7; -} - -.tier-premium { - background: rgba(245, 158, 11, 0.1); - color: #f59e0b; -} - -.add-property-form { - margin-bottom: 24px; -} - -.add-property-form h3 { - font-size: 1rem; - font-weight: 600; - margin-bottom: 12px; - color: var(--text-secondary); -} - -.add-property-form .form-row { - display: flex; - gap: 12px; -} - -.add-property-form input[type="text"] { - flex: 1; - padding: 12px 16px; - border-radius: var(--radius-sm); - border: 1px solid var(--border-light); - background: var(--bg-secondary); - color: var(--text-primary); - font-size: 0.938rem; - font-family: var(--font-sans); - outline: none; - transition: border-color 0.2s; -} - -.add-property-form input[type="text"]:focus { - border-color: var(--accent-primary); -} - -.add-property-form button { - padding: 12px 20px; - border-radius: var(--radius-sm); - border: none; - background: var(--accent-gradient); - color: white; - font-size: 0.938rem; - font-weight: 600; - font-family: var(--font-sans); - cursor: pointer; - transition: opacity 0.2s; - white-space: nowrap; -} - -.add-property-form button:hover:not(:disabled) { - opacity: 0.9; -} - -.add-property-form button:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.limit-warning { - margin-top: 8px; - font-size: 0.813rem; - color: var(--text-muted); -} - -.properties-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 16px; - margin-top: 24px; -} - -.property-card { - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius); - padding: 20px; - transition: border-color 0.2s; -} - -.property-card:hover { - border-color: var(--border-light); -} - -.property-card-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 12px; - margin-bottom: 12px; -} - -.property-address { - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); - word-break: break-word; -} - -.badge { - display: inline-block; - padding: 3px 10px; - border-radius: 999px; - font-size: 0.75rem; - font-weight: 600; - flex-shrink: 0; -} - -.badge-ok { - background: rgba(34, 197, 94, 0.1); - color: var(--success); -} - -.badge-alert { - background: rgba(249, 115, 22, 0.1); - color: #f97316; -} - -.badge-error { - background: rgba(239, 68, 68, 0.1); - color: var(--error); -} - -.property-card-body { - margin-bottom: 12px; -} - -.property-meta { - display: flex; - gap: 16px; - font-size: 0.813rem; - color: var(--text-muted); -} - -.property-card-footer { - display: flex; - justify-content: flex-end; -} - -.btn-icon { - background: none; - border: 1px solid var(--border-light); - color: var(--text-muted); - width: 32px; - height: 32px; - border-radius: var(--radius-sm); - font-size: 1.25rem; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: border-color 0.2s, color 0.2s; -} - -.btn-icon:hover { - border-color: var(--error); - color: var(--error); -} - -.alerts-list { - display: flex; - flex-direction: column; - gap: 12px; - margin-top: 24px; -} - -.alert-item { - display: flex; - gap: 12px; - padding: 16px; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: var(--radius-sm); - transition: border-color 0.2s; -} - -.alert-unread { - border-color: var(--accent-primary); - background: rgba(59, 130, 246, 0.03); -} - -.alert-read { - opacity: 0.6; -} - -.alert-severity { - padding: 4px 10px; - border-radius: 6px; - font-size: 0.688rem; - font-weight: 700; - color: white; - text-transform: uppercase; - letter-spacing: 0.05em; - flex-shrink: 0; - width: 64px; - text-align: center; -} - -.alert-content { - flex: 1; - min-width: 0; -} - -.alert-title { - font-size: 0.938rem; - font-weight: 600; - color: var(--text-primary); - margin-bottom: 4px; -} - -.alert-property { - font-size: 0.813rem; - color: var(--text-muted); - margin-bottom: 4px; - word-break: break-word; -} - -.alert-message { - font-size: 0.813rem; - color: var(--text-secondary); - margin-bottom: 8px; - line-height: 1.5; -} - -.alert-time { - font-size: 0.75rem; - color: var(--text-muted); -} - -.btn-small { - padding: 6px 12px; - border-radius: var(--radius-sm); - border: 1px solid var(--border-light); - background: none; - color: var(--text-secondary); - font-size: 0.813rem; - font-weight: 500; - font-family: var(--font-sans); - cursor: pointer; - flex-shrink: 0; - transition: border-color 0.2s, color 0.2s; -} - -.btn-small:hover { - border-color: var(--accent-primary); - color: var(--accent-primary); -} - -.error-banner { - text-align: center; - padding: 64px 24px; - background: var(--bg-card); - border: 1px solid var(--border-color); - border-radius: var(--radius); -} - -.error-banner h2 { - font-size: 1.5rem; - font-weight: 700; - margin-bottom: 12px; - color: var(--text-primary); -} - -.error-banner p { - color: var(--text-secondary); - font-size: 1rem; - max-width: 400px; - margin: 0 auto; -} - -/* Dashboard responsive */ -@media (max-width: 1024px) { - .dashboard-stats-grid { - grid-template-columns: repeat(2, 1fr); - } -} - -@media (max-width: 768px) { - .dashboard-header { - flex-direction: column; - } - - .dashboard-header h1 { - font-size: 2rem; - } - - .dashboard-stats-grid { - grid-template-columns: 1fr; - } - - .properties-grid { - grid-template-columns: 1fr; - } - - .add-property-form .form-row { - flex-direction: column; - } - - .dashboard-actions { - flex-direction: column; - align-items: flex-start; - } -} diff --git a/packages/web/src/main.tsx b/packages/web/src/main.tsx deleted file mode 100644 index a3ad2df..0000000 --- a/packages/web/src/main.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { render } from 'solid-js/web'; -import { Router, Route } from '@solidjs/router'; -import App from './App'; -import LandingPage from './pages/LandingPage'; -import AdsLandingPage from './pages/AdsLandingPage'; -import BlogPage from './pages/BlogPage'; -import BlogPostPage from './pages/BlogPostPage'; -import DashboardPage from './pages/DashboardPage'; -import './index.css'; - -const root = document.getElementById('root'); -if (!root) throw new Error('Root element not found'); - -render(() => ( - - - - - - - -), root); diff --git a/packages/web/src/pages/AdsLandingPage.tsx b/packages/web/src/pages/AdsLandingPage.tsx deleted file mode 100644 index 44b9ead..0000000 --- a/packages/web/src/pages/AdsLandingPage.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, onMount } from 'solid-js'; -import { initAnalytics, trackPageView } from '../hooks/useAnalytics'; -import HeroSection from '../components/HeroSection'; -import FeaturesSection from '../components/FeaturesSection'; -import TierComparison from '../components/TierComparison'; -import Footer from '../components/Footer'; - -const AdsLandingPage: Component = () => { - onMount(() => { - initAnalytics(); - trackPageView('/ads', 'ShieldAI — Ads | AI-Powered Identity Protection'); - }); - - return ( -
          - - - -
          -
          - ); -}; - -export default AdsLandingPage; diff --git a/packages/web/src/pages/BlogPage.tsx b/packages/web/src/pages/BlogPage.tsx deleted file mode 100644 index 89a8226..0000000 --- a/packages/web/src/pages/BlogPage.tsx +++ /dev/null @@ -1,121 +0,0 @@ -import { Component, createSignal, onMount, For } from 'solid-js'; -import { initAnalytics, trackPageView } from '../hooks/useAnalytics'; -import WaitlistForm from '../components/WaitlistForm'; -import Footer from '../components/Footer'; - -interface BlogPost { - slug: string; - title: string; - excerpt: string | null; - authorName: string | null; - coverImageUrl: string | null; - tags: string[]; - publishedAt: string; - viewCount: number; -} - -interface Pagination { - page: number; - limit: number; - total: number; - totalPages: number; -} - -const BlogPage: Component = () => { - const [posts, setPosts] = createSignal([]); - const [pagination, setPagination] = createSignal(null); - const [loading, setLoading] = createSignal(true); - - onMount(async () => { - initAnalytics(); - trackPageView('/blog', 'Blog — Free Rights & Strategies'); - - try { - const res = await fetch('/api/blog?limit=10'); - if (res.ok) { - const data = await res.json(); - setPosts(data.posts); - setPagination(data.pagination); - } - } catch { - // handle error silently - } finally { - setLoading(false); - } - }); - - return ( -
          -
          -
          - ← Back to ShieldAI -

          Free Rights & Strategies

          -

          Educational guides to protect yourself and your family from AI-powered scams.

          -
          -
          - -
          -
          - {loading() ? ( -

          Loading articles...

          - ) : posts().length === 0 ? ( -
          -

          Coming Soon

          -

          Our educational blog posts are being written. Join the waitlist to be notified when we publish.

          -
          - ) : ( - <> - - - {pagination() && pagination()!.totalPages > 1 && ( - - )} - - )} -
          -
          - -
          -
          -

          Stay Protected

          -

          Get notified when we publish new guides and early access to ShieldAI.

          - -
          -
          - -
          -
          - ); -}; - -export default BlogPage; diff --git a/packages/web/src/pages/BlogPostPage.tsx b/packages/web/src/pages/BlogPostPage.tsx deleted file mode 100644 index 0ec182f..0000000 --- a/packages/web/src/pages/BlogPostPage.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Component, createSignal, onMount } from 'solid-js'; -import { useParams } from '@solidjs/router'; -import { initAnalytics, trackPageView } from '../hooks/useAnalytics'; -import Footer from '../components/Footer'; - -interface BlogPost { - slug: string; - title: string; - excerpt: string | null; - content: string; - authorName: string | null; - coverImageUrl: string | null; - tags: string[]; - publishedAt: string; - viewCount: number; -} - -const BlogPostPage: Component = () => { - const params = useParams(); - const [post, setPost] = createSignal(null); - const [loading, setLoading] = createSignal(true); - const [notFound, setNotFound] = createSignal(false); - - onMount(async () => { - initAnalytics(); - - try { - const res = await fetch(`/api/blog/${params.slug}`); - if (res.ok) { - const data = await res.json(); - setPost(data.post); - trackPageView(`/blog/${params.slug}`, data.post.title); - } else { - setNotFound(true); - } - } catch { - setNotFound(true); - } finally { - setLoading(false); - } - }); - - return ( -
          - {loading() ? ( -
          -
          -

          Loading article...

          -
          -
          - ) : notFound() || !post() ? ( -
          -
          - ← Back to Blog -

          Article Not Found

          -

          The article you're looking for doesn't exist or has been removed.

          -
          -
          - ) : ( - <> -
          -
          - ← Back to Blog -
          -

          {post()!.title}

          - - {post()!.tags.length > 0 && ( - - )} -
          - {post()!.coverImageUrl && ( -
          - {post()!.title} -
          - )} -
          -
          -
          -
          - - )} -
          - ); -}; - -export default BlogPostPage; diff --git a/packages/web/src/pages/DashboardPage.tsx b/packages/web/src/pages/DashboardPage.tsx deleted file mode 100644 index d17a73f..0000000 --- a/packages/web/src/pages/DashboardPage.tsx +++ /dev/null @@ -1,458 +0,0 @@ -import { Component, JSX, onMount, createSignal } from 'solid-js'; -import { ComponentProps } from 'solid-js'; - -interface StatCardProps { - title: string; - value: string | number; - icon: JSX.Element; - color?: string; -} - -const StatCard: Component = (props) => { - return ( -
          -
          - {props.icon} -
          -
          -
          {props.value}
          -
          {props.title}
          -
          -
          - ); -}; - -interface PropertyCardProps { - id: string; - address: string; - status: 'monitored' | 'alert' | 'error'; - lastChange: string | null; - alertCount: number; - onRemove: (id: string) => void; -} - -const PropertyCard: Component = (props) => { - const statusBadge = () => { - switch (props.status) { - case 'alert': - return Alert; - case 'error': - return Error; - default: - return Monitored; - } - }; - - const formatDate = (dateStr: string | null) => { - if (!dateStr) return 'Never'; - return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); - }; - - return ( -
          -
          -
          {props.address}
          - {statusBadge()} -
          -
          -
          - Changes: {props.alertCount} - Last change: {formatDate(props.lastChange)} -
          -
          - -
          - ); -}; - -interface AlertItem { - id: string; - propertyAddress: string; - severity: string; - title: string; - message: string; - isRead: boolean; - createdAt: string; -} - -interface AlertsListProps { - alerts: AlertItem[]; - onMarkRead: (id: string) => void; -} - -const AlertsList: Component = (props) => { - const severityColor = (severity: string) => { - switch (severity) { - case 'CRITICAL': - return 'var(--error)'; - case 'HIGH': - return '#f97316'; - case 'MEDIUM': - return '#eab308'; - default: - return 'var(--accent-primary)'; - } - }; - - const formatDate = (dateStr: string) => { - return new Date(dateStr).toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); - }; - - if (props.alerts.length === 0) { - return ( -
          -

          No alerts

          -

          All monitored properties are secure.

          -
          - ); - } - - return ( -
          - {props.alerts.map((alert) => ( -
          -
          - {alert.severity} -
          -
          -
          {alert.title}
          -
          {alert.propertyAddress}
          -
          {alert.message}
          -
          {formatDate(alert.createdAt)}
          -
          - {!alert.isRead && ( - - )} -
          - ))} -
          - ); -}; - -interface AddPropertyFormProps { - onAdd: (address: string) => Promise; - canAddMore: boolean; - limit: number; - monitoredCount: number; -} - -const AddPropertyForm: Component = (props) => { - const [address, setAddress] = createSignal(''); - const [loading, setLoading] = createSignal(false); - const [error, setError] = createSignal(''); - - const handleSubmit = async (e: SubmitEvent) => { - e.preventDefault(); - const addr = address().trim(); - if (!addr) { - setError('Address is required'); - return; - } - const addressPattern = /^\d+[\s,].+$/i; - if (!addressPattern.test(addr)) { - setError('Please enter a valid street address (e.g., 123 Main Street)'); - return; - } - setLoading(true); - setError(''); - try { - await props.onAdd(addr); - setAddress(''); - } catch { - setError('Failed to add property. Please try again.'); - } finally { - setLoading(false); - } - }; - - return ( -
          -

          Add a property to monitor

          -
          handleSubmit(e as unknown as SubmitEvent)}> -
          - setAddress((e.target as HTMLInputElement).value)} - disabled={loading() || !props.canAddMore} - /> - -
          -
          - {error() &&
          {error()}
          } - {!props.canAddMore && ( -
          - Property limit reached ({props.monitoredCount}/{props.limit}). Upgrade to add more. -
          - )} -
          - ); -}; - -const DashboardPage: Component = () => { - const [stats, setStats] = createSignal<{ - monitoredProperties: number; - tier: string; - isPremium: boolean; - propertyLimit: number; - canAddMore: boolean; - recentAlertCount: number; - criticalAlertCount: number; - } | null>(null); - - const [properties, setProperties] = createSignal>([]); - - const [alerts, setAlerts] = createSignal([]); - const [loading, setLoading] = createSignal(true); - const [error, setError] = createSignal(''); - const [showAlerts, setShowAlerts] = createSignal(false); - - const fetchStats = async () => { - try { - const res = await fetch('/hometitle/properties/stats'); - if (!res.ok) { - if (res.status === 401) { - setError('Please log in to view your dashboard'); - } else if (res.status === 404) { - setError('No active subscription found'); - } else if (res.status === 402) { - const data = await res.json(); - setError(data.message || 'Premium tier required for home title monitoring'); - } - return; - } - const data = await res.json(); - setStats(data); - } catch { - setError('Failed to load dashboard stats'); - } - }; - - const fetchProperties = async () => { - try { - const res = await fetch('/hometitle/properties'); - if (!res.ok) return; - const data = await res.json(); - setProperties(data.properties || []); - } catch { - // Silently fail - properties may not be available - } - }; - - const fetchAlerts = async () => { - try { - const res = await fetch('/hometitle/alerts?limit=20'); - if (!res.ok) return; - const data = await res.json(); - setAlerts(data.alerts || []); - } catch { - // Silently fail - } - }; - - const loadData = async () => { - setLoading(true); - await Promise.all([fetchStats(), fetchProperties(), fetchAlerts()]); - setLoading(false); - }; - - onMount(() => { - loadData(); - }); - - const handleAddProperty = async (address: string) => { - try { - const res = await fetch('/hometitle/properties', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ address }), - }); - if (!res.ok) { - const data = await res.json(); - throw new Error(data.message || 'Failed to add property'); - } - await loadData(); - } catch (err) { - throw err; - } - }; - - const handleRemoveProperty = async (id: string) => { - try { - const res = await fetch(`/hometitle/properties/${id}`, { method: 'DELETE' }); - if (!res.ok) return; - await loadData(); - } catch { - // Silently fail - } - }; - - const handleMarkAlertRead = async (id: string) => { - try { - const res = await fetch(`/hometitle/alerts/${id}/read`, { method: 'PATCH' }); - if (!res.ok) return; - setAlerts((prev) => prev.map((a) => (a.id === id ? { ...a, isRead: true } : a))); - } catch { - // Silently fail - } - }; - - const handleScan = async () => { - try { - const res = await fetch('/hometitle/scan', { method: 'POST' }); - if (!res.ok) return; - await loadData(); - } catch { - // Silently fail - } - }; - - const errorTitle = () => { - if (error().includes('Premium')) return 'Upgrade Required'; - if (error().includes('subscription')) return 'Subscription Needed'; - return 'Dashboard Unavailable'; - }; - - if (loading()) { - return ( -
          -
          -
          -

          Home Title Monitor

          -

          Monitor your properties for title changes and alerts

          -
          -
          Loading dashboard...
          -
          -
          - ); - } - - if (error()) { - return ( -
          -
          -
          -

          Home Title Monitor

          -

          Monitor your properties for title changes and alerts

          -
          -
          -

          {errorTitle()}

          -

          {error()}

          -
          -
          -
          - ); - } - - const s = stats() || { monitoredProperties: 0, tier: 'free', isPremium: false, propertyLimit: 0, canAddMore: false, recentAlertCount: 0, criticalAlertCount: 0 }; - - return ( -
          -
          -
          -
          -

          Home Title Monitor

          -

          Monitor your properties for title changes and alerts

          -
          -
          - {s.tier} tier - {s.isPremium && ( - - )} -
          -
          - -
          - 🏠} - color="rgba(59, 130, 246, 0.1)" - /> - 🔔} - color="rgba(249, 115, 22, 0.1)" - /> - ⚠} - color="rgba(239, 68, 68, 0.1)" - /> - 📋} - color="rgba(34, 197, 94, 0.1)" - /> -
          - -
          -
          -
          -

          Monitored Properties

          - -
          - - {showAlerts() ? ( - - ) : ( - <> - - {properties().length === 0 ? ( -
          -

          No properties monitored

          -

          Add your first property above to start monitoring for title changes.

          -
          - ) : ( -
          - {properties().map((p) => ( - - ))} -
          - )} - - )} -
          -
          -
          -
          - ); -}; - -export default DashboardPage; diff --git a/packages/web/src/pages/LandingPage.tsx b/packages/web/src/pages/LandingPage.tsx deleted file mode 100644 index 2bada04..0000000 --- a/packages/web/src/pages/LandingPage.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Component, onMount } from 'solid-js'; -import { initAnalytics, trackPageView } from '../hooks/useAnalytics'; -import HeroSection from '../components/HeroSection'; -import FeaturesSection from '../components/FeaturesSection'; -import TierComparison from '../components/TierComparison'; -import BlogPreview from '../components/BlogPreview'; -import Footer from '../components/Footer'; - -const LandingPage: Component = () => { - onMount(() => { - initAnalytics(); - trackPageView('/', 'ShieldAI — AI-Powered Identity Protection'); - }); - - return ( -
          - - - - -
          -
          - ); -}; - -export default LandingPage; diff --git a/packages/web/tsconfig.json b/packages/web/tsconfig.json deleted file mode 100644 index 5e8ee3f..0000000 --- a/packages/web/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ESNext", - "moduleResolution": "bundler", - "jsx": "preserve", - "jsxImportSource": "solid-js", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "baseUrl": ".", - "paths": { - "@/*": ["src/*"] - } - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/packages/web/tsconfig.node.json b/packages/web/tsconfig.node.json deleted file mode 100644 index 42872c5..0000000 --- a/packages/web/tsconfig.node.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts deleted file mode 100644 index 3c3cdd3..0000000 --- a/packages/web/vite.config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig } from 'vite'; -import solidPlugin from 'vite-plugin-solid'; - -export default defineConfig({ - plugins: [solidPlugin()], - server: { - port: 3001, - proxy: { - '/api': { - target: 'http://localhost:3000', - changeOrigin: true, - }, - }, - }, - build: { - target: 'esnext', - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf6a86d..025e930 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,590 +7,40 @@ settings: importers: .: - dependencies: - ws: - specifier: ^8.16.0 - version: 8.20.1 devDependencies: '@types/node': specifier: ^25.6.0 - version: 25.8.0 - '@types/ws': - specifier: ^8.5.10 - version: 8.18.1 + version: 25.9.1 '@vitest/coverage-v8': specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) + version: 4.1.7(vitest@4.1.7) turbo: specifier: ^2.3.0 - version: 2.9.12 + version: 2.9.14 typescript: specifier: ^5.7.0 version: 5.9.3 vitest: specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) + version: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) - packages/api: + web: dependencies: - '@fastify/cors': - specifier: ^10.0.1 - version: 10.1.0 - '@fastify/helmet': - specifier: ^13.0.1 - version: 13.0.2 - '@fastify/multipart': - specifier: ^7.7.3 - version: 7.7.3 - '@fastify/rate-limit': - specifier: ^9.0.0 - version: 9.1.0 - '@fastify/sensible': - specifier: ^6.0.1 - version: 6.0.4 - '@fastify/swagger': - specifier: ^9.4.0 - version: 9.7.0 - '@fastify/swagger-ui': - specifier: ^5.2.0 - version: 5.2.6 - '@shieldai/correlation': - specifier: workspace:* - version: link:../correlation - '@shieldai/db': - specifier: workspace:* - version: link:../db - '@shieldai/monitoring': - specifier: workspace:* - version: link:../monitoring - '@shieldai/removebrokers': - specifier: workspace:* - version: link:../../services/removebrokers - '@shieldai/report': - specifier: workspace:* - version: link:../report - '@shieldai/shared-notifications': - specifier: workspace:* - version: link:../shared-notifications - '@shieldai/types': - specifier: workspace:* - version: link:../types - '@shieldai/voiceprint': - specifier: workspace:* - version: link:../../services/voiceprint - '@shieldsai/shared-auth': - specifier: workspace:* - version: link:../shared-auth - bullmq: - specifier: ^5.24.0 - version: 5.76.8 - fastify: - specifier: ^5.2.0 - version: 5.8.5 - fastify-raw-body: - specifier: ^5.0.0 - version: 5.0.0 - ioredis: - specifier: ^5.4.0 - version: 5.10.1 - jsonwebtoken: - specifier: ^9.0.2 - version: 9.0.3 - devDependencies: - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - packages/correlation: - dependencies: - '@shieldai/db': - specifier: workspace:* - version: link:../db - '@shieldai/types': - specifier: workspace:* - version: link:../types - - packages/db: - dependencies: - '@prisma/client': - specifier: ^6.2.0 - version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) - prisma: - specifier: ^6.2.0 - version: 6.19.3(typescript@5.9.3) - zod: - specifier: ^4.3.6 - version: 4.4.3 - devDependencies: - tsx: - specifier: ^4.19.0 - version: 4.22.0 - typescript: - specifier: ^5.3.3 - version: 5.9.3 - - packages/extension: - dependencies: - '@shieldai/types': - specifier: workspace:* - version: link:../types - devDependencies: - '@types/chrome': - specifier: ^0.0.268 - version: 0.0.268 - typescript: - specifier: ^5.7.0 - version: 5.9.3 - vite: - specifier: ^5.4.0 - version: 5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1) - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - packages/integration-tests: - dependencies: - '@jest/globals': - specifier: ^29.7.0 - version: 29.7.0 - '@shieldai/db': - specifier: workspace:* - version: link:../db - '@shieldai/shared-billing': - specifier: workspace:* - version: link:../shared-billing - '@shieldai/shared-notifications': - specifier: workspace:* - version: link:../shared-notifications - '@types/jest': - specifier: ^29.5.0 - version: 29.5.14 - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) - ts-jest: - specifier: ^29.1.0 - version: 29.4.9(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)))(typescript@5.9.3) - typescript: - specifier: ^5.0.0 - version: 5.9.3 - devDependencies: - '@types/node': - specifier: ^20.0.0 - version: 20.19.41 - ts-node: - specifier: ^10.9.0 - version: 10.9.2(@types/node@20.19.41)(typescript@5.9.3) - - packages/jobs: - dependencies: - '@shieldai/darkwatch': - specifier: workspace:* - version: link:../../services/darkwatch - '@shieldai/db': - specifier: workspace:* - version: link:../db - '@shieldai/report': - specifier: workspace:* - version: link:../report - '@shieldai/shared-notifications': - specifier: workspace:* - version: link:../shared-notifications - '@shieldai/types': - specifier: workspace:* - version: link:../types - bullmq: - specifier: ^5.24.0 - version: 5.76.8 - ioredis: - specifier: ^5.4.0 - version: 5.10.1 - devDependencies: - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - packages/mobile: - dependencies: - '@expo/vector-icons': - specifier: ^14.0.0 - version: 14.1.0(expo-font@12.0.10(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - '@react-native-async-storage/async-storage': - specifier: 1.23.1 - version: 1.23.1(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0)) - '@react-native-community/netinfo': - specifier: 11.4.1 - version: 11.4.1(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0)) - '@react-navigation/bottom-tabs': - specifier: ^6.5.0 - version: 6.6.1(@react-navigation/native@6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-screens@3.31.0(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - '@react-navigation/native': - specifier: ^6.1.0 - version: 6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - '@react-navigation/stack': - specifier: ^6.3.0 - version: 6.4.1(@react-navigation/native@6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-gesture-handler@2.16.2(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-screens@3.31.0(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - '@shieldai/mobile-api-client': - specifier: workspace:* - version: link:../mobile-api-client - expo: - specifier: ~51.0.0 - version: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - expo-av: - specifier: ~14.0.0 - version: 14.0.7(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-constants: - specifier: ~16.0.0 - version: 16.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-crypto: - specifier: ~13.0.0 - version: 13.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-device: - specifier: ~6.0.0 - version: 6.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-file-system: - specifier: ~17.0.0 - version: 17.0.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-image-picker: - specifier: ~15.0.0 - version: 15.0.7(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-linking: - specifier: ~6.3.0 - version: 6.3.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-local-authentication: - specifier: ~14.0.0 - version: 14.0.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-notifications: - specifier: ~0.28.0 - version: 0.28.19(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-secure-store: - specifier: ~13.0.0 - version: 13.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-status-bar: - specifier: ~1.12.0 - version: 1.12.1 - expo-updates: - specifier: ~0.18.0 - version: 0.18.19(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - react: - specifier: 18.2.0 - version: 18.2.0 - react-native: - specifier: 0.74.5 - version: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - react-native-gesture-handler: - specifier: ~2.16.0 - version: 2.16.2(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - react-native-reanimated: - specifier: ~3.10.0 - version: 3.10.1(@babel/core@7.29.0)(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - react-native-safe-area-context: - specifier: 4.10.5 - version: 4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - react-native-screens: - specifier: 3.31.0 - version: 3.31.0(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - react-native-svg: - specifier: 15.2.0 - version: 15.2.0(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - zustand: - specifier: ^4.4.0 - version: 4.5.7(@types/react@18.3.28)(react@18.2.0) - devDependencies: - '@babel/core': - specifier: ^7.24.0 - version: 7.29.0 - '@types/react': - specifier: ^18.2.0 - version: 18.3.28 - '@types/react-test-renderer': - specifier: ^18.0.0 - version: 18.3.1 - babel-preset-expo: - specifier: ^11.0.0 - version: 11.0.15(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0)) - eslint: - specifier: ^8.57.0 - version: 8.57.1 - eslint-plugin-react: - specifier: ^7.34.0 - version: 7.37.5(eslint@8.57.1) - eslint-plugin-react-native: - specifier: ^4.1.0 - version: 4.1.0(eslint@8.57.1) - jest: - specifier: ^29.7.0 - version: 29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)) - react-test-renderer: - specifier: 18.2.0 - version: 18.2.0(react@18.2.0) - typescript: - specifier: ^5.3.0 - version: 5.9.3 - - packages/mobile-api-client: - dependencies: - '@react-native-async-storage/async-storage': - specifier: 1.23.1 - version: 1.23.1(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)) - axios: - specifier: ^1.6.0 - version: 1.16.1 - expo: - specifier: '>=49.0.0' - version: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - expo-secure-store: - specifier: ^12.8.0 - version: 12.8.1(expo@55.0.24) - react-native: - specifier: '>=0.72.0' - version: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - devDependencies: - '@types/react': - specifier: ^18.2.0 - version: 18.3.28 - typescript: - specifier: ^5.3.0 - version: 5.9.3 - - packages/monitoring: - dependencies: - '@aws-sdk/client-cloudwatch': - specifier: ^3.500.0 - version: 3.1046.0 - '@sentry/node': - specifier: ^8.0.0 - version: 8.55.2 - dd-trace: - specifier: ^5.0.0 - version: 5.103.0(@openfeature/server-sdk@1.21.0(@openfeature/core@1.10.0)) - zod: - specifier: ^3.23.0 - version: 3.25.76 - devDependencies: - '@types/node': - specifier: ^25.6.0 - version: 25.8.0 - typescript: - specifier: ^5.7.0 - version: 5.9.3 - - packages/report: - dependencies: - '@shieldai/db': - specifier: workspace:* - version: link:../db - '@shieldai/types': - specifier: workspace:* - version: link:../types - handlebars: - specifier: ^4.7.8 - version: 4.7.9 - pdfkit: - specifier: ^0.15.0 - version: 0.15.2 - devDependencies: - '@types/handlebars': - specifier: ^4.1.0 - version: 4.1.0 - '@types/pdfkit': - specifier: ^0.13.3 - version: 0.13.9 - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - packages/shared-analytics: - dependencies: - '@segment/analytics-node': - specifier: ^1.0.0 - version: 1.3.0 - googleapis: - specifier: ^128.0.0 - version: 128.0.0 - zod: - specifier: ^4.3.6 - version: 4.4.3 - devDependencies: - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - packages/shared-auth: - dependencies: - next-auth: - specifier: ^4.24.0 - version: 4.24.14(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - zod: - specifier: ^4.3.6 - version: 4.4.3 - devDependencies: - typescript: - specifier: ^5.3.3 - version: 5.9.3 - - packages/shared-billing: - dependencies: - '@shieldai/shared-notifications': - specifier: workspace:* - version: link:../shared-notifications - '@shieldsai/shared-db': - specifier: workspace:* - version: link:../shared-db - express: - specifier: ^4.22.1 - version: 4.22.2 - stripe: - specifier: ^14.25.0 - version: 14.25.0 - zod: - specifier: ^3.25.76 - version: 3.25.76 - devDependencies: - '@types/express': - specifier: ^4.17.0 - version: 4.17.25 - typescript: - specifier: ^5.0.0 - version: 5.9.3 - - packages/shared-db: - dependencies: - '@prisma/client': - specifier: ^5.14.0 - version: 5.22.0(prisma@5.22.0) - zod: - specifier: ^4.3.6 - version: 4.4.3 - devDependencies: - prisma: - specifier: ^5.14.0 - version: 5.22.0 - typescript: - specifier: ^5.3.3 - version: 5.9.3 - - packages/shared-notifications: - dependencies: - express: - specifier: ^4.18.0 - version: 4.22.2 - firebase-admin: - specifier: ^12.0.0 - version: 12.7.0 - ioredis: - specifier: ^5.4.0 - version: 5.10.1 - resend: - specifier: ^3.0.0 - version: 3.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - twilio: - specifier: ^4.0.0 - version: 4.23.0 - zod: - specifier: ^3.22.0 - version: 3.25.76 - devDependencies: - '@types/express': - specifier: ^4.17.0 - version: 4.17.25 - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) - typescript: - specifier: ^5.0.0 - version: 5.9.3 - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - packages/shared-ui: - dependencies: - solid-js: - specifier: ^1.8.14 - version: 1.9.12 - devDependencies: - typescript: - specifier: ^5.3.3 - version: 5.9.3 - - packages/shared-utils: - devDependencies: - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - packages/types: {} - - packages/web: - dependencies: - '@shieldsai/shared-auth': - specifier: workspace:* - version: link:../shared-auth - '@shieldsai/shared-ui': - specifier: workspace:* - version: link:../shared-ui - '@shieldsai/shared-utils': - specifier: workspace:* - version: link:../shared-utils - '@solidjs/router': - specifier: ^0.14.0 - version: 0.14.10(solid-js@1.9.12) - solid-js: - specifier: ^1.8.14 - version: 1.9.12 - devDependencies: - '@types/node': - specifier: ^25.6.0 - version: 25.8.0 - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vite: - specifier: ^5.1.4 - version: 5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1) - vite-plugin-solid: - specifier: ^2.8.2 - version: 2.11.12(solid-js@1.9.12)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - redux: - dependencies: - '@libsql/client': - specifier: ^0.17.3 - version: 0.17.3 '@solidjs/meta': specifier: ^0.29.4 - version: 0.29.4(solid-js@1.9.12) + version: 0.29.4(solid-js@1.9.13) '@solidjs/router': specifier: ^0.15.0 - version: 0.15.4(solid-js@1.9.12) + version: 0.15.4(solid-js@1.9.13) '@solidjs/start': specifier: 2.0.0-alpha.2 - version: 2.0.0-alpha.2(crossws@0.3.5)(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0)) + version: 2.0.0-alpha.2(crossws@0.3.5)(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) '@solidjs/vite-plugin-nitro-2': specifier: ^0.1.0 - version: 0.1.0(@libsql/client@0.17.3)(drizzle-orm@0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3)))(oxc-parser@0.129.0)(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0))(xml2js@0.6.0) + version: 0.1.0(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + '@tailwindcss/vite': + specifier: ^4.0.0 + version: 4.3.0(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) '@trpc/client': specifier: ^10.45.2 version: 10.45.4(@trpc/server@10.45.4) @@ -600,1080 +50,161 @@ importers: '@typeschema/valibot': specifier: ^0.13.4 version: 0.13.5(valibot@0.29.0) - drizzle-orm: - specifier: ^0.45.2 - version: 0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3)) solid-js: specifier: ^1.9.5 - version: 1.9.12 + version: 1.9.13 + tailwindcss: + specifier: ^4.0.0 + version: 4.3.0 valibot: specifier: ^0.29.0 version: 0.29.0 vite: specifier: ^7.0.0 - version: 7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0) + version: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) devDependencies: - drizzle-kit: - specifier: ^0.31.10 - version: 0.31.10 - - services/darkwatch: - dependencies: - '@shieldai/correlation': - specifier: workspace:* - version: link:../../packages/correlation - '@shieldai/db': - specifier: workspace:* - version: link:../../packages/db - '@shieldai/types': - specifier: workspace:* - version: link:../../packages/types - node-cache: - specifier: ^5.1.2 - version: 5.1.2 - devDependencies: - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) vitest: specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - services/hometitle: - dependencies: - '@shieldai/correlation': - specifier: workspace:* - version: link:../../packages/correlation - '@shieldai/db': - specifier: workspace:* - version: link:../../packages/db - '@shieldai/shared-notifications': - specifier: workspace:* - version: link:../../packages/shared-notifications - '@shieldai/types': - specifier: workspace:* - version: link:../../packages/types - cache-manager: - specifier: ^6.4.2 - version: 6.4.3 - cacheable: - specifier: ^1.10.0 - version: 1.10.4 - rate-limiter-flexible: - specifier: ^5.0.5 - version: 5.0.5 - uuid: - specifier: ^11.1.0 - version: 11.1.1 - devDependencies: - '@types/uuid': - specifier: ^10.0.0 - version: 10.0.0 - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - services/removebrokers: - dependencies: - '@shieldai/correlation': - specifier: workspace:* - version: link:../../packages/correlation - '@shieldai/db': - specifier: workspace:* - version: link:../../packages/db - '@shieldai/shared-notifications': - specifier: workspace:* - version: link:../../packages/shared-notifications - '@shieldai/types': - specifier: workspace:* - version: link:../../packages/types - node-cache: - specifier: ^5.1.2 - version: 5.1.2 - devDependencies: - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - services/spamshield: - dependencies: - '@prisma/client': - specifier: ^6.2.0 - version: 6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3) - '@shieldai/correlation': - specifier: workspace:* - version: link:../../packages/correlation - '@shieldai/db': - specifier: workspace:* - version: link:../../packages/db - '@shieldai/types': - specifier: workspace:* - version: link:../../packages/types - '@shieldsai/shared-analytics': - specifier: workspace:* - version: link:../../packages/shared-analytics - libphonenumber-js: - specifier: ^1.10.50 - version: 1.13.1 - ws: - specifier: ^8.16.0 - version: 8.20.1 - devDependencies: - '@types/ws': - specifier: ^8.5.10 - version: 8.18.1 - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) - eslint: - specifier: ^8.56.0 - version: 8.57.1 - tsx: - specifier: ^4.19.0 - version: 4.22.0 - typescript: - specifier: ^5.3.3 - version: 5.9.3 - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - - services/voiceprint: - dependencies: - '@shieldai/correlation': - specifier: workspace:* - version: link:../../packages/correlation - '@shieldai/db': - specifier: workspace:* - version: link:../../packages/db - '@shieldai/types': - specifier: workspace:* - version: link:../../packages/types - '@types/uuid': - specifier: ^11.0.0 - version: 11.0.0 - node-cache: - specifier: ^5.1.2 - version: 5.1.2 - uuid: - specifier: ^14.0.0 - version: 14.0.0 - devDependencies: - '@vitest/coverage-v8': - specifier: ^4.1.5 - version: 4.1.6(vitest@4.1.6) - vitest: - specifier: ^4.1.5 - version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) + version: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) packages: - '@aws-crypto/crc32@5.2.0': - resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/sha256-browser@5.2.0': - resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - - '@aws-crypto/sha256-js@5.2.0': - resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/supports-web-crypto@5.2.0': - resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - - '@aws-crypto/util@5.2.0': - resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - - '@aws-sdk/client-cloudwatch@3.1046.0': - resolution: {integrity: sha512-dvuP0O+s+oFv9Q5jJZvoijVY/zKoI4ymPc22PUV9wny3qbRZJrwTI9HM/RdntCqmOz4FpqRbJBLMiRlNwMna9A==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/core@3.974.9': - resolution: {integrity: sha512-bXxosFunr+v/kqNb99r1NRkrVBha7CG036fRSpWGbC1A/e363XFQN6wcZMx7MYTdRr1tYwNnkrWX2xc1rT3BCQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-env@3.972.35': - resolution: {integrity: sha512-WkFQ8BedszVomhh/Zzs8WwnE/XBmTqZjoQVB8u/4zH6kZCjouXZpPpb93gD8m0EZmzAl7dxHE/y+yDpuKzNCMw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-http@3.972.37': - resolution: {integrity: sha512-ylx0ZJTU+2eNcvXQ69VNR3TVSYa/ibpvdK717/NxqR9aXRMn2QRWZaiI8aa5yY/fOWZ5mknSmxGaVxxtdwv3EA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-ini@3.972.39': - resolution: {integrity: sha512-QhRSrdkk+Gq0AFIylpiI0N6lcJqFYV9Jtr4Luz5FpYOYbjJSfyTG6iLhnK/UPIgN1Jnon8WAmSC//16XYGvwkA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-login@3.972.39': - resolution: {integrity: sha512-1hU0NtC04QbFIuoBuF4aQ2A97GsSE5/A0ZJpDijwexsBREIQ4KPRYl3v/FfKCPBYsaTeGjkOFx5nLhWHY24LOw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-node@3.972.40': - resolution: {integrity: sha512-ZgrQaGkpyTlVSCCsffzijVg+KgftTAWYvI5Otc36J/4jNiHb+7MmBiJIR0a5AHLvifC92PiYHt5pijP0dswd1w==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-process@3.972.35': - resolution: {integrity: sha512-hNj1rAwZWT1vfz54BwH8FUWxZuqStrM25Q5LEIwn2erHPMRVAjLlpZqEbCEEqS99eEEOhdeetnS0WeNa3iYeEQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-sso@3.972.39': - resolution: {integrity: sha512-mwIPNPldyCZkvHozb6E0X/vuQLN1UCjcA6MwUf1gaO7EwghCmuNZXatq0L3zptKFvPC4Nds7+WFUkifI1XmbSw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-web-identity@3.972.39': - resolution: {integrity: sha512-b9HT8CnpyPVn1hU14Q7ihjwSPlRzToYmRYJxRd5jNHEZ43lrIhoLaTT8JmfQQt5j5M8rTX1iN1X8mvu0SM1dXA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-host-header@3.972.11': - resolution: {integrity: sha512-CBC6+tVYaOJo7QXgN1zJ4Ba2f3/Cpy4eRViYFimXW/O5Mn8hBmgXXzHu4vy4ubT80YWnp8aCFygr7dTOa14yQg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-logger@3.972.10': - resolution: {integrity: sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-recursion-detection@3.972.12': - resolution: {integrity: sha512-5eltYxKB4MfdQv7/VhWxRbAVQKow5dz9votRFigTYrWJHMQXwLMltlbk7KFWSZh5NDBySfmjT7Jv/DWfYCmDng==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-user-agent@3.972.39': - resolution: {integrity: sha512-MlNSvNsSVlMKKWaCzA0GP1nS4Cuq3WCXUN1vmMvd+Ctztib5kmRcpmTtKx9kikN8szAc+gcdp7uqJJervV2nQg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/nested-clients@3.997.7': - resolution: {integrity: sha512-jT2AXOODobQfTYGC2SChMSnZ/voIcRV/LHlY1suyhY1bdgP/voKkhEg8Ci1jiGQ4lBiaso5BEAV3ZWWpPTfmYA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/region-config-resolver@3.972.14': - resolution: {integrity: sha512-VuLXVmm7+lKVxqFcOItPkXhjbJ02iUfxkxheRu41SfWf6/xrZup2A2SwHZos/LeQGu3SBHeqTQht80Uo3ienPA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/signature-v4-multi-region@3.996.26': - resolution: {integrity: sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1046.0': - resolution: {integrity: sha512-9je8nZt+ntB8IjhpGNayU/AkBgvq/f4aFO2bH1LSNC5JX6K8zY4LUnr/ymqunePrwq+B5OVBpL7ILjYzMFSZAw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/types@3.973.8': - resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-endpoints@3.996.9': - resolution: {integrity: sha512-ibx8Vd73rCTHekNGeXX8cpGWoBKbNAlwKHL3yjSxxttu5QnNDaSAM7/0MFYDjU31/F4lyrPoQcGirT0ew61xcg==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-locate-window@3.965.5': - resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-user-agent-browser@3.972.11': - resolution: {integrity: sha512-kq3RS6XQtHMrLFShbkem6h+8fxazB3jEIsbMC6aaSInOciRGE+eGAqTgJ+obO7Euo/pjM8thVqLiLISEH9X9DA==} - - '@aws-sdk/util-user-agent-node@3.973.25': - resolution: {integrity: sha512-066hKH/0nvV7x4ofV/iK9kz8r/qNfcR6rzuEOFqI2vQL/fcTTsDAbTw0jmXkyMzANK8ltQdALj19ns3zuOJiUw==} - engines: {node: '>=20.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true - - '@aws-sdk/xml-builder@3.972.23': - resolution: {integrity: sha512-A0YmgYFv+hTI9c17Ntvd2hSehm9bmJfkb+ggADBwVKA8H/3+Jx94SzR2qOB9bAA9WFeDqnfz9PKKQ+D+YAKomA==} - engines: {node: '>=20.0.0'} - - '@aws/lambda-invoke-store@0.2.4': - resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} - engines: {node: '>=18.0.0'} - - '@babel/code-frame@7.10.4': - resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} - '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.29.3': - resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.2.0': - resolution: {integrity: sha512-BA75MVfRlFQG2EZgFYIwyT1r6xSkwfP2bdkY/kLZusEYWiJs4xCowab/alaEaT0wSvmVuXGqiefeBlP+7V1yKg==} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} engines: {node: '>=6.9.0'} - '@babel/helper-annotate-as-pure@7.27.3': - resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} - '@babel/helper-create-class-features-plugin@7.29.3': - resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==} + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-create-regexp-features-plugin@7.28.5': - resolution: {integrity: sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-define-polyfill-provider@0.6.8': - resolution: {integrity: sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - '@babel/helper-environment-visitor@7.24.7': - resolution: {integrity: sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==} + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-member-expression-to-functions@7.28.5': - resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.18.6': resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + '@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-optimise-call-expression@7.27.1': - resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} engines: {node: '>=6.9.0'} - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} engines: {node: '>=6.9.0'} - '@babel/helper-remap-async-to-generator@7.27.1': - resolution: {integrity: sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==} + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-replace-supers@7.28.6': - resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': - resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} - '@babel/helper-wrap-function@7.28.6': - resolution: {integrity: sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==} + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.29.2': - resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} - engines: {node: '>=6.9.0'} - - '@babel/highlight@7.25.9': - resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.3': - resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5': - resolution: {integrity: sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1': - resolution: {integrity: sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1': - resolution: {integrity: sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3': - resolution: {integrity: sha512-SRS46DFR4HqzUzCVgi90/xMoL+zeBDBvWdKYXSEzh79kXswNFEglUpMKxR04//dPqwYXWUBJ3mpUd933ru9Kmg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1': - resolution: {integrity: sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.13.0 - - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6': - resolution: {integrity: sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-proposal-async-generator-functions@7.20.7': - resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-async-generator-functions instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-class-properties@7.18.6': - resolution: {integrity: sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-class-properties instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-decorators@7.29.0': - resolution: {integrity: sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-export-default-from@7.27.1': - resolution: {integrity: sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-logical-assignment-operators@7.20.7': - resolution: {integrity: sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-logical-assignment-operators instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6': - resolution: {integrity: sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-nullish-coalescing-operator instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-numeric-separator@7.18.6': - resolution: {integrity: sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-numeric-separator instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-object-rest-spread@7.20.7': - resolution: {integrity: sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-object-rest-spread instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-optional-catch-binding@7.18.6': - resolution: {integrity: sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-catch-binding instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-optional-chaining@7.21.0': - resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} - engines: {node: '>=6.9.0'} - deprecated: This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-optional-chaining instead. - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2': - resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-async-generators@7.8.4': - resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-bigint@7.8.3': - resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-properties@7.12.13': - resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-class-static-block@7.14.5': - resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-decorators@7.28.6': - resolution: {integrity: sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==} + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-dynamic-import@7.8.3': - resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-export-default-from@7.28.6': - resolution: {integrity: sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==} + '@babel/plugin-transform-modules-commonjs@7.29.7': + resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-flow@7.28.6': - resolution: {integrity: sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==} + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-assertions@7.28.6': - resolution: {integrity: sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==} + '@babel/preset-typescript@7.29.7': + resolution: {integrity: sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-import-attributes@7.28.6': - resolution: {integrity: sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==} + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-import-meta@7.10.4': - resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-json-strings@7.8.3': - resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-syntax-jsx@7.28.6': - resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4': - resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': - resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-numeric-separator@7.10.4': - resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-object-rest-spread@7.8.3': - resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3': - resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-optional-chaining@7.8.3': - resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-private-property-in-object@7.14.5': - resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-top-level-await@7.14.5': - resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.28.6': - resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-unicode-sets-regex@7.18.6': - resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-arrow-functions@7.27.1': - resolution: {integrity: sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-async-generator-functions@7.29.0': - resolution: {integrity: sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-async-to-generator@7.28.6': - resolution: {integrity: sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoped-functions@7.27.1': - resolution: {integrity: sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-block-scoping@7.28.6': - resolution: {integrity: sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-class-properties@7.28.6': - resolution: {integrity: sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-class-static-block@7.28.6': - resolution: {integrity: sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.12.0 - - '@babel/plugin-transform-classes@7.28.6': - resolution: {integrity: sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-computed-properties@7.28.6': - resolution: {integrity: sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-destructuring@7.28.5': - resolution: {integrity: sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-dotall-regex@7.28.6': - resolution: {integrity: sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-duplicate-keys@7.27.1': - resolution: {integrity: sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0': - resolution: {integrity: sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-dynamic-import@7.27.1': - resolution: {integrity: sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-explicit-resource-management@7.28.6': - resolution: {integrity: sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-exponentiation-operator@7.28.6': - resolution: {integrity: sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-export-namespace-from@7.27.1': - resolution: {integrity: sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-flow-strip-types@7.27.1': - resolution: {integrity: sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-for-of@7.27.1': - resolution: {integrity: sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-function-name@7.27.1': - resolution: {integrity: sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==} + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-json-strings@7.28.6': - resolution: {integrity: sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-literals@7.27.1': - resolution: {integrity: sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-logical-assignment-operators@7.28.6': - resolution: {integrity: sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-member-expression-literals@7.27.1': - resolution: {integrity: sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-amd@7.27.1': - resolution: {integrity: sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-commonjs@7.28.6': - resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-systemjs@7.29.4': - resolution: {integrity: sha512-N7QmZ0xRZfjHOfZeQLJjwgX2zS9pdGHSVl/cjSGlo4dXMqvurfxXDMKY4RqEKzPozV78VMcd0lxyG13mlbKc4w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-umd@7.27.1': - resolution: {integrity: sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-named-capturing-groups-regex@7.29.0': - resolution: {integrity: sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-new-target@7.27.1': - resolution: {integrity: sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6': - resolution: {integrity: sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-numeric-separator@7.28.6': - resolution: {integrity: sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-object-rest-spread@7.28.6': - resolution: {integrity: sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-object-super@7.27.1': - resolution: {integrity: sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-optional-catch-binding@7.28.6': - resolution: {integrity: sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-optional-chaining@7.28.6': - resolution: {integrity: sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-parameters@7.27.7': - resolution: {integrity: sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-private-methods@7.28.6': - resolution: {integrity: sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-private-property-in-object@7.28.6': - resolution: {integrity: sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-property-literals@7.27.1': - resolution: {integrity: sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-display-name@7.28.0': - resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-development@7.27.1': - resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-self@7.27.1': - resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx-source@7.27.1': - resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-jsx@7.28.6': - resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-react-pure-annotations@7.27.1': - resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-regenerator@7.29.0': - resolution: {integrity: sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-regexp-modifiers@7.28.6': - resolution: {integrity: sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/plugin-transform-reserved-words@7.27.1': - resolution: {integrity: sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-runtime@7.29.0': - resolution: {integrity: sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-shorthand-properties@7.27.1': - resolution: {integrity: sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-spread@7.28.6': - resolution: {integrity: sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-sticky-regex@7.27.1': - resolution: {integrity: sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-template-literals@7.27.1': - resolution: {integrity: sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typeof-symbol@7.27.1': - resolution: {integrity: sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.28.6': - resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-escapes@7.27.1': - resolution: {integrity: sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-property-regex@7.28.6': - resolution: {integrity: sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-regex@7.27.1': - resolution: {integrity: sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-unicode-sets-regex@7.28.6': - resolution: {integrity: sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/preset-env@7.29.5': - resolution: {integrity: sha512-/69t2aEzGKHD76DyLbHysF/QH2LJOB8iFnYO37unDTKBTubzcMRv0f3H5EiN1Q6ajOd/eB7dAInF0qdFVS06kA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-flow@7.27.1': - resolution: {integrity: sha512-ez3a2it5Fn6P54W8QkbfIyyIbxlXvcxyWHHvno1Wg0Ej5eiJY5hBb8ExttoIOJJk7V2dZE6prP7iby5q2aQ0Lg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-modules@0.1.6-no-external-plugins': - resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} - peerDependencies: - '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 - - '@babel/preset-react@7.28.5': - resolution: {integrity: sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-typescript@7.28.5': - resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/register@7.29.3': - resolution: {integrity: sha512-F6C1KpIdoImKQfsD6HSxZ+mS4YY/2Q+JsqrmTC5ApVkTR2rG+nnbpjhWwzA5bDNu8mJjB3AryqDaWFLd4gCbJQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/runtime@7.29.2': - resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} - engines: {node: '>=6.9.0'} - - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@bcoe/v8-coverage@0.2.3': - resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} @@ -1683,71 +214,6 @@ packages: resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} engines: {node: '>=18.0.0'} - '@cspotcode/source-map-support@0.8.1': - resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} - engines: {node: '>=12'} - - '@datadog/flagging-core@0.3.3': - resolution: {integrity: sha512-LnkTXMVxaCDGCOF2I+CCACndpbi4E8CP8NIsb1IbMmmATzkQHmYiL1ntFcS4mt5kNGAWXNrKquM02jhoiVc+dA==} - - '@datadog/libdatadog@0.9.3': - resolution: {integrity: sha512-L+scIlcRRRF0qjeSU3VQLQlqezfQHkDdnOdbmx/gLjPqewKSyqVGp7XRdKXYo2vZTzmG8dH6rPKXwgI68UQufw==} - - '@datadog/native-appsec@11.0.1': - resolution: {integrity: sha512-Y/XfknUmmJcw4hhQVhqzgdQvfjy+EGmXuUBgtVkI1r+/qS00egYu+wD/x7pOvjdbZNqN96znVszAnXvDQAzMDQ==} - engines: {node: '>=16'} - - '@datadog/native-iast-taint-tracking@4.1.0': - resolution: {integrity: sha512-g9K9Ddx1YQfrQIC2hgtfnYUGuzAFvSvhvt2lPZOAWBPo+bkYoW5KEkMHoY5XykCigTfXBYcQicRV0xB22AMkHw==} - - '@datadog/native-metrics@3.1.2': - resolution: {integrity: sha512-7AEWt0ZLr/ogR/9if1DmFBDTg3y67xM+gdhXUXKs+UQMxK0lnjrOHgN7fkpEmUG1uL+EkX2BDE3ENDlQ23J7OQ==} - engines: {node: '>=16'} - - '@datadog/openfeature-node-server@1.1.2': - resolution: {integrity: sha512-fr+zCaKoCSdizX22H7eA9Z3QaZrOMu5qU0yK0M2aWtUxokSAKPlLz3+9HdmqwBWU/ikqI+lbiAK0oNdvmUKeQA==} - engines: {node: '>=18.0.0'} - peerDependencies: - '@openfeature/server-sdk': '>=1.15.1' - - '@datadog/pprof@5.14.1': - resolution: {integrity: sha512-MlODCE9Gltmx7WChOg1BkIm7W2iE4CDW7K72BMwgzCvxFkG9rFWVsuA7/NEZL1nDvJ0qDe2du7DZZdZHTjcVPw==} - engines: {node: '>=16'} - - '@datadog/wasm-js-rewriter@5.0.1': - resolution: {integrity: sha512-EzbV3Lrdt3udQEsbDOVC5gB1y7yxfpBbrSIk4jaEsGjyj0Dbv2HGj7tZjs+qXzIzNonHc8h5El2bYZOGfC2wwg==} - engines: {node: '>= 10'} - - '@drizzle-team/brocli@0.10.2': - resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} - - '@egjs/hammerjs@2.0.17': - resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} - engines: {node: '>=0.8.0'} - - '@emnapi/core@1.10.0': - resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - - '@emnapi/runtime@1.10.0': - resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@esbuild-kit/core-utils@3.3.2': - resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild-kit/esm-loader@2.6.5': - resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==} - deprecated: 'Merged into tsx: https://tsx.is' - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - '@esbuild/aix-ppc64@0.25.12': resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} @@ -1766,18 +232,6 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.18.20': - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - '@esbuild/android-arm64@0.25.12': resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} @@ -1796,18 +250,6 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm@0.18.20': - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - '@esbuild/android-arm@0.25.12': resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} @@ -1826,18 +268,6 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-x64@0.18.20': - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - '@esbuild/android-x64@0.25.12': resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} @@ -1856,18 +286,6 @@ packages: cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.18.20': - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - '@esbuild/darwin-arm64@0.25.12': resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} @@ -1886,18 +304,6 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.18.20': - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - '@esbuild/darwin-x64@0.25.12': resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} @@ -1916,18 +322,6 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.18.20': - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - '@esbuild/freebsd-arm64@0.25.12': resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} @@ -1946,18 +340,6 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.18.20': - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - '@esbuild/freebsd-x64@0.25.12': resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} @@ -1976,18 +358,6 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.18.20': - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - '@esbuild/linux-arm64@0.25.12': resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} @@ -2006,18 +376,6 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.18.20': - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - '@esbuild/linux-arm@0.25.12': resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} @@ -2036,18 +394,6 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.18.20': - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - '@esbuild/linux-ia32@0.25.12': resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} @@ -2066,18 +412,6 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.18.20': - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - '@esbuild/linux-loong64@0.25.12': resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} @@ -2096,18 +430,6 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.18.20': - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - '@esbuild/linux-mips64el@0.25.12': resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} @@ -2126,18 +448,6 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.18.20': - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - '@esbuild/linux-ppc64@0.25.12': resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} @@ -2156,18 +466,6 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.18.20': - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - '@esbuild/linux-riscv64@0.25.12': resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} @@ -2186,18 +484,6 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.18.20': - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - '@esbuild/linux-s390x@0.25.12': resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} @@ -2216,18 +502,6 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.18.20': - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - '@esbuild/linux-x64@0.25.12': resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} @@ -2264,18 +538,6 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.18.20': - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - '@esbuild/netbsd-x64@0.25.12': resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} @@ -2312,18 +574,6 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.18.20': - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - '@esbuild/openbsd-x64@0.25.12': resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} @@ -2360,18 +610,6 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.18.20': - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - '@esbuild/sunos-x64@0.25.12': resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} @@ -2390,18 +628,6 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.18.20': - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - '@esbuild/win32-arm64@0.25.12': resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} @@ -2420,18 +646,6 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.18.20': - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - '@esbuild/win32-ia32@0.25.12': resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} @@ -2450,18 +664,6 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.18.20': - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - '@esbuild/win32-x64@0.25.12': resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} @@ -2480,546 +682,6 @@ packages: cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.9.1': - resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/eslintrc@2.1.4': - resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@eslint/js@8.57.1': - resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - '@expo/bunyan@4.0.1': - resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {node: '>=0.10.0'} - - '@expo/cli@0.18.31': - resolution: {integrity: sha512-v9llw9fT3Uv+TCM6Xllo54t672CuYtinEQZ2LPJ2EJsCwuTc4Cd2gXQaouuIVD21VoeGQnr5JtJuWbF97sBKzQ==} - hasBin: true - - '@expo/cli@55.0.30': - resolution: {integrity: sha512-luWcCgompncWtCi1HqQfY32MVOuD0kUeARpr1Le1LeKVtZykjOwnz7YWXZo5zjISiD7L/gQnBNGVrRjvREsJqg==} - hasBin: true - peerDependencies: - expo: '*' - expo-router: '*' - react-native: '*' - peerDependenciesMeta: - expo-router: - optional: true - react-native: - optional: true - - '@expo/code-signing-certificates@0.0.5': - resolution: {integrity: sha512-BNhXkY1bblxKZpltzAx98G2Egj9g1Q+JRcvR7E99DOj862FTCX+ZPsAUtPTr7aHxwtrL7+fL3r0JSmM9kBm+Bw==} - - '@expo/code-signing-certificates@0.0.6': - resolution: {integrity: sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==} - - '@expo/config-plugins@55.0.9': - resolution: {integrity: sha512-jLfpxru8dTo7eU0cqeTWuQav7byyjb37eF/mbXl1/3eTBHBvFU1VGxpeKxanUdTQAAjqzH8KGgWb0fWcce+z1w==} - - '@expo/config-plugins@7.2.5': - resolution: {integrity: sha512-w+5ccu1IxBHgyQk9CPFKLZOk8yZQEyTjbJwOzESK1eR7QwosbcsLkN1c1WWUZYiCXwORu3UTwJYll4+X2xxJhQ==} - - '@expo/config-plugins@8.0.11': - resolution: {integrity: sha512-oALE1HwnLFthrobAcC9ocnR9KXLzfWEjgIe4CPe+rDsfC6GDs8dGYCXfRFoCEzoLN4TGYs9RdZ8r0KoCcNrm2A==} - - '@expo/config-types@49.0.0': - resolution: {integrity: sha512-8eyREVi+K2acnMBe/rTIu1dOfyR2+AMnTLHlut+YpMV9OZPdeKV0Bs9BxAewGqBA2slslbQ9N39IS2CuTKpXkA==} - - '@expo/config-types@51.0.3': - resolution: {integrity: sha512-hMfuq++b8VySb+m9uNNrlpbvGxYc8OcFCUX9yTmi9tlx6A4k8SDabWFBgmnr4ao3wEArvWrtUQIfQCVtPRdpKA==} - - '@expo/config-types@55.0.5': - resolution: {integrity: sha512-sCmSUZG4mZ/ySXvfyyBdhjivz8Q539X1NondwDdYG7s3SBsk+wsgPJzYsqgAG/P9+l0xWjUD2F+kQ1cAJ6NNLg==} - - '@expo/config@55.0.17': - resolution: {integrity: sha512-Y3VaRg7Jllg3MhlUOTQqHm6/dttsqcjYlnS9enhAllZvPUpTHnRA4YPETtUZlxkdMJy6y3UZe986pd/KfJ6OTg==} - - '@expo/config@8.1.2': - resolution: {integrity: sha512-4e7hzPj50mQIlsrzOH6XZ36O094mPfPTIDIH4yv49bWNMc7GFLTofB/lcT+QyxiLaJuC0Wlk9yOLB8DIqmtwug==} - - '@expo/config@9.0.4': - resolution: {integrity: sha512-g5ns5u1JSKudHYhjo1zaSfkJ/iZIcWmUmIQptMJZ6ag1C0ShL2sj8qdfU8MmAMuKLOgcIfSaiWlQnm4X3VJVkg==} - - '@expo/devcert@1.2.1': - resolution: {integrity: sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==} - - '@expo/devtools@55.0.3': - resolution: {integrity: sha512-KoIDgo0NoXeWLsIcOdZqtAG/1LlsM+JL0DA3bo0vCYaOYTBLXi/ZvRBqa20Ub8D2vKLNa+FgRQW0gRg04Ps1Pg==} - peerDependencies: - react: '*' - react-native: '*' - peerDependenciesMeta: - react: - optional: true - react-native: - optional: true - - '@expo/dom-webview@55.0.6': - resolution: {integrity: sha512-ZNm8tiNEZysxrr36J0x4mOCGyJDcaIvL/3tMxBz0VJIJDcV19xjuJAhJQxHovu+jKx6s9tRyEAINa1mdrzV39g==} - peerDependencies: - expo: '*' - react: '*' - react-native: '*' - - '@expo/env@0.3.0': - resolution: {integrity: sha512-OtB9XVHWaXidLbHvrVDeeXa09yvTl3+IQN884sO6PhIi2/StXfgSH/9zC7IvzrDB8kW3EBJ1PPLuCUJ2hxAT7Q==} - - '@expo/env@2.1.2': - resolution: {integrity: sha512-RJtGFfj/ygO/6zcVbV3cckHf4THcEkv5IZft1GjCB3dfT6axvzvIwXE9EiQqQYmGHcQ+ZrvC8xZcIhiHba0pYg==} - engines: {node: '>=20.12.0'} - - '@expo/fingerprint@0.16.7': - resolution: {integrity: sha512-BH8sicYOqZ1iBMwCVEGIz6uTTfylosjc49FoMmCYIzKOiYdiVehsfoYBwyfxwWIiya1VMhm1gv0cgOP8fxHpDw==} - hasBin: true - - '@expo/image-utils@0.5.1': - resolution: {integrity: sha512-U/GsFfFox88lXULmFJ9Shfl2aQGcwoKPF7fawSCLixIKtMCpsI+1r0h+5i0nQnmt9tHuzXZDL8+Dg1z6OhkI9A==} - - '@expo/image-utils@0.8.14': - resolution: {integrity: sha512-5Sn+jG4Cw+shC2wDMXoqSAJnvERbiwzHn05FpWtD5IBflfTIs5gUmjzwiGVyjOdlMSQhgRrw/AymPbmO9h9mpQ==} - - '@expo/json-file@10.0.14': - resolution: {integrity: sha512-yWwBFywFv+SxkJp/pIzzA416JVYflNUh7pqQzgaA6nXDqRyK7KfrqVzk8PdUfDnqbBcaZZxpzNssfQZzp5KHrA==} - - '@expo/json-file@8.2.37': - resolution: {integrity: sha512-YaH6rVg11JoTS2P6LsW7ybS2CULjf40AbnAHw2F1eDPuheprNjARZMnyHFPkKv7GuxCy+B9GPcbOKgc4cgA80Q==} - - '@expo/json-file@8.3.3': - resolution: {integrity: sha512-eZ5dld9AD0PrVRiIWpRkm5aIoWBw3kAyd8VkuWEy92sEthBKDDDHAnK2a0dw0Eil6j7rK7lS/Qaq/Zzngv2h5A==} - - '@expo/local-build-cache-provider@55.0.13': - resolution: {integrity: sha512-Vg5BE10UL+0yg3BVtIeiSoeHU31Qe1m3UxhBPS478ACY1zzKuxZE30x2sym/B2OIWypjmPzXDRt8J9TOGFuFNw==} - - '@expo/log-box@55.0.12': - resolution: {integrity: sha512-f9ARS8J60cq3LLNdIqmUjYwyerBzVS5Ecp7KjIf3GOIPjW0571rkcwLz4/U18l/1DeSkSzIkYsNl2TC9oTdWaQ==} - peerDependencies: - '@expo/dom-webview': ^55.0.6 - expo: '*' - react: '*' - react-native: '*' - - '@expo/metro-config@0.18.11': - resolution: {integrity: sha512-/uOq55VbSf9yMbUO1BudkUM2SsGW1c5hr9BnhIqYqcsFv0Jp5D3DtJ4rljDKaUeNLbwr6m7pqIrkSMq5NrYf4Q==} - - '@expo/metro-config@55.0.21': - resolution: {integrity: sha512-pJ8G0uCxqA9KK+XCzXZF7ZI37rduD2l7Cun2e3rVAgB2yeOZagUD+VBvooU9QPiWx9e/7EbimH5/JP81JyhQlg==} - peerDependencies: - expo: '*' - peerDependenciesMeta: - expo: - optional: true - - '@expo/metro@55.1.1': - resolution: {integrity: sha512-/wfXo5hTuAVpVLG/4hzlmD9NBGJkzkmBEMm/4VICajYRbj7y8OmqqPWbbymzHiBiHB6tI9BnsyXpQM6zVZEECg==} - - '@expo/osascript@2.4.3': - resolution: {integrity: sha512-wbuj3EebM7W9hN/Wp4xTzKd6rQ2zKJzAxkFxkOOwyysLp0HOAgQ4/5RINyoS241pZUX2rUHq7mAJ7pcCQ8U0Ow==} - engines: {node: '>=12'} - - '@expo/package-manager@1.10.5': - resolution: {integrity: sha512-nCP9Mebfl3jvOr0/P6VAuyah6PAtun+aihIL2zAtuE8uSe94JWkVZ7051i0MUVO+y3gFpBqnr8IIH5ch+VJjHA==} - - '@expo/plist@0.0.20': - resolution: {integrity: sha512-UXQ4LXCfTZ580LDHGJ5q62jSTwJFFJ1GqBu8duQMThiHKWbMJ+gajJh6rsB6EJ3aLUr9wcauxneL5LVRFxwBEA==} - - '@expo/plist@0.1.3': - resolution: {integrity: sha512-GW/7hVlAylYg1tUrEASclw1MMk9FP4ZwyFAY/SUTJIhPDQHtfOlXREyWV3hhrHdX/K+pS73GNgdfT6E/e+kBbg==} - - '@expo/plist@0.5.3': - resolution: {integrity: sha512-jz5oPcPDd3fygwVxwSwmO6wodTwm0Qa14NUyPy0ka7H8sFmCtNZUI2+DzVe/EXjOhq1FbEjrwl89gdlWYOnVjQ==} - - '@expo/prebuild-config@55.0.18': - resolution: {integrity: sha512-2oKXyy5pyM87DJqXW5Z+Sakle6rApFFtpPhWOiNsOdoh6rOAD+EqVgyrs2OEEic8CE0tTt27w3SRfSZe/PZrxg==} - peerDependencies: - expo: '*' - - '@expo/prebuild-config@7.0.9': - resolution: {integrity: sha512-9i6Cg7jInpnGEHN0jxnW0P+0BexnePiBzmbUvzSbRXpdXihYUX2AKMu73jgzxn5P1hXOSkzNS7umaY+BZ+aBag==} - peerDependencies: - expo-modules-autolinking: '>=0.8.1' - - '@expo/require-utils@55.0.5': - resolution: {integrity: sha512-U4K/CQ2VpXuwfNGsN+daKmYOt15hCP8v/pXaYH6eut7kdYZo6SfJ1yr67BIcJ+1Gzzs+QzTxswAZChKpXmceyw==} - peerDependencies: - typescript: ^5.0.0 || ^5.0.0-0 - peerDependenciesMeta: - typescript: - optional: true - - '@expo/router-server@55.0.16': - resolution: {integrity: sha512-LvAdrm039nQBG+95+ff5Rc4CsBuoc/giDhjQrgxB9lKJqC/ZTq1xbwfEZFNq6yokX6fOCs/vlxdhmSkOjMIrvg==} - peerDependencies: - '@expo/metro-runtime': ^55.0.11 - expo: '*' - expo-constants: ^55.0.16 - expo-font: ^55.0.7 - expo-router: '*' - expo-server: ^55.0.9 - react: '*' - react-dom: '*' - react-server-dom-webpack: ~19.0.1 || ~19.1.2 || ~19.2.1 - peerDependenciesMeta: - '@expo/metro-runtime': - optional: true - expo-router: - optional: true - react-dom: - optional: true - react-server-dom-webpack: - optional: true - - '@expo/rudder-sdk-node@1.1.1': - resolution: {integrity: sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ==} - engines: {node: '>=12'} - - '@expo/schema-utils@55.0.4': - resolution: {integrity: sha512-65IdeeE8dAZR3n3J5Eq7LYiQ8BFGeEYCWPBCzycvafL7PkskbCyIclTQarRwf/HXFoRvezKCjaLwy/8v9Prk6g==} - - '@expo/sdk-runtime-versions@1.0.0': - resolution: {integrity: sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==} - - '@expo/spawn-async@1.7.2': - resolution: {integrity: sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==} - engines: {node: '>=12'} - - '@expo/sudo-prompt@9.3.2': - resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==} - - '@expo/vector-icons@14.1.0': - resolution: {integrity: sha512-7T09UE9h8QDTsUeMGymB4i+iqvtEeaO5VvUjryFB4tugDTG/bkzViWA74hm5pfjjDEhYMXWaX112mcvhccmIwQ==} - peerDependencies: - expo-font: '*' - react: '*' - react-native: '*' - - '@expo/vector-icons@15.1.1': - resolution: {integrity: sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==} - peerDependencies: - expo-font: '>=14.0.4' - react: '*' - react-native: '*' - - '@expo/ws-tunnel@1.0.6': - resolution: {integrity: sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==} - - '@expo/xcpretty@4.4.4': - resolution: {integrity: sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw==} - hasBin: true - - '@fastify/accept-negotiator@1.1.0': - resolution: {integrity: sha512-OIHZrb2ImZ7XG85HXOONLcJWGosv7sIvM2ifAPQVhg9Lv7qdmMBNVaai4QTdyuaqbKM5eO6sLSQOYI7wEQeCJQ==} - engines: {node: '>=14'} - - '@fastify/accept-negotiator@2.0.1': - resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} - - '@fastify/ajv-compiler@4.0.5': - resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} - - '@fastify/busboy@1.2.1': - resolution: {integrity: sha512-7PQA7EH43S0CxcOa9OeAnaeA0oQ+e/DHNPZwSQM9CQHW76jle5+OvLdibRp/Aafs9KXbLhxyjOTkRjWUbQEd3Q==} - engines: {node: '>=14'} - - '@fastify/busboy@3.2.0': - resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} - - '@fastify/cors@10.1.0': - resolution: {integrity: sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==} - - '@fastify/deepmerge@1.3.0': - resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} - - '@fastify/error@3.4.1': - resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==} - - '@fastify/error@4.2.0': - resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} - - '@fastify/fast-json-stringify-compiler@5.0.3': - resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} - - '@fastify/forwarded@3.0.1': - resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} - - '@fastify/helmet@13.0.2': - resolution: {integrity: sha512-tO1QMkOfNeCt9l4sG/FiWErH4QMm+RjHzbMTrgew1DYOQ2vb/6M1G2iNABBrD7Xq6dUk+HLzWW8u+rmmhQHifA==} - - '@fastify/merge-json-schemas@0.2.1': - resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} - - '@fastify/multipart@7.7.3': - resolution: {integrity: sha512-MG4Gd9FNEXc8qx0OgqoXM10EGO/dN/0iVQ8SrpFMU3d6F6KUfcqD2ZyoQhkm9LWrbiMgdHv5a43x78lASdn5GA==} - - '@fastify/proxy-addr@5.1.0': - resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} - - '@fastify/rate-limit@9.1.0': - resolution: {integrity: sha512-h5dZWCkuZXN0PxwqaFQLxeln8/LNwQwH9popywmDCFdKfgpi4b/HoMH1lluy6P+30CG9yzzpSpwTCIPNB9T1JA==} - - '@fastify/send@2.1.0': - resolution: {integrity: sha512-yNYiY6sDkexoJR0D8IDy3aRP3+L4wdqCpvx5WP+VtEU58sn7USmKynBzDQex5X42Zzvw2gNzzYgP90UfWShLFA==} - - '@fastify/send@4.1.0': - resolution: {integrity: sha512-TMYeQLCBSy2TOFmV95hQWkiTYgC/SEx7vMdV+wnZVX4tt8VBLKzmH8vV9OzJehV0+XBfg+WxPMt5wp+JBUKsVw==} - - '@fastify/sensible@6.0.4': - resolution: {integrity: sha512-1vxcCUlPMew6WroK8fq+LVOwbsLtX+lmuRuqpcp6eYqu6vmkLwbKTdBWAZwbeaSgCfW4tzUpTIHLLvTiQQ1BwQ==} - - '@fastify/static@6.12.0': - resolution: {integrity: sha512-KK1B84E6QD/FcQWxDI2aiUCwHxMJBI1KeCUzm1BwYpPY1b742+jeKruGHP2uOluuM6OkBPI8CIANrXcCRtC2oQ==} - - '@fastify/static@9.1.3': - resolution: {integrity: sha512-aXrYtsiryLhRxRNaxNqsn7FUISeb7rB9q4eHUPIot5aeQBLNahnz1m6thzm7JWC1poSGXS9XrX8DvuMivp2hkQ==} - - '@fastify/swagger-ui@1.10.2': - resolution: {integrity: sha512-f2mRqtblm6eRAFQ3e8zSngxVNEtiYY7rISKQVjPA++ZsWc5WYlPVTb6Bx0G/zy0BIoucNqDr/Q2Vb/kTYkOq1A==} - - '@fastify/swagger-ui@5.2.6': - resolution: {integrity: sha512-OMnms0O5s9wb6wis/K5nlrAMLsgUbr1GA8uphM41IasWe3AFdgxz6r/3bA9HTxlDNUYc2FGGKeqMp3ntxmSiNA==} - - '@fastify/swagger@8.15.0': - resolution: {integrity: sha512-zy+HEEKFqPMS2sFUsQU5X0MHplhKJvWeohBwTCkBAJA/GDYGLGUWQaETEhptiqxK7Hs0fQB9B4MDb3pbwIiCwA==} - - '@fastify/swagger@9.7.0': - resolution: {integrity: sha512-Vp1SC1GC2Hrkd3faFILv86BzUNyFz5N4/xdExqtCgkGASOzn/x+eMe4qXIGq7cdT6wif/P/oa6r1Ruqx19paZA==} - - '@firebase/app-check-interop-types@0.3.2': - resolution: {integrity: sha512-LMs47Vinv2HBMZi49C09dJxp0QT5LwDzFaVGf/+ITHe3BlIhUiLNttkATSXplc89A2lAaeTqjgqVkiRfUGyQiQ==} - - '@firebase/app-types@0.9.2': - resolution: {integrity: sha512-oMEZ1TDlBz479lmABwWsWjzHwheQKiAgnuKxE0pz0IXCVx7/rtlkx1fQ6GfgK24WCrxDKMplZrT50Kh04iMbXQ==} - - '@firebase/auth-interop-types@0.2.3': - resolution: {integrity: sha512-Fc9wuJGgxoxQeavybiuwgyi+0rssr76b+nHpj+eGhXFYAdudMWyfBHvFL/I5fEHniUM/UQdFzi9VXJK2iZF7FQ==} - - '@firebase/component@0.6.9': - resolution: {integrity: sha512-gm8EUEJE/fEac86AvHn8Z/QW8BvR56TBw3hMW0O838J/1mThYQXAIQBgUv75EqlCZfdawpWLrKt1uXvp9ciK3Q==} - - '@firebase/database-compat@1.0.8': - resolution: {integrity: sha512-OpeWZoPE3sGIRPBKYnW9wLad25RaWbGyk7fFQe4xnJQKRzlynWeFBSRRAoLE2Old01WXwskUiucNqUUVlFsceg==} - - '@firebase/database-types@1.0.5': - resolution: {integrity: sha512-fTlqCNwFYyq/C6W7AJ5OCuq5CeZuBEsEwptnVxlNPkWCo5cTTyukzAHRSO/jaQcItz33FfYrrFk1SJofcu2AaQ==} - - '@firebase/database@1.0.8': - resolution: {integrity: sha512-dzXALZeBI1U5TXt6619cv0+tgEhJiwlUtQ55WNZY7vGAjv7Q1QioV969iYwt1AQQ0ovHnEW0YW9TiBfefLvErg==} - - '@firebase/logger@0.4.2': - resolution: {integrity: sha512-Q1VuA5M1Gjqrwom6I6NUU4lQXdo9IAQieXlujeHZWvRt1b7qQ0KwBaNAjgxG27jgF9/mUwsNmO8ptBCGVYhB0A==} - - '@firebase/util@1.10.0': - resolution: {integrity: sha512-xKtx4A668icQqoANRxyDLBLz51TAbDP9KRfpbKGxiCAW346d0BeJe5vN6/hKxxmWwnZ0mautyv39JxviwwQMOQ==} - - '@google-cloud/firestore@7.11.6': - resolution: {integrity: sha512-EW/O8ktzwLfyWBOsNuhRoMi8lrC3clHM5LVFhGvO1HCsLozCOOXRAlHrYBoE6HL42Sc8yYMuCb2XqcnJ4OOEpw==} - engines: {node: '>=14.0.0'} - - '@google-cloud/paginator@5.0.2': - resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} - engines: {node: '>=14.0.0'} - - '@google-cloud/projectify@4.0.0': - resolution: {integrity: sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==} - engines: {node: '>=14.0.0'} - - '@google-cloud/promisify@4.0.0': - resolution: {integrity: sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==} - engines: {node: '>=14'} - - '@google-cloud/storage@7.19.0': - resolution: {integrity: sha512-n2FjE7NAOYyshogdc7KQOl/VZb4sneqPjWouSyia9CMDdMhRX5+RIbqalNmC7LOLzuLAN89VlF2HvG8na9G+zQ==} - engines: {node: '>=14'} - - '@graphql-typed-document-node/core@3.2.0': - resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} - peerDependencies: - graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - - '@grpc/grpc-js@1.14.3': - resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==} - engines: {node: '>=12.10.0'} - - '@grpc/proto-loader@0.7.15': - resolution: {integrity: sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==} - engines: {node: '>=6'} - hasBin: true - - '@grpc/proto-loader@0.8.1': - resolution: {integrity: sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==} - engines: {node: '>=6'} - hasBin: true - - '@hapi/hoek@9.3.0': - resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==} - - '@hapi/topo@5.1.0': - resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==} - - '@humanwhocodes/config-array@0.13.0': - resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} - engines: {node: '>=10.10.0'} - deprecated: Use @eslint/config-array instead - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/object-schema@2.0.3': - resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} - deprecated: Use @eslint/object-schema instead - - '@ide/backoff@1.0.0': - resolution: {integrity: sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==} - - '@img/colour@1.1.0': - resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@ioredis/commands@1.5.1': resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} @@ -3031,96 +693,6 @@ packages: resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} - '@isaacs/ttlcache@1.4.1': - resolution: {integrity: sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==} - engines: {node: '>=12'} - - '@istanbuljs/load-nyc-config@1.1.0': - resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} - engines: {node: '>=8'} - - '@istanbuljs/schema@0.1.6': - resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} - engines: {node: '>=8'} - - '@jest/console@29.7.0': - resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/core@29.7.0': - resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/create-cache-key-function@29.7.0': - resolution: {integrity: sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/environment@29.7.0': - resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect-utils@29.7.0': - resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/expect@29.7.0': - resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/fake-timers@29.7.0': - resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/globals@29.7.0': - resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/reporters@29.7.0': - resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - '@jest/schemas@29.6.3': - resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/source-map@29.6.3': - resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-result@29.7.0': - resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/test-sequencer@29.7.0': - resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/transform@29.7.0': - resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - '@jest/types@24.9.0': - resolution: {integrity: sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==} - engines: {node: '>= 6'} - - '@jest/types@26.6.2': - resolution: {integrity: sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==} - engines: {node: '>= 10.14.2'} - - '@jest/types@29.6.3': - resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -3140,182 +712,11 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jridgewell/trace-mapping@0.3.9': - resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - - '@js-sdsl/ordered-map@4.4.2': - resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} - - '@keyv/serialize@1.1.1': - resolution: {integrity: sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==} - - '@libsql/client@0.17.3': - resolution: {integrity: sha512-HXk9wiAoJbKFbyBH4O+aEhN6ir5ERXuXvwE5OD2eR4/5RUa3Pw/8L9zrnVdU+iNJitRvisPWaIwmhkO3bH7giA==} - - '@libsql/core@0.17.3': - resolution: {integrity: sha512-2UjK1i7JBkMduJo4WdvvBxMMvVJ31pArBZNONyz/GCJJAH+1UHat2X6vn10S/WpY5fKzIT98WqYFl2vzWRLOfg==} - - '@libsql/darwin-arm64@0.5.29': - resolution: {integrity: sha512-K+2RIB1OGFPYQbfay48GakLhqf3ArcbHqPFu7EZiaUcRgFcdw8RoltsMyvbj5ix2fY0HV3Q3Ioa/ByvQdaSM0A==} - cpu: [arm64] - os: [darwin] - - '@libsql/darwin-x64@0.5.29': - resolution: {integrity: sha512-OtT+KFHsKFy1R5FVadr8FJ2Bb1mghtXTyJkxv0trocq7NuHntSki1eUbxpO5ezJesDvBlqFjnWaYYY516QNLhQ==} - cpu: [x64] - os: [darwin] - - '@libsql/hrana-client@0.10.0': - resolution: {integrity: sha512-OoA4EMqRAC7kn7V2P6EQqRcpZf2W+AjsNIyCizBg339Tq/aMC7sRnzs3SklderhmQWAqEzvv8A2vhxVmWpkVvw==} - - '@libsql/isomorphic-ws@0.1.5': - resolution: {integrity: sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg==} - - '@libsql/linux-arm-gnueabihf@0.5.29': - resolution: {integrity: sha512-CD4n4zj7SJTHso4nf5cuMoWoMSS7asn5hHygsDuhRl8jjjCTT3yE+xdUvI4J7zsyb53VO5ISh4cwwOtf6k2UhQ==} - cpu: [arm] - os: [linux] - - '@libsql/linux-arm-musleabihf@0.5.29': - resolution: {integrity: sha512-2Z9qBVpEJV7OeflzIR3+l5yAd4uTOLxklScYTwpZnkm2vDSGlC1PRlueLaufc4EFITkLKXK2MWBpexuNJfMVcg==} - cpu: [arm] - os: [linux] - - '@libsql/linux-arm64-gnu@0.5.29': - resolution: {integrity: sha512-gURBqaiXIGGwFNEaUj8Ldk7Hps4STtG+31aEidCk5evMMdtsdfL3HPCpvys+ZF/tkOs2MWlRWoSq7SOuCE9k3w==} - cpu: [arm64] - os: [linux] - - '@libsql/linux-arm64-musl@0.5.29': - resolution: {integrity: sha512-fwgYZ0H8mUkyVqXZHF3mT/92iIh1N94Owi/f66cPVNsk9BdGKq5gVpoKO+7UxaNzuEH1roJp2QEwsCZMvBLpqg==} - cpu: [arm64] - os: [linux] - - '@libsql/linux-x64-gnu@0.5.29': - resolution: {integrity: sha512-y14V0vY0nmMC6G0pHeJcEarcnGU2H6cm21ZceRkacWHvQAEhAG0latQkCtoS2njFOXiYIg+JYPfAoWKbi82rkg==} - cpu: [x64] - os: [linux] - - '@libsql/linux-x64-musl@0.5.29': - resolution: {integrity: sha512-gquqwA/39tH4pFl+J9n3SOMSymjX+6kZ3kWgY3b94nXFTwac9bnFNMffIomgvlFaC4ArVqMnOZD3nuJ3H3VO1w==} - cpu: [x64] - os: [linux] - - '@libsql/win32-x64-msvc@0.5.29': - resolution: {integrity: sha512-4/0CvEdhi6+KjMxMaVbFM2n2Z44escBRoEYpR+gZg64DdetzGnYm8mcNLcoySaDJZNaBd6wz5DNdgRmcI4hXcg==} - cpu: [x64] - os: [win32] - - '@lukeed/csprng@1.1.0': - resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} - engines: {node: '>=8'} - - '@lukeed/ms@2.0.2': - resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} - engines: {node: '>=8'} - - '@lukeed/uuid@2.0.1': - resolution: {integrity: sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==} - engines: {node: '>=8'} - '@mapbox/node-pre-gyp@2.0.3': resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} engines: {node: '>=18'} hasBin: true - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==} - cpu: [x64] - os: [win32] - - '@napi-rs/wasm-runtime@1.1.4': - resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - - '@neon-rs/load@0.0.4': - resolution: {integrity: sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw==} - - '@next/env@16.2.6': - resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} - - '@next/swc-darwin-arm64@16.2.6': - resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@next/swc-darwin-x64@16.2.6': - resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@next/swc-linux-arm64-gnu@16.2.6': - resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-arm64-musl@16.2.6': - resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@next/swc-linux-x64-gnu@16.2.6': - resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-linux-x64-musl@16.2.6': - resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@next/swc-win32-arm64-msvc@16.2.6': - resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@next/swc-win32-x64-msvc@16.2.6': - resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@nodable/entities@2.1.0': - resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} - '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3328,375 +729,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@npmcli/fs@3.1.1': - resolution: {integrity: sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - '@one-ini/wasm@0.1.1': - resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} - - '@openfeature/core@1.10.0': - resolution: {integrity: sha512-C3ynxtPYhe5qVJgQIqCNxZXeRXo4t1Eo86uiROr8+sD6F2OjEJGIp6VOGxz+r6jkad2s8Br9BnUJOvskhai/3A==} - - '@openfeature/server-sdk@1.21.0': - resolution: {integrity: sha512-ZBVAiyMeN+dxurcXGJlvuOpYg9X7V923MVCI5dq/kE/5o8ys7MCOPhW44e4PS+XABHzSRTw44hcgWez93xwifw==} - engines: {node: '>=20'} - peerDependencies: - '@openfeature/core': ^1.10.0 - - '@opentelemetry/api-logs@0.218.0': - resolution: {integrity: sha512-fmEWp5kXlGEc3i/lR698Hz41DfGyN4Tbe4g7L1AxSc7fF8Xeh/FQ9Quqpa9dVA413Q1Ad43QOLzU4JoXgbFPWw==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/api-logs@0.53.0': - resolution: {integrity: sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==} - engines: {node: '>=14'} - - '@opentelemetry/api-logs@0.57.1': - resolution: {integrity: sha512-I4PHczeujhQAQv6ZBzqHYEUiggZL4IdSMixtVD3EYqbdrjujE7kRfI5QohjlPoJm8BvenoW5YaTMWRrbpot6tg==} - engines: {node: '>=14'} - - '@opentelemetry/api-logs@0.57.2': - resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} - engines: {node: '>=14'} - - '@opentelemetry/api@1.9.1': - resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/context-async-hooks@1.30.1': - resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/core@1.30.1': - resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/instrumentation-amqplib@0.46.1': - resolution: {integrity: sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-connect@0.43.0': - resolution: {integrity: sha512-Q57JGpH6T4dkYHo9tKXONgLtxzsh1ZEW5M9A/OwKrZFyEpLqWgjhcZ3hIuVvDlhb426iDF1f9FPToV/mi5rpeA==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-dataloader@0.16.0': - resolution: {integrity: sha512-88+qCHZC02up8PwKHk0UQKLLqGGURzS3hFQBZC7PnGwReuoKjHXS1o29H58S+QkXJpkTr2GACbx8j6mUoGjNPA==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-express@0.47.0': - resolution: {integrity: sha512-XFWVx6k0XlU8lu6cBlCa29ONtVt6ADEjmxtyAyeF2+rifk8uBJbk1La0yIVfI0DoKURGbaEDTNelaXG9l/lNNQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-fastify@0.44.1': - resolution: {integrity: sha512-RoVeMGKcNttNfXMSl6W4fsYoCAYP1vi6ZAWIGhBY+o7R9Y0afA7f9JJL0j8LHbyb0P0QhSYk+6O56OwI2k4iRQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-fs@0.19.0': - resolution: {integrity: sha512-JGwmHhBkRT2G/BYNV1aGI+bBjJu4fJUD/5/Jat0EWZa2ftrLV3YE8z84Fiij/wK32oMZ88eS8DI4ecLGZhpqsQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-generic-pool@0.43.0': - resolution: {integrity: sha512-at8GceTtNxD1NfFKGAuwtqM41ot/TpcLh+YsGe4dhf7gvv1HW/ZWdq6nfRtS6UjIvZJOokViqLPJ3GVtZItAnQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-graphql@0.47.0': - resolution: {integrity: sha512-Cc8SMf+nLqp0fi8oAnooNEfwZWFnzMiBHCGmDFYqmgjPylyLmi83b+NiTns/rKGwlErpW0AGPt0sMpkbNlzn8w==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-hapi@0.45.1': - resolution: {integrity: sha512-VH6mU3YqAKTePPfUPwfq4/xr049774qWtfTuJqVHoVspCLiT3bW+fCQ1toZxt6cxRPYASoYaBsMA3CWo8B8rcw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-http@0.57.1': - resolution: {integrity: sha512-ThLmzAQDs7b/tdKI3BV2+yawuF09jF111OFsovqT1Qj3D8vjwKBwhi/rDE5xethwn4tSXtZcJ9hBsVAlWFQZ7g==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-ioredis@0.47.0': - resolution: {integrity: sha512-4HqP9IBC8e7pW9p90P3q4ox0XlbLGme65YTrA3UTLvqvo4Z6b0puqZQP203YFu8m9rE/luLfaG7/xrwwqMUpJw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-kafkajs@0.7.0': - resolution: {integrity: sha512-LB+3xiNzc034zHfCtgs4ITWhq6Xvdo8bsq7amR058jZlf2aXXDrN9SV4si4z2ya9QX4tz6r4eZJwDkXOp14/AQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-knex@0.44.0': - resolution: {integrity: sha512-SlT0+bLA0Lg3VthGje+bSZatlGHw/vwgQywx0R/5u9QC59FddTQSPJeWNw29M6f8ScORMeUOOTwihlQAn4GkJQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-koa@0.47.0': - resolution: {integrity: sha512-HFdvqf2+w8sWOuwtEXayGzdZ2vWpCKEQv5F7+2DSA74Te/Cv4rvb2E5So5/lh+ok4/RAIPuvCbCb/SHQFzMmbw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-lru-memoizer@0.44.0': - resolution: {integrity: sha512-Tn7emHAlvYDFik3vGU0mdwvWJDwtITtkJ+5eT2cUquct6nIs+H8M47sqMJkCpyPe5QIBJoTOHxmc6mj9lz6zDw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mongodb@0.51.0': - resolution: {integrity: sha512-cMKASxCX4aFxesoj3WK8uoQ0YUrRvnfxaO72QWI2xLu5ZtgX/QvdGBlU3Ehdond5eb74c2s1cqRQUIptBnKz1g==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mongoose@0.46.0': - resolution: {integrity: sha512-mtVv6UeaaSaWTeZtLo4cx4P5/ING2obSqfWGItIFSunQBrYROfhuVe7wdIrFUs2RH1tn2YYpAJyMaRe/bnTTIQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mysql2@0.45.0': - resolution: {integrity: sha512-qLslv/EPuLj0IXFvcE3b0EqhWI8LKmrgRPIa4gUd8DllbBpqJAvLNJSv3cC6vWwovpbSI3bagNO/3Q2SuXv2xA==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mysql@0.45.0': - resolution: {integrity: sha512-tWWyymgwYcTwZ4t8/rLDfPYbOTF3oYB8SxnYMtIQ1zEf5uDm90Ku3i6U/vhaMyfHNlIHvDhvJh+qx5Nc4Z3Acg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-nestjs-core@0.44.0': - resolution: {integrity: sha512-t16pQ7A4WYu1yyQJZhRKIfUNvl5PAaF2pEteLvgJb/BWdd1oNuU1rOYt4S825kMy+0q4ngiX281Ss9qiwHfxFQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-pg@0.50.0': - resolution: {integrity: sha512-TtLxDdYZmBhFswm8UIsrDjh/HFBeDXd4BLmE8h2MxirNHewLJ0VS9UUddKKEverb5Sm2qFVjqRjcU+8Iw4FJ3w==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-redis-4@0.46.0': - resolution: {integrity: sha512-aTUWbzbFMFeRODn3720TZO0tsh/49T8H3h8vVnVKJ+yE36AeW38Uj/8zykQ/9nO8Vrtjr5yKuX3uMiG/W8FKNw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-tedious@0.18.0': - resolution: {integrity: sha512-9zhjDpUDOtD+coeADnYEJQ0IeLVCj7w/hqzIutdp5NqS1VqTAanaEfsEcSypyvYv5DX3YOsTUoF+nr2wDXPETA==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-undici@0.10.0': - resolution: {integrity: sha512-vm+V255NGw9gaSsPD6CP0oGo8L55BffBc8KnxqsMuc6XiAD1L8SFNzsW0RHhxJFqy9CJaJh+YiJ5EHXuZ5rZBw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.7.0 - - '@opentelemetry/instrumentation@0.53.0': - resolution: {integrity: sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation@0.57.1': - resolution: {integrity: sha512-SgHEKXoVxOjc20ZYusPG3Fh+RLIZTSa4x8QtD3NfgAUDyqdFFS9W1F2ZVbZkqDCdyMcQG02Ok4duUGLHJXHgbA==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation@0.57.2': - resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/redis-common@0.36.2': - resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} - engines: {node: '>=14'} - - '@opentelemetry/resources@1.30.1': - resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/sdk-trace-base@1.30.1': - resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/semantic-conventions@1.27.0': - resolution: {integrity: sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==} - engines: {node: '>=14'} - - '@opentelemetry/semantic-conventions@1.28.0': - resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} - engines: {node: '>=14'} - - '@opentelemetry/semantic-conventions@1.41.1': - resolution: {integrity: sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==} - engines: {node: '>=14'} - - '@opentelemetry/sql-common@0.40.1': - resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.1.0 - - '@oxc-parser/binding-android-arm-eabi@0.129.0': - resolution: {integrity: sha512-sG37CfXLlYXdDrggAFO/mKcO4w36piwf862xAZXIuf3nzKhWK1FvW4dqie+06++z+mDto2QeOQSvhyzBeK5jsQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [android] - - '@oxc-parser/binding-android-arm64@0.129.0': - resolution: {integrity: sha512-DVyLFN2+S0VOhT6lm5++tFqlu3x2Njiby6y5DhTzjV5uRsZWpifsBn6+yjtwAxl105peEjs5BHE3ToBJuQjLTg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@oxc-parser/binding-darwin-arm64@0.129.0': - resolution: {integrity: sha512-QeqThtB8qax4IL+NFBWgshudyKkj5c076L8vyd8PCEx7U1wHyIbH49MEQ5J5iURFhUW5jiFmdnLKEwyOo0GAJA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@oxc-parser/binding-darwin-x64@0.129.0': - resolution: {integrity: sha512-zn5+7nv4DlK4uFgblmhKm6xRV0QUHXOHyIDkjmhxJ53xSA9ahkb3pHNiHesNPXn/nK/VWU+C+Z6JYHdatZBh7g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@oxc-parser/binding-freebsd-x64@0.129.0': - resolution: {integrity: sha512-SPTcDBiHWlgRpWFC1jnoi0sBWqCw4DFR+4b8+dV+NAhUu2ONERWyIVIOCfcE9a8BlvZsDCuXf3l/x7wQUs1Rsw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@oxc-parser/binding-linux-arm-gnueabihf@0.129.0': - resolution: {integrity: sha512-Rgc9+WNKLbc+chyDTXyyJ7gbgLo+ve27CrRnmIwGgucGflrBZbutge5jdPPegcgf46RrR4dkBbMCp0/x16mdig==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxc-parser/binding-linux-arm-musleabihf@0.129.0': - resolution: {integrity: sha512-YtSsJ51VysXqlO8Cs2mWTyXvxBRemTHj4WDQjXwKl0SAxh+CVrEdXrdH+RnjxLj3JCUMFeYuHs5c+/DImfbKkg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@oxc-parser/binding-linux-arm64-gnu@0.129.0': - resolution: {integrity: sha512-9oK8iQr9KtgI5JhaJ+5IwiQsXEo6NuasFgovtJGrdK/RxbA0bO4YKRvVY7M+8lozUCVz1U7XrFFODv3emIOPRA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - - '@oxc-parser/binding-linux-arm64-musl@0.129.0': - resolution: {integrity: sha512-GghE/bf9ZqgqZFxLacgP0ImVD6UiLKQOpvpgUoIsqjopu2ms/+p1L0d0Dv2Sck+8p0FbKS2WE3IjqmIlLbxJgA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - - '@oxc-parser/binding-linux-ppc64-gnu@0.129.0': - resolution: {integrity: sha512-A2PW0UbERzKGV6fKX1zoe2Tkc1zVcEJSSPW9IUSKbZAPuPe+M5/5hTA+6fQbWmevabe2B3IDky66a1lFGjpBKA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - - '@oxc-parser/binding-linux-riscv64-gnu@0.129.0': - resolution: {integrity: sha512-omwxd9H+jrl1T72RI666k4ho7Eli2iHdELzf+dL0D+uXThNZXYJCbKjm5rK2hrHmDy4O+NWv7+khBrEkorLsgw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - - '@oxc-parser/binding-linux-riscv64-musl@0.129.0': - resolution: {integrity: sha512-v2hi8id+M8C0uY8uuG2t2a5vr8H9XyHXiHL12yMdMNtgn04nnM/8hlOGuoJuxVc07PhClNiaoSaY2eXehSRa7w==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [riscv64] - os: [linux] - - '@oxc-parser/binding-linux-s390x-gnu@0.129.0': - resolution: {integrity: sha512-UXrdDyLh1Obgj5X+IVVXWoo5/FJbFsU8/uLQ/M9lkVUwBUKpRFxNEhzwBNv21qqdKgAh+pr2CCVD8J1JfRPsIQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - - '@oxc-parser/binding-linux-x64-gnu@0.129.0': - resolution: {integrity: sha512-hsL/3/kdX9FGLqOj8DR3Eki4Y6zO1i3+ZHhiPwX0hDt4n+18abkfUzePCv3h8SShprwCmwdxPnlrebZ5+MZ+cw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - - '@oxc-parser/binding-linux-x64-musl@0.129.0': - resolution: {integrity: sha512-kdXvJ4crOeRld3vWl0J0VU4nmnT4pZ3lKGA5tZ1y0UPWsBtElDYd+jsz4lE36tpAbCiWm0M0PG0laUNBSE+Wlw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - - '@oxc-parser/binding-openharmony-arm64@0.129.0': - resolution: {integrity: sha512-DusJfcK7EGwf9TEakB+z6SXCLdHGvDZ8U8882bzWb4oVrORHpbkFl9npS7cN3YC2axcVKoktbxZevS1nxVCKFw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@oxc-parser/binding-wasm32-wasi@0.129.0': - resolution: {integrity: sha512-Iie9CcII+ELSinKFnxTR15xhI9qriVivEhbFh3driRNbzms/5ioDAU0fwe8Mf1FEaz3n2FtiUVX0h0nwKLYk0A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@oxc-parser/binding-win32-arm64-msvc@0.129.0': - resolution: {integrity: sha512-99kH1udLyrts+wGm+u0VhPbogkb2wxc/6J1XMKOpS6Kx5DjBWGRZZfBjfCGI3xKSInpYbZn4TLWLX1Q1GURYwg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@oxc-parser/binding-win32-ia32-msvc@0.129.0': - resolution: {integrity: sha512-tmSBR1A4yH697qV291xKyDe4OAWFchJ+cXf2wuipx/vK3n5d5Ej9MVLRtXlIcZ38n8qAjsF0/AnskaYgxM151A==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ia32] - os: [win32] - - '@oxc-parser/binding-win32-x64-msvc@0.129.0': - resolution: {integrity: sha512-Z1PbJvkPeLASIUxa3AnrQ5H+vv1K9zC0IGnQqoKfM0ZvsvCSe0d3u5m7d9iuy+HB7GrcElHuwKb0d0qFdtG0iA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@oxc-project/types@0.129.0': - resolution: {integrity: sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==} - - '@panva/hkdf@1.2.1': - resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} - '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -3785,9 +817,6 @@ packages: resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} engines: {node: '>= 10.0.0'} - '@pinojs/redact@0.4.0': - resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3801,354 +830,6 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - '@prisma/client@5.22.0': - resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} - engines: {node: '>=16.13'} - peerDependencies: - prisma: '*' - peerDependenciesMeta: - prisma: - optional: true - - '@prisma/client@6.19.3': - resolution: {integrity: sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==} - engines: {node: '>=18.18'} - peerDependencies: - prisma: '*' - typescript: '>=5.1.0' - peerDependenciesMeta: - prisma: - optional: true - typescript: - optional: true - - '@prisma/config@6.19.3': - resolution: {integrity: sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==} - - '@prisma/debug@5.22.0': - resolution: {integrity: sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==} - - '@prisma/debug@6.19.3': - resolution: {integrity: sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==} - - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': - resolution: {integrity: sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==} - - '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': - resolution: {integrity: sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==} - - '@prisma/engines@5.22.0': - resolution: {integrity: sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==} - - '@prisma/engines@6.19.3': - resolution: {integrity: sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==} - - '@prisma/fetch-engine@5.22.0': - resolution: {integrity: sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==} - - '@prisma/fetch-engine@6.19.3': - resolution: {integrity: sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==} - - '@prisma/get-platform@5.22.0': - resolution: {integrity: sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==} - - '@prisma/get-platform@6.19.3': - resolution: {integrity: sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==} - - '@prisma/instrumentation@5.22.0': - resolution: {integrity: sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==} - - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.5': - resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/inquire@1.1.1': - resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.1': - resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} - - '@react-email/render@0.0.16': - resolution: {integrity: sha512-wDaMy27xAq1cJHtSFptp0DTKPuV2GYhloqia95ub/DH9Dea1aWYsbdM918MOc/b/HvVS3w1z8DWzfAk13bGStQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - react: ^18.2.0 - react-dom: ^18.2.0 - - '@react-native-async-storage/async-storage@1.23.1': - resolution: {integrity: sha512-Qd2kQ3yi6Y3+AcUlrHxSLlnBvpdCEMVGFlVBneVOjaFaPU61g1huc38g339ysXspwY1QZA2aNhrk/KlHGO+ewA==} - peerDependencies: - react-native: ^0.0.0-0 || >=0.60 <1.0 - - '@react-native-community/cli-clean@13.6.9': - resolution: {integrity: sha512-7Dj5+4p9JggxuVNOjPbduZBAP1SUgNhLKVw5noBUzT/3ZpUZkDM+RCSwyoyg8xKWoE4OrdUAXwAFlMcFDPKykA==} - - '@react-native-community/cli-config@13.6.9': - resolution: {integrity: sha512-rFfVBcNojcMm+KKHE/xqpqXg8HoKl4EC7bFHUrahMJ+y/tZll55+oX/PGG37rzB8QzP2UbMQ19DYQKC1G7kXeg==} - - '@react-native-community/cli-debugger-ui@13.6.9': - resolution: {integrity: sha512-TkN7IdFmGPPvTpAo3nCAH9uwGCPxWBEAwpqEZDrq0NWllI7Tdie8vDpGdrcuCcKalmhq6OYnkXzeBah7O1Ztpw==} - - '@react-native-community/cli-doctor@13.6.9': - resolution: {integrity: sha512-5quFaLdWFQB+677GXh5dGU9I5eg2z6Vg4jOX9vKnc9IffwyIFAyJfCZHrxLSRPDGNXD7biDQUdoezXYGwb6P/A==} - - '@react-native-community/cli-hermes@13.6.9': - resolution: {integrity: sha512-GvwiwgvFw4Ws+krg2+gYj8sR3g05evmNjAHkKIKMkDTJjZ8EdyxbkifRUs1ZCq3TMZy2oeblZBXCJVOH4W7ZbA==} - - '@react-native-community/cli-platform-android@13.6.9': - resolution: {integrity: sha512-9KsYGdr08QhdvT3Ht7e8phQB3gDX9Fs427NJe0xnoBh+PDPTI2BD5ks5ttsH8CzEw8/P6H8tJCHq6hf2nxd9cw==} - - '@react-native-community/cli-platform-apple@13.6.9': - resolution: {integrity: sha512-KoeIHfhxMhKXZPXmhQdl6EE+jGKWwoO9jUVWgBvibpVmsNjo7woaG/tfJMEWfWF3najX1EkQAoJWpCDBMYWtlA==} - - '@react-native-community/cli-platform-ios@13.6.9': - resolution: {integrity: sha512-CiUcHlGs8vE0CAB4oi1f+dzniqfGuhWPNrDvae2nm8dewlahTBwIcK5CawyGezjcJoeQhjBflh9vloska+nlnw==} - - '@react-native-community/cli-server-api@13.6.9': - resolution: {integrity: sha512-W8FSlCPWymO+tlQfM3E0JmM8Oei5HZsIk5S0COOl0MRi8h0NmHI4WSTF2GCfbFZkcr2VI/fRsocoN8Au4EZAug==} - - '@react-native-community/cli-tools@13.6.9': - resolution: {integrity: sha512-OXaSjoN0mZVw3nrAwcY1PC0uMfyTd9fz7Cy06dh+EJc+h0wikABsVRzV8cIOPrVV+PPEEXE0DBrH20T2puZzgQ==} - - '@react-native-community/cli-types@13.6.9': - resolution: {integrity: sha512-RLxDppvRxXfs3hxceW/mShi+6o5yS+kFPnPqZTaMKKR5aSg7LwDpLQW4K2D22irEG8e6RKDkZUeH9aL3vO2O0w==} - - '@react-native-community/cli@13.6.9': - resolution: {integrity: sha512-hFJL4cgLPxncJJd/epQ4dHnMg5Jy/7Q56jFvA3MHViuKpzzfTCJCB+pGY54maZbtym53UJON9WTGpM3S81UfjQ==} - engines: {node: '>=18'} - hasBin: true - - '@react-native-community/netinfo@11.4.1': - resolution: {integrity: sha512-B0BYAkghz3Q2V09BF88RA601XursIEA111tnc2JOaN7axJWmNefmfjZqw/KdSxKZp7CZUuPpjBmz/WCR9uaHYg==} - peerDependencies: - react-native: '>=0.59' - - '@react-native/assets-registry@0.74.87': - resolution: {integrity: sha512-1XmRhqQchN+pXPKEKYdpJlwESxVomJOxtEnIkbo7GAlaN2sym84fHEGDXAjLilih5GVPpcpSmFzTy8jx3LtaFg==} - engines: {node: '>=18'} - - '@react-native/assets-registry@0.85.3': - resolution: {integrity: sha512-u9ZiYP23vA2IFtdFQFmetzSmk6SM0xgKIoiOsr1hXNHjHaLhOm+/Ph1ud57wX6+Dbwdzx8coJgnzSKL3W21PCg==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - '@react-native/babel-plugin-codegen@0.74.87': - resolution: {integrity: sha512-+vJYpMnENFrwtgvDfUj+CtVJRJuUnzAUYT0/Pb68Sq9RfcZ5xdcCuUgyf7JO+akW2VTBoJY427wkcxU30qrWWw==} - engines: {node: '>=18'} - - '@react-native/babel-plugin-codegen@0.83.6': - resolution: {integrity: sha512-qfRXsHGeucT5c6mK+8Q7v4Ly3zmygfVmFlEtkiq7q07W1OTreld6nib4rJ/DBEeNiKBoBTuHjWliYGNuDjLFQA==} - engines: {node: '>= 20.19.4'} - - '@react-native/babel-preset@0.74.87': - resolution: {integrity: sha512-hyKpfqzN2nxZmYYJ0tQIHG99FQO0OWXp/gVggAfEUgiT+yNKas1C60LuofUsK7cd+2o9jrpqgqW4WzEDZoBlTg==} - engines: {node: '>=18'} - peerDependencies: - '@babel/core': '*' - - '@react-native/babel-preset@0.83.6': - resolution: {integrity: sha512-4/fXFDUvGOObETZq4+SUFkafld6OGgQWut5cQiqVghlhCB5z/p2lVhPgEUr/aTxTzeS3AmN+ztC+GpYPQ7tsTw==} - engines: {node: '>= 20.19.4'} - peerDependencies: - '@babel/core': '*' - - '@react-native/codegen@0.74.87': - resolution: {integrity: sha512-GMSYDiD+86zLKgMMgz9z0k6FxmRn+z6cimYZKkucW4soGbxWsbjUAZoZ56sJwt2FJ3XVRgXCrnOCgXoH/Bkhcg==} - engines: {node: '>=18'} - peerDependencies: - '@babel/preset-env': ^7.1.6 - - '@react-native/codegen@0.83.6': - resolution: {integrity: sha512-doB/Pq6Cf6IjF3wlQXTIiZOnsX9X8mEEk+CdGfyuCwZjWrf7IB8KaZEXXckJmfUcIwvJ9u/a72ZoTTCIoxAc9A==} - engines: {node: '>= 20.19.4'} - peerDependencies: - '@babel/core': '*' - - '@react-native/codegen@0.85.3': - resolution: {integrity: sha512-/JkS1lGLyzBWP1FbgDwaqEf7qShIC6pUC1M0a/YMAd/v4iqR24MRkQWe7jkYvcBQ2LpEhs5NGE9InhxSv21zCA==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - peerDependencies: - '@babel/core': '*' - - '@react-native/community-cli-plugin@0.74.87': - resolution: {integrity: sha512-EgJG9lSr8x3X67dHQKQvU6EkO+3ksVlJHYIVv6U/AmW9dN80BEFxgYbSJ7icXS4wri7m4kHdgeq2PQ7/3vvrTQ==} - engines: {node: '>=18'} - - '@react-native/community-cli-plugin@0.85.3': - resolution: {integrity: sha512-fs85dmbIqNmtzEixDb0g+q6R3Vt4H9eAt8/inIZdDKfjN76+sUJA2r1nxODQ76bU23MrIbz8sI7KFBPaWk/zQw==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - peerDependencies: - '@react-native-community/cli': '*' - '@react-native/metro-config': 0.85.3 - peerDependenciesMeta: - '@react-native-community/cli': - optional: true - '@react-native/metro-config': - optional: true - - '@react-native/debugger-frontend@0.74.85': - resolution: {integrity: sha512-gUIhhpsYLUTYWlWw4vGztyHaX/kNlgVspSvKe2XaPA7o3jYKUoNLc3Ov7u70u/MBWfKdcEffWq44eSe3j3s5JQ==} - engines: {node: '>=18'} - - '@react-native/debugger-frontend@0.74.87': - resolution: {integrity: sha512-MN95DJLYTv4EqJc+9JajA3AJZSBYJz2QEJ3uWlHrOky2vKrbbRVaW1ityTmaZa2OXIvNc6CZwSRSE7xCoHbXhQ==} - engines: {node: '>=18'} - - '@react-native/debugger-frontend@0.83.6': - resolution: {integrity: sha512-TyWXEpAjVundrc87fPWg91piOUg75+X9iutcfDe7cO3NrAEYCsl7Z09rKHuiAGkxfG9/rFD13dPsYIixUFkSFA==} - engines: {node: '>= 20.19.4'} - - '@react-native/debugger-frontend@0.85.3': - resolution: {integrity: sha512-uAu7rM5o/Np1zgp6fi5zM1sP1aB8DcS7DdOLcj/TkSutOAjkMqqd2lWt1/+3S7qXexRHVK5XcP+o3VXo4L/V0A==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - '@react-native/debugger-shell@0.83.6': - resolution: {integrity: sha512-684TJMBCU0l0ZjJWzrnK0HH+ERaM9KLyxyArE1k7BrP+gVl4X9GO0Pi94RoInOxvW/nyV65sOU6Ip1F3ygS0cg==} - engines: {node: '>= 20.19.4'} - - '@react-native/debugger-shell@0.85.3': - resolution: {integrity: sha512-/jRAaT9boiCttIcEwS02WPwYkUihqsjSaK/TMtHz05vT6uMgac9PaQt5kzBQLIABv5aEIa5gtrMmKVz49MjkjQ==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - '@react-native/dev-middleware@0.74.85': - resolution: {integrity: sha512-BRmgCK5vnMmHaKRO+h8PKJmHHH3E6JFuerrcfE3wG2eZ1bcSr+QTu8DAlpxsDWvJvHpCi8tRJGauxd+Ssj/c7w==} - engines: {node: '>=18'} - - '@react-native/dev-middleware@0.74.87': - resolution: {integrity: sha512-7TmZ3hTHwooYgIHqc/z87BMe1ryrIqAUi+AF7vsD+EHCGxHFdMjSpf1BZ2SUPXuLnF2cTiTfV2RwhbPzx0tYIA==} - engines: {node: '>=18'} - - '@react-native/dev-middleware@0.83.6': - resolution: {integrity: sha512-22xoddLTelpcVnF385SNH2hdP7X2av5pu7yRl/WnM5jBznbcl0+M9Ce94cj+WVeomsoUF/vlfuB0Ooy+RMlRiA==} - engines: {node: '>= 20.19.4'} - - '@react-native/dev-middleware@0.85.3': - resolution: {integrity: sha512-JYzBiT4A8w+KQt+dOD5v+ti+tDrGoPnsSTuApq3Ls4RB5sfWbDlYMyz3dbc8qBIHz9tv0sQ5+eOu6Xwqzr5AQA==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - '@react-native/gradle-plugin@0.74.87': - resolution: {integrity: sha512-T+VX0N1qP+U9V4oAtn7FTX7pfsoVkd1ocyw9swYXgJqU2fK7hC9famW7b3s3ZiufPGPr1VPJe2TVGtSopBjL6A==} - engines: {node: '>=18'} - - '@react-native/gradle-plugin@0.85.3': - resolution: {integrity: sha512-39dY2j50Q1pntejzwt3XL7vwXtrj8jcIfHq6E+gyu3jzYxZJVvMkMutQ39vSg6zinIQOX36oQDhidXUbCXzgoA==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - '@react-native/js-polyfills@0.74.87': - resolution: {integrity: sha512-M5Evdn76CuVEF0GsaXiGi95CBZ4IWubHqwXxV9vG9CC9kq0PSkoM2Pn7Lx7dgyp4vT7ccJ8a3IwHbe+5KJRnpw==} - engines: {node: '>=18'} - - '@react-native/js-polyfills@0.85.3': - resolution: {integrity: sha512-U2+aMshIXf1uFn77tpBb/xhHWB9vkVrMpt7kkucAugF8hJKYTDGB587X7WwelHduK2KBfhl4giSv0rzZGoef9A==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - '@react-native/metro-babel-transformer@0.74.87': - resolution: {integrity: sha512-UsJCO24sNax2NSPBmV1zLEVVNkS88kcgAiYrZHtYSwSjpl4WZ656tIeedBfiySdJ94Hr3kQmBYLipV5zk0NI1A==} - engines: {node: '>=18'} - peerDependencies: - '@babel/core': '*' - - '@react-native/normalize-color@2.1.0': - resolution: {integrity: sha512-Z1jQI2NpdFJCVgpY+8Dq/Bt3d+YUi1928Q+/CZm/oh66fzM0RUl54vvuXlPJKybH4pdCZey1eDTPaLHkMPNgWA==} - - '@react-native/normalize-colors@0.74.85': - resolution: {integrity: sha512-pcE4i0X7y3hsAE0SpIl7t6dUc0B0NZLd1yv7ssm4FrLhWG+CGyIq4eFDXpmPU1XHmL5PPySxTAjEMiwv6tAmOw==} - - '@react-native/normalize-colors@0.74.87': - resolution: {integrity: sha512-Xh7Nyk/MPefkb0Itl5Z+3oOobeG9lfLb7ZOY2DKpFnoCE1TzBmib9vMNdFaLdSxLIP+Ec6icgKtdzYg8QUPYzA==} - - '@react-native/normalize-colors@0.83.6': - resolution: {integrity: sha512-bTM24b5v4qN3h52oflnv+OujFORn/kVi06WaWhnQQw14/ycilPqIsqsa+DpIBqdBrXxvLa9fXtCRrQtGATZCEw==} - - '@react-native/normalize-colors@0.85.3': - resolution: {integrity: sha512-hj0PScZEhIbcOvQV5yMKX3ha4XEIOy/SVE1Rrpp0beW0dpNLOgSC7KDxGewmDnIHK9YdQUXGY9eMEfShUMIaZw==} - - '@react-native/virtualized-lists@0.74.87': - resolution: {integrity: sha512-lsGxoFMb0lyK/MiplNKJpD+A1EoEUumkLrCjH4Ht+ZlG8S0BfCxmskLZ6qXn3BiDSkLjfjI/qyZ3pnxNBvkXpQ==} - engines: {node: '>=18'} - peerDependencies: - '@types/react': ^18.2.6 - react: '*' - react-native: '*' - peerDependenciesMeta: - '@types/react': - optional: true - - '@react-native/virtualized-lists@0.85.3': - resolution: {integrity: sha512-dsCjI//OIPEUJMyNHp4l7zNLVjCx7bcaRUceOCkU+IB17hkbtbGWvi7HjGFSzy7FJGmS/MOlcfpb72xXiy1Oig==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - peerDependencies: - '@types/react': ^19.2.0 - react: '*' - react-native: 0.85.3 - peerDependenciesMeta: - '@types/react': - optional: true - - '@react-navigation/bottom-tabs@6.6.1': - resolution: {integrity: sha512-9oD4cypEBjPuaMiu9tevWGiQ4w/d6l3HNhcJ1IjXZ24xvYDSs0mqjUcdt8SWUolCvRrYc/DmNBLlT83bk0bHTw==} - peerDependencies: - '@react-navigation/native': ^6.0.0 - react: '*' - react-native: '*' - react-native-safe-area-context: '>= 3.0.0' - react-native-screens: '>= 3.0.0' - - '@react-navigation/core@6.4.17': - resolution: {integrity: sha512-Nd76EpomzChWAosGqWOYE3ItayhDzIEzzZsT7PfGcRFDgW5miHV2t4MZcq9YIK4tzxZjVVpYbIynOOQQd1e0Cg==} - peerDependencies: - react: '*' - - '@react-navigation/elements@1.3.31': - resolution: {integrity: sha512-bUzP4Awlljx5RKEExw8WYtif8EuQni2glDaieYROKTnaxsu9kEIA515sXQgUDZU4Ob12VoL7+z70uO3qrlfXcQ==} - peerDependencies: - '@react-navigation/native': ^6.0.0 - react: '*' - react-native: '*' - react-native-safe-area-context: '>= 3.0.0' - - '@react-navigation/native@6.1.18': - resolution: {integrity: sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==} - peerDependencies: - react: '*' - react-native: '*' - - '@react-navigation/routers@6.1.9': - resolution: {integrity: sha512-lTM8gSFHSfkJvQkxacGM6VJtBt61ip2XO54aNfswD+KMw6eeZ4oehl7m0me3CR9hnDE4+60iAZR8sAhvCiI3NA==} - - '@react-navigation/stack@6.4.1': - resolution: {integrity: sha512-upMEHOKMtuMu4c9gmoPlO/JqI6mDlSqwXg1aXKOTQLXAF8H5koOLRfrmi7AkdiE9A7lDXWUAZoGuD9O88cYvDQ==} - peerDependencies: - '@react-navigation/native': ^6.0.0 - react: '*' - react-native: '*' - react-native-gesture-handler: '>= 1.0.0' - react-native-safe-area-context: '>= 3.0.0' - react-native-screens: '>= 3.0.0' - - '@rnx-kit/chromium-edge-launcher@1.0.0': - resolution: {integrity: sha512-lzD84av1ZQhYUS+jsGqJiCMaJO2dn9u+RTT9n9q6D3SaKVwWqv+7AoRKqBu19bkwyE+iFRl1ymr40QS90jVFYg==} - engines: {node: '>=14.15'} - '@rollup/plugin-alias@6.0.0': resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==} engines: {node: '>=20.19.0'} @@ -4346,41 +1027,6 @@ packages: cpu: [x64] os: [win32] - '@segment/analytics-core@1.4.1': - resolution: {integrity: sha512-kV0Pf33HnthuBOVdYNani21kYyj118Fn+9757bxqoksiXoZlYvBsFq6giNdCsKcTIE1eAMqNDq3xE1VQ0cfsHA==} - - '@segment/analytics-generic-utils@1.1.1': - resolution: {integrity: sha512-THTIzBPHnvu1HYJU3fARdJ3qIkukO3zDXsmDm+kAeUks5R9CBXOQ6rPChiASVzSmwAIIo5uFIXXnCraojlq/Gw==} - - '@segment/analytics-node@1.3.0': - resolution: {integrity: sha512-lRLz1WZaDokMoUe299yP5JkInc3OgJuqNNlxb6j0q22umCiq6b5iDo2gRmFn93reirIvJxWIicQsGrHd93q8GQ==} - engines: {node: '>=14'} - - '@segment/loosely-validate-event@2.0.0': - resolution: {integrity: sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==} - - '@selderee/plugin-htmlparser2@0.11.0': - resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - - '@sentry/core@8.55.2': - resolution: {integrity: sha512-YlEBwybUcOQ/KjMHDmof1vwweVnBtBxYlQp7DE3fOdtW4pqqdHWTnTntQs4VgYfxzjJYgtkd9LHlGtg8qy+JVQ==} - engines: {node: '>=14.18'} - - '@sentry/node@8.55.2': - resolution: {integrity: sha512-x3Whryb4TytiIhH9ABLVuASfBvwA50v6PpJYvq0Y9dUMi9Eb0cfuqvRCB3e+oVntZHQpnXor2U/gRBIdG2jp4w==} - engines: {node: '>=14.18'} - - '@sentry/opentelemetry@8.55.2': - resolution: {integrity: sha512-pbhXi4cS1W4l392yEfIx3UD28OYAl9JkYOmh/Cpm6cPTtRMPxi3hWeujGbcXV9T/RkWYjqd+JdUDJjqsWSww9A==} - engines: {node: '>=14.18'} - peerDependencies: - '@opentelemetry/api': ^1.9.0 - '@opentelemetry/context-async-hooks': ^1.30.1 - '@opentelemetry/core': ^1.30.1 - '@opentelemetry/instrumentation': ^0.57.1 - '@opentelemetry/sdk-trace-base': ^1.30.1 - '@opentelemetry/semantic-conventions': ^1.28.0 - '@shikijs/core@1.29.2': resolution: {integrity: sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==} @@ -4402,18 +1048,6 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@sideway/address@4.1.5': - resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} - - '@sideway/formula@3.0.1': - resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==} - - '@sideway/pinpoint@2.0.0': - resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==} - - '@sinclair/typebox@0.27.10': - resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} - '@sindresorhus/is@7.2.0': resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} engines: {node: '>=18'} @@ -4422,62 +1056,11 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@10.3.0': - resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - - '@smithy/core@3.24.2': - resolution: {integrity: sha512-IKS7qX59fAGCYBmt5JChcDswQDupZqT2Yn2ZBA3UgTlsjRNNkQzZobbn95xoAAdtTyJmBiJB3Y02qR3rgy3Zog==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.3.2': - resolution: {integrity: sha512-iYr9ekBjmZ+FwkiHEopqGscBbl78X62cq3p5Dd0eC+gNd7fybNZFQQdDuOQjTVmFymleuA8YRWZnuXWZ8B3kKA==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.4.2': - resolution: {integrity: sha512-3wF40g8OOCA5BnwQUvwtzZqYBbWWftDjpAlWIUo6Yld3ZzJaMAKqg7MWQBPjE8oLaqvZQUE7tVGlZPsae6A4bQ==} - engines: {node: '>=18.0.0'} - - '@smithy/is-array-buffer@2.2.0': - resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} - engines: {node: '>=14.0.0'} - - '@smithy/middleware-compression@4.4.2': - resolution: {integrity: sha512-9zuwsXfOKWS3uUbHimLRM7ScHLU3/aL1kWTr17i+K0W7kflbBF3Zbed2lL/QPhzCBh1FSJGoKlrRn2ueI9rCjw==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.7.2': - resolution: {integrity: sha512-EdksTZ8UXYxGUgQ4mpIKrHoaj9WVGsp66TpZuixLAz1Jex8YDLnS4RH9ktGED5aOpN0OJlEtrsC9IGt76go1eA==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.4.2': - resolution: {integrity: sha512-1km1OjdLRFuITWpCPofjFqzZ+tbeWuB72ZhcYjbjkCxZ21tTPfIs4GUxRrelMyKMLxLghGD58RENnXorU/O8cw==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.14.1': - resolution: {integrity: sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-buffer-from@2.2.0': - resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} - engines: {node: '>=14.0.0'} - - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} - '@solidjs/meta@0.29.4': resolution: {integrity: sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g==} peerDependencies: solid-js: '>=1.8.4' - '@solidjs/router@0.14.10': - resolution: {integrity: sha512-5B8LVgvvXijfXyXWPVLUm7RQ05BhjIpAyRkYVDZtrR3OaSvftXobWc6qSEwk4ICLoGi/IE9CUp2LUdCBIs9AXg==} - peerDependencies: - solid-js: ^1.8.6 - '@solidjs/router@0.15.4': resolution: {integrity: sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ==} peerDependencies: @@ -4500,11 +1083,95 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@swc/helpers@0.3.17': - resolution: {integrity: sha512-tb7Iu+oZ+zWJZ3HJqwx8oNwSDIU440hmVMDPhpACWQWnrZHK99Bxs70gT1L2dnr5Hg50ZRWEFkQCAnOVVV0z1Q==} + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} - '@swc/helpers@0.5.15': - resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 '@tanstack/directive-functions-plugin@1.134.5': resolution: {integrity: sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw==} @@ -4520,10 +1187,6 @@ packages: resolution: {integrity: sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw==} engines: {node: '>=12'} - '@tootallnate/once@2.0.1': - resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} - engines: {node: '>= 10'} - '@trpc/client@10.45.4': resolution: {integrity: sha512-ItpH5FMIJFt862kbAgC8hf5NJnDLo0FwEvMEX6zSLCenzWD0UH6H/fvAX1suFo1e0wcAmMhiEU1Tl6xKk/Pd1g==} peerDependencies: @@ -4532,51 +1195,36 @@ packages: '@trpc/server@10.45.4': resolution: {integrity: sha512-5MuyK5sDuFVGHOT9EMVHgRjP5I5j3RqGymcb5D47UOp8tzn6LH0g1lEgYrAsOZ12vmk1gpxMw/v/y4xHHK/Qdg==} - '@tsconfig/node10@1.0.12': - resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} - - '@tsconfig/node12@1.0.11': - resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} - - '@tsconfig/node14@1.0.3': - resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} - - '@tsconfig/node16@1.0.4': - resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} - - '@turbo/darwin-64@2.9.12': - resolution: {integrity: sha512-eu3eFRmE9NjgZ0wPdRJ44l+LGSeIky+tz5ZQd8zQkw/Yqi+BM7wq+8nbabeoiVUcICi/IZweMOKl/MCmkrd1+g==} + '@turbo/darwin-64@2.9.14': + resolution: {integrity: sha512-t7QiPflaEyBE4oayeZtSmu4mEfjgIrcNlNNl1z1dmIVPqEdtA7+CfTf8d7KXsOGPh6aNgWjKxyvQg9uGfDQF+A==} cpu: [x64] os: [darwin] - '@turbo/darwin-arm64@2.9.12': - resolution: {integrity: sha512-RUkAE404z/J8NsyrUosMcBaXT6M4bRFxTQrmkDQBLQVXaC8Jl0e9bMvYDSX0GW7Ffm2m3j9y7RXgR1foeUAM9w==} + '@turbo/darwin-arm64@2.9.14': + resolution: {integrity: sha512-d23147mC9BsCPA9mJ0h/ubcpbRgcJBXbcG3+Vq7YLhjz3IXuvQsJ1UXH8f4MD76ZjJ4m/E4aRdJV+MW88CDfbw==} cpu: [arm64] os: [darwin] - '@turbo/linux-64@2.9.12': - resolution: {integrity: sha512-InIUtH7cw/vqXNX1Gr7QgWfmw3ct08pV5CpfdEOR48z2u2rzdmpIuk00B/Q2xCb0PMWtKgiMQynfuphmEuUyTQ==} + '@turbo/linux-64@2.9.14': + resolution: {integrity: sha512-P3ZKB5tuUDdDQWuAsACGUR1qv9W7BNWxdxqVJ0kZNuNNPRaVYTPPikLcp79+GiEcW3npsR+KyP38lnQiBc5aSA==} cpu: [x64] os: [linux] - '@turbo/linux-arm64@2.9.12': - resolution: {integrity: sha512-lC6nD//Xh67fmJM0LKaLsg74Wry0aYrgMklpiNgCbUaMdPIOqj0A00iri3NU7Lb7pZHx8ViisgpeDKlpSgFUCA==} + '@turbo/linux-arm64@2.9.14': + resolution: {integrity: sha512-ZRTlzcUMrrPv9ZuDzRF9n60Ym13bKeG9jDB8WjxyLhWNzV+AJQN+zdpIk3NJYf2zQsGUm1mNar2P0elRzLw25g==} cpu: [arm64] os: [linux] - '@turbo/windows-64@2.9.12': - resolution: {integrity: sha512-conYri8VUl72JOdYnLDPYwzqbPcY5ECoHmo9FWoKznemhaAIilj4maHqs9Uar0aKfNoZIULniy+6iWaLtLO34A==} + '@turbo/windows-64@2.9.14': + resolution: {integrity: sha512-exanwN6sIduZwykYeiTQj8kCmOhazP5WOz3bvXMcYtjhL6Z3iRWLewKrXCBq0bqwSP3iBMb/AerRCnHI4lx46A==} cpu: [x64] os: [win32] - '@turbo/windows-arm64@2.9.12': - resolution: {integrity: sha512-XoR4bsg62/L/esRVcmoMESEiNZ36+YmyjYGLpoqk8nwMgXzzVjNOgX0lRSz5w/U/ajLGv3nhMsS0Q2QOdvp2AQ==} + '@turbo/windows-arm64@2.9.14': + resolution: {integrity: sha512-fVdCsnmYoKICsycbWuuGp6Jvi51/3G/UluFWuAUCvR8PIW5IJkAk5BM9UF8PSm0Q2IphWHFZjYEgjHsh3B9y/g==} cpu: [arm64] os: [win32] - '@tybys/wasm-util@0.10.2': - resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -4589,27 +1237,12 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/braces@3.0.5': resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} - '@types/caseless@0.12.5': - resolution: {integrity: sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/chrome@0.0.268': - resolution: {integrity: sha512-7N1QH9buudSJ7sI8Pe4mBHJr5oZ48s0hcanI9w3wgijAlv1OZNUZve9JR4x42dn5lJ5Sm87V1JNfnoh10EnQlA==} - - '@types/connect@3.4.36': - resolution: {integrity: sha512-P63Zd/JUGq+PdrM1lv0Wv5SBYeA2+CORvbrXbngriYY0jzLUWfQMQQxOhjONEz/wlHOAxOdY7CY65rgQdTjq2w==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -4619,164 +1252,24 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/express-serve-static-core@4.19.8': - resolution: {integrity: sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==} - - '@types/express@4.17.25': - resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} - - '@types/filesystem@0.0.36': - resolution: {integrity: sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==} - - '@types/filewriter@0.0.33': - resolution: {integrity: sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g==} - - '@types/graceful-fs@4.1.9': - resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - - '@types/hammerjs@2.0.46': - resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} - - '@types/handlebars@4.1.0': - resolution: {integrity: sha512-gq9YweFKNNB1uFK71eRqsd4niVkXrxHugqWFQkeLRJvGjnxsLr16bYtcsG4tOFwmYi0Bax+wCkbf1reUfdl4kA==} - deprecated: This is a stub types definition. handlebars provides its own type definitions, so you do not need this installed. - - '@types/har-format@1.2.16': - resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - - '@types/istanbul-lib-coverage@2.0.6': - resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} - - '@types/istanbul-lib-report@3.0.3': - resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} - - '@types/istanbul-reports@1.1.2': - resolution: {integrity: sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==} - - '@types/istanbul-reports@3.0.4': - resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} - - '@types/jest@29.5.14': - resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} - - '@types/jsonwebtoken@9.0.10': - resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} - - '@types/long@4.0.2': - resolution: {integrity: sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} '@types/micromatch@4.0.10': resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==} - '@types/mime@1.3.5': - resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} - - '@types/ms@2.1.0': - resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - - '@types/mysql@2.15.26': - resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} - - '@types/node-forge@1.3.14': - resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} - - '@types/node@18.19.130': - resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - - '@types/node@20.19.41': - resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} - - '@types/node@22.19.19': - resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} - - '@types/node@25.8.0': - resolution: {integrity: sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==} - - '@types/pdfkit@0.13.9': - resolution: {integrity: sha512-RDG8Yb1zT7I01FfpwK7nMSA433XWpblMqSCtA5vJlSyavWZb303HUYPCel6JTiDDFqwGLvtAnYbH8N/e0Cb89g==} - - '@types/pg-pool@2.0.6': - resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} - - '@types/pg@8.6.1': - resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} - - '@types/prop-types@15.7.15': - resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} - - '@types/qs@6.15.1': - resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - - '@types/react-test-renderer@18.3.1': - resolution: {integrity: sha512-vAhnk0tG2eGa37lkU9+s5SoroCsRI08xnsWFiAXOuPH2jqzMbcXvKExXViPi1P5fIklDeCvXqyrdmipFaSkZrA==} - - '@types/react@18.3.28': - resolution: {integrity: sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==} - - '@types/request@2.48.13': - resolution: {integrity: sha512-FGJ6udDNUCjd19pp0Q3iTiDkwhYup7J8hpMW9c4k53NrccQFFWKRho6hvtPPEhnXWKvukfwAlB6DbDz4yhH5Gg==} + '@types/node@25.9.1': + resolution: {integrity: sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/send@0.17.6': - resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} - - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - - '@types/serve-static@1.15.10': - resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} - - '@types/shimmer@1.2.0': - resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} - - '@types/stack-utils@2.0.3': - resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} - - '@types/tedious@4.0.14': - resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} - - '@types/tough-cookie@4.0.5': - resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} - '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/uuid@10.0.0': - resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} - - '@types/uuid@11.0.0': - resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==} - deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed. - - '@types/ws@8.18.1': - resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - - '@types/yargs-parser@21.0.3': - resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} - - '@types/yargs@13.0.12': - resolution: {integrity: sha512-qCxJE1qgz2y0hA4pIxjBR+PelCH0U5CK1XJXFwCNqfmliatKp47UCXXE9Dyk1OXBDLvsCF57TqQEJaeLfDYEOQ==} - - '@types/yargs@15.0.20': - resolution: {integrity: sha512-KIkX+/GgfFitlASYCGoSF+T4XRXhOubJLhkLVtSfsRTe9jWMmuM2g28zQ41BtPTG7TRBb2xHW+LCNVE9QR/vsg==} - - '@types/yargs@17.0.35': - resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@typeschema/core@0.13.2': resolution: {integrity: sha512-pAt0MK249/9szYaoPuvzhSfOd3smrLhhwCCpUNB4onX32mRx5F3lzDIveIYGQkLYRq58xOX5sjoW+n72f/MLLw==} peerDependencies: @@ -4799,35 +1292,25 @@ packages: '@ungap/structured-clone@1.3.1': resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} - '@urql/core@2.3.6': - resolution: {integrity: sha512-PUxhtBh7/8167HJK6WqBv6Z0piuiaZHQGYbhwpNL9aIQmLROPEdaUYkY4wh45wPQXcTpnd11l0q3Pw+TI11pdw==} - peerDependencies: - graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - '@urql/exchange-retry@0.3.0': - resolution: {integrity: sha512-hHqer2mcdVC0eYnVNbWyi28AlGOPb2vjH3lP3/Bc8Lc8BjhMsDwFMm7WhoP5C1+cfbr/QJ6Er3H/L08wznXxfg==} - peerDependencies: - graphql: ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 - '@vercel/nft@1.5.0': resolution: {integrity: sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==} engines: {node: '>=20'} hasBin: true - '@vitest/coverage-v8@4.1.6': - resolution: {integrity: sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==} + '@vitest/coverage-v8@4.1.7': + resolution: {integrity: sha512-qsYPeXc5Q9dFLd1i8Ap+Bx8sQgcp+rFVQo4R0dDsWNBzl26ldVF1qOO+RL24K7FDrR6pA+50XedRLSoSG24bVQ==} peerDependencies: - '@vitest/browser': 4.1.6 - vitest: 4.1.6 + '@vitest/browser': 4.1.7 + vitest: 4.1.7 peerDependenciesMeta: '@vitest/browser': optional: true - '@vitest/expect@4.1.6': - resolution: {integrity: sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==} + '@vitest/expect@4.1.7': + resolution: {integrity: sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==} - '@vitest/mocker@4.1.6': - resolution: {integrity: sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==} + '@vitest/mocker@4.1.7': + resolution: {integrity: sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -4837,37 +1320,20 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.6': - resolution: {integrity: sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==} + '@vitest/pretty-format@4.1.7': + resolution: {integrity: sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==} - '@vitest/runner@4.1.6': - resolution: {integrity: sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==} + '@vitest/runner@4.1.7': + resolution: {integrity: sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==} - '@vitest/snapshot@4.1.6': - resolution: {integrity: sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==} + '@vitest/snapshot@4.1.7': + resolution: {integrity: sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==} - '@vitest/spy@4.1.6': - resolution: {integrity: sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==} + '@vitest/spy@4.1.7': + resolution: {integrity: sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==} - '@vitest/utils@4.1.6': - resolution: {integrity: sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==} - - '@xmldom/xmldom@0.7.13': - resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} - engines: {node: '>=10.0.0'} - deprecated: this version has critical issues, please update to the latest version - - '@xmldom/xmldom@0.8.13': - resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} - engines: {node: '>=10.0.0'} - - '@xmldom/xmldom@0.9.10': - resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} - engines: {node: '>=14.6'} - - abbrev@2.0.0: - resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + '@vitest/utils@4.1.7': + resolution: {integrity: sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==} abbrev@3.0.1: resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} @@ -4877,76 +1343,20 @@ packages: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} - abstract-logging@2.0.1: - resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} - - accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} - - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - acorn-import-attributes@1.9.5: resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} peerDependencies: acorn: ^8 - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn-walk@8.3.5: - resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} - engines: {node: '>=0.4.0'} - acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} - aggregate-error@3.1.0: - resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} - engines: {node: '>=8'} - - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - - ajv@6.15.0: - resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} - - ajv@8.20.0: - resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} - - anser@1.4.10: - resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} - - ansi-escapes@4.3.2: - resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} - engines: {node: '>=8'} - - ansi-fragments@0.2.1: - resolution: {integrity: sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w==} - - ansi-regex@4.1.1: - resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} - engines: {node: '>=6'} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -4955,18 +1365,10 @@ packages: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@5.2.0: - resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} - engines: {node: '>=10'} - ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -4975,16 +1377,10 @@ packages: resolution: {integrity: sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg==} engines: {node: '>=14'} - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - appdirsjs@1.2.7: - resolution: {integrity: sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw==} - archiver-utils@5.0.2: resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} engines: {node: '>= 14'} @@ -4993,90 +1389,12 @@ packages: resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} engines: {node: '>= 14'} - arg@4.1.0: - resolution: {integrity: sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==} - - arg@4.1.3: - resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - array-buffer-byte-length@1.0.2: - resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} - engines: {node: '>= 0.4'} - - array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} - - array-includes@3.1.9: - resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} - engines: {node: '>= 0.4'} - - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - - array.prototype.findlast@1.2.5: - resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} - engines: {node: '>= 0.4'} - - array.prototype.flat@1.3.3: - resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} - engines: {node: '>= 0.4'} - - array.prototype.flatmap@1.3.3: - resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} - engines: {node: '>= 0.4'} - - array.prototype.tosorted@1.1.4: - resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} - engines: {node: '>= 0.4'} - - arraybuffer.prototype.slice@1.0.4: - resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} - engines: {node: '>= 0.4'} - - arrify@2.0.1: - resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} - engines: {node: '>=8'} - - asap@2.0.6: - resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - - assert@2.1.0: - resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} - assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-types@0.15.2: - resolution: {integrity: sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==} - engines: {node: '>=4'} - - ast-v8-to-istanbul@1.0.0: - resolution: {integrity: sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==} - - astral-regex@1.0.0: - resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==} - engines: {node: '>=4'} - - async-function@1.0.0: - resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} - engines: {node: '>= 0.4'} - - async-limiter@1.0.1: - resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==} - - async-retry@1.3.3: - resolution: {integrity: sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==} + ast-v8-to-istanbul@1.0.2: + resolution: {integrity: sha512-dKmJxJsGItLmc5CYZKuEjuG6GnBs6PG4gohMhyFOWKaNQoYCuRZJDECaBlHmcG0lv2wc2E0uU8lESmBEumC3DQ==} async-sema@3.1.1: resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} @@ -5084,27 +1402,6 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - - at-least-node@1.0.0: - resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} - engines: {node: '>= 4.0.0'} - - atomic-sleep@1.0.0: - resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} - engines: {node: '>=8.0.0'} - - available-typed-arrays@1.0.7: - resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} - engines: {node: '>= 0.4'} - - avvio@9.2.0: - resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} - - axios@1.16.1: - resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} - b4a@1.8.1: resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} peerDependencies: @@ -5113,106 +1410,14 @@ packages: react-native-b4a: optional: true - babel-core@7.0.0-bridge.0: - resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} - peerDependencies: - '@babel/core': ^7.0.0-0 - babel-dead-code-elimination@1.0.12: resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} - babel-jest@29.7.0: - resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.8.0 - - babel-plugin-istanbul@6.1.1: - resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} - engines: {node: '>=8'} - - babel-plugin-jest-hoist@29.6.3: - resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - babel-plugin-jsx-dom-expressions@0.40.6: - resolution: {integrity: sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ==} + babel-plugin-jsx-dom-expressions@0.40.7: + resolution: {integrity: sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==} peerDependencies: '@babel/core': ^7.20.12 - babel-plugin-polyfill-corejs2@0.4.17: - resolution: {integrity: sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-polyfill-corejs3@0.13.0: - resolution: {integrity: sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-polyfill-corejs3@0.14.2: - resolution: {integrity: sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-polyfill-regenerator@0.6.8: - resolution: {integrity: sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==} - peerDependencies: - '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 - - babel-plugin-react-compiler@0.0.0-experimental-592953e-20240517: - resolution: {integrity: sha512-OjG1SVaeQZaJrqkMFJatg8W/MTow8Ak5rx2SI0ETQBO1XvOk/XZGMbltNCPdFJLKghBYoBjC+Y3Ap/Xr7B01mA==} - - babel-plugin-react-compiler@1.0.0: - resolution: {integrity: sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==} - - babel-plugin-react-native-web@0.19.13: - resolution: {integrity: sha512-4hHoto6xaN23LCyZgL9LJZc3olmAxd7b6jDzlZnKXAh4rRAbZRKNBJoOOdp46OBqgy+K0t0guTj5/mhA8inymQ==} - - babel-plugin-react-native-web@0.21.2: - resolution: {integrity: sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==} - - babel-plugin-syntax-hermes-parser@0.32.0: - resolution: {integrity: sha512-m5HthL++AbyeEA2FcdwOLfVFvWYECOBObLHNqdR8ceY4TsEdn4LdX2oTvbB2QJSSElE2AWA/b2MXZ/PF/CqLZg==} - - babel-plugin-syntax-hermes-parser@0.32.1: - resolution: {integrity: sha512-HgErPZTghW76Rkq9uqn5ESeiD97FbqpZ1V170T1RG2RDp+7pJVQV2pQJs7y5YzN0/gcT6GM5ci9apRnIwuyPdQ==} - - babel-plugin-syntax-hermes-parser@0.33.3: - resolution: {integrity: sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA==} - - babel-plugin-transform-flow-enums@0.0.2: - resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==} - - babel-preset-current-node-syntax@1.2.0: - resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} - peerDependencies: - '@babel/core': ^7.0.0 || ^8.0.0-0 - - babel-preset-expo@11.0.15: - resolution: {integrity: sha512-rgiMTYwqIPULaO7iZdqyL7aAff9QLOX6OWUtLZBlOrOTreGY1yHah/5+l8MvI6NVc/8Zj5LY4Y5uMSnJIuzTLw==} - - babel-preset-expo@55.0.21: - resolution: {integrity: sha512-anXoUZBcxydLdVs2L+r3bWKGUvZv2FtgOl8xRJ12i/YfKICBpwTGZWSTiEYTqBByZ6GkA3mE9+3TW97X2ocFTQ==} - peerDependencies: - '@babel/runtime': ^7.20.0 - expo: '*' - expo-widgets: ^55.0.17 - react-refresh: '>=0.14.0 <1.0.0' - peerDependenciesMeta: - '@babel/runtime': - optional: true - expo: - optional: true - expo-widgets: - optional: true - - babel-preset-jest@29.6.3: - resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@babel/core': ^7.0.0 - babel-preset-solid@1.9.12: resolution: {integrity: sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==} peerDependencies: @@ -5222,9 +1427,6 @@ packages: solid-js: optional: true - badgin@1.2.3: - resolution: {integrity: sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -5273,64 +1475,19 @@ packages: bare-url@2.4.3: resolution: {integrity: sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==} - base64-js@0.0.8: - resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} - engines: {node: '>= 0.4'} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.29: - resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==} + baseline-browser-mapping@2.10.32: + resolution: {integrity: sha512-wbPvpyjJPC0zdfdKXxqEL3Ea+bOMD/87X4lftiJkkaBiuG6ALQy1SLmEd7BSmVCuwCQsBrCamgBoLyfFDD1EPg==} engines: {node: '>=6.0.0'} hasBin: true - better-opn@3.0.2: - resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} - engines: {node: '>=12.0.0'} - - big-integer@1.6.52: - resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} - engines: {node: '>=0.6'} - - bignumber.js@9.3.1: - resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - - body-parser@1.20.5: - resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - boolbase@1.0.0: - resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - - bowser@2.14.1: - resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} - - bplist-creator@0.0.7: - resolution: {integrity: sha512-xp/tcaV3T5PCiaY04mXga7o/TE+t95gqeLmADeBI1CvZtdWTbgBt3uLpvh4UWtenKeBhCV6oVxGk38yZr2uYEA==} - - bplist-creator@0.1.0: - resolution: {integrity: sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==} - - bplist-parser@0.3.1: - resolution: {integrity: sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==} - engines: {node: '>= 5.10.0'} - - bplist-parser@0.3.2: - resolution: {integrity: sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==} - engines: {node: '>= 5.10.0'} - - brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - - brace-expansion@2.1.0: - resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==} + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} brace-expansion@5.0.6: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} @@ -5340,72 +1497,25 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - brotli@1.3.3: - resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} - - browserify-zlib@0.2.0: - resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} - browserslist@4.28.2: resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true - bs-logger@0.2.6: - resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} - engines: {node: '>= 6'} - - bser@2.1.1: - resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - - buffer-alloc-unsafe@1.1.0: - resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} - - buffer-alloc@1.2.0: - resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} - buffer-crc32@1.0.0: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - - buffer-fill@1.0.0: - resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - builtins@1.0.3: - resolution: {integrity: sha512-uYBjakWipfaO/bXI7E8rq6kpwHRZK5cNYrUv2OzZSI/FvmdMyXJ2tG9dKcjEC5YHmHpUAwsargWIZNWdxb/bnQ==} - - bullmq@5.76.8: - resolution: {integrity: sha512-v3WTwA8diFtsADaJ8eK2ozyi2CYK9rDZCeoKF+dIPF/MUL8HxAOa3H72Gmu1lC4yKlho6t1PwNr/QpDVqaNEZQ==} - engines: {node: '>=12.22.0'} - bundle-name@4.1.0: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - c12@3.1.0: - resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} - peerDependencies: - magicast: ^0.3.5 - peerDependenciesMeta: - magicast: - optional: true - c12@3.3.4: resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} peerDependencies: @@ -5414,54 +1524,8 @@ packages: magicast: optional: true - cacache@18.0.4: - resolution: {integrity: sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==} - engines: {node: ^16.14.0 || >=18.0.0} - - cache-manager@6.4.3: - resolution: {integrity: sha512-VV5eq/QQ5rIVix7/aICO4JyvSeEv9eIQuKL5iFwgM2BrcYoE0A/D1mNsAHJAsB0WEbNdBlKkn6Tjz6fKzh/cKQ==} - - cacheable@1.10.4: - resolution: {integrity: sha512-Gd7ccIUkZ9TE2odLQVS+PDjIvQCdJKUlLdJRVvZu0aipj07Qfx+XIej7hhDrKGGoIxV5m5fT/kOJNJPQhQneRg==} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bind@1.0.9: - resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - - caller-callsite@2.0.0: - resolution: {integrity: sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==} - engines: {node: '>=4'} - - caller-path@2.0.0: - resolution: {integrity: sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==} - engines: {node: '>=4'} - - callsites@2.0.0: - resolution: {integrity: sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==} - engines: {node: '>=4'} - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - camelcase@5.3.1: - resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} - engines: {node: '>=6'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - caniuse-lite@1.0.30001792: - resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -5470,239 +1534,67 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - char-regex@1.0.2: - resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} - engines: {node: '>=10'} - character-entities-html4@2.1.0: resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - charenc@0.0.2: - resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} - chownr@2.0.0: - resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} - engines: {node: '>=10'} - chownr@3.0.0: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} - chrome-launcher@0.15.2: - resolution: {integrity: sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==} - engines: {node: '>=12.13.0'} - hasBin: true - - chromium-edge-launcher@0.2.0: - resolution: {integrity: sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==} - - chromium-edge-launcher@0.3.0: - resolution: {integrity: sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA==} - - ci-info@2.0.0: - resolution: {integrity: sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==} - - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} - citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} citty@0.2.2: resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} - - cjs-module-lexer@2.2.0: - resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} - - clean-stack@2.2.0: - resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} - engines: {node: '>=6'} - - cli-cursor@2.1.0: - resolution: {integrity: sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==} - engines: {node: '>=4'} - - cli-cursor@3.1.0: - resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} - engines: {node: '>=8'} - - cli-spinners@2.9.2: - resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} - engines: {node: '>=6'} - - client-only@0.0.1: - resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - - cliui@6.0.0: - resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} - clone-deep@4.0.1: - resolution: {integrity: sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==} - engines: {node: '>=6'} - - clone@1.0.4: - resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} - engines: {node: '>=0.8'} - - clone@2.1.2: - resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} - engines: {node: '>=0.8'} - cluster-key-slot@1.1.2: resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} engines: {node: '>=0.10.0'} - co@4.6.0: - resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - collect-v8-coverage@1.0.3: - resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} - - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - - colorette@1.4.0: - resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} - - combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} - comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - command-exists@1.2.9: - resolution: {integrity: sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==} - - commander@10.0.1: - resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} - engines: {node: '>=14'} - - commander@12.1.0: - resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} - engines: {node: '>=18'} - commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - - commander@7.2.0: - resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} - engines: {node: '>= 10'} - - commander@9.5.0: - resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} - engines: {node: ^12.20.0 || >=14} - commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} compatx@0.2.0: resolution: {integrity: sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA==} - component-type@1.2.2: - resolution: {integrity: sha512-99VUHREHiN5cLeHm3YLq312p6v+HUEcwtLCAtelvUDI6+SH5g5Cr85oNR2S1o6ywzL0ykMbuwLzM2ANocjEOIA==} - compress-commons@6.0.2: resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} engines: {node: '>= 14'} - compressible@2.0.18: - resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} - engines: {node: '>= 0.6'} - - compression@1.8.1: - resolution: {integrity: sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==} - engines: {node: '>= 0.8.0'} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} confbox@0.2.4: resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} - config-chain@1.1.13: - resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} - - connect@3.7.0: - resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} - engines: {node: '>= 0.10.0'} - consola@3.4.2: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} - content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} - - content-disposition@1.1.0: - resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - content-type@2.0.0: - resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} - engines: {node: '>=18'} - convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -5715,27 +1607,9 @@ packages: cookie-es@3.1.1: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} - cookie-signature@1.0.7: - resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} - - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cookie@1.1.1: - resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} - engines: {node: '>=18'} - - core-js-compat@3.49.0: - resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} - core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - cosmiconfig@5.2.1: - resolution: {integrity: sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==} - engines: {node: '>=4'} - crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -5745,29 +1619,10 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} - create-jest@29.7.0: - resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - - create-require@1.1.1: - resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} - - cron-parser@4.9.0: - resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} - engines: {node: '>=12.0.0'} - croner@10.0.1: resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} engines: {node: '>=18.0'} - cross-fetch@3.2.0: - resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} - - cross-spawn@6.0.6: - resolution: {integrity: sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==} - engines: {node: '>=4.8'} - cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -5775,52 +1630,9 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - crypt@0.0.2: - resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==} - - crypto-js@4.2.0: - resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} - - crypto-random-string@1.0.0: - resolution: {integrity: sha512-GsVpkFPlycH7/fRR7Dhcmnoii54gV1nz7y4CWyeFS14N+JVBBhY+r8amRHE4BwSYal7BPTDp8isvAlCxyFt3Hg==} - engines: {node: '>=4'} - - crypto-random-string@2.0.0: - resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} - engines: {node: '>=8'} - - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} - - css-tree@1.1.3: - resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} - engines: {node: '>=8.0.0'} - - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} - engines: {node: '>= 6'} - csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - dag-map@1.0.2: - resolution: {integrity: sha512-+LSAiGFwQ9dRnRdOeaj7g47ZFJcOUPukAP8J3A3fuZ1g9Y44BG+P1sgApjLXTQPOzC4+7S9Wr8kXsfpINM4jpw==} - - data-view-buffer@1.0.2: - resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} - engines: {node: '>= 0.4'} - - data-view-byte-length@1.0.2: - resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} - engines: {node: '>= 0.4'} - - data-view-byte-offset@1.0.1: - resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} - engines: {node: '>= 0.4'} - - dayjs@1.11.20: - resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} - db0@0.3.4: resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==} peerDependencies: @@ -5844,30 +1656,6 @@ packages: sqlite3: optional: true - dc-polyfill@0.1.11: - resolution: {integrity: sha512-TyyeGcjx0YeThAI9fTFtgsvj5qd4R+aGfVmXiUhevbgzWFDr7IK4tv4YjE6jaGzLHQTchk4h7DHdr5q4WGgaZw==} - engines: {node: '>=12.17'} - - dd-trace@5.103.0: - resolution: {integrity: sha512-evIKDqY8X/zmbMKyDZ020FmzAf1IxPe0m3AIdo1mHk9YkFmO4S1RIuFuffK8dIA0BYkP56q/iQvmIfCQ5UNsrg==} - engines: {node: '>=18 <26'} - - debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -5877,37 +1665,6 @@ packages: supports-color: optional: true - decamelize@1.2.0: - resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} - engines: {node: '>=0.10.0'} - - decode-uri-component@0.2.2: - resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} - engines: {node: '>=0.10'} - - dedent@1.7.2: - resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} - peerDependencies: - babel-plugin-macros: ^3.1.0 - peerDependenciesMeta: - babel-plugin-macros: - optional: true - - deep-equal@2.2.3: - resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} - engines: {node: '>= 0.4'} - - deep-extend@0.6.0: - resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} - engines: {node: '>=4.0.0'} - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - deepmerge-ts@7.1.5: - resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} - engines: {node: '>=16.0.0'} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -5920,43 +1677,13 @@ packages: resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} engines: {node: '>=18'} - default-gateway@4.2.0: - resolution: {integrity: sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==} - engines: {node: '>=6'} - - defaults@1.0.4: - resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} - - define-data-property@1.1.4: - resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} - engines: {node: '>= 0.4'} - - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - define-lazy-prop@3.0.0: resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} engines: {node: '>=12'} - define-properties@1.2.1: - resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} - engines: {node: '>= 0.4'} - defu@6.1.7: resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} - del@6.1.1: - resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==} - engines: {node: '>=10'} - - delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - - denodeify@1.2.1: - resolution: {integrity: sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg==} - denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} @@ -5972,226 +1699,36 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} - - detect-libc@1.0.3: - resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} - engines: {node: '>=0.10'} - hasBin: true - - detect-libc@2.0.2: - resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} - engines: {node: '>=8'} - detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - detect-newline@3.1.0: - resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} - engines: {node: '>=8'} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - dfa@1.2.0: - resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} - - diff-sequences@29.6.3: - resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - diff@4.0.4: - resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} - engines: {node: '>=0.3.1'} - diff@8.0.4: resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} engines: {node: '>=0.3.1'} - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - - dnssd-advertise@1.1.4: - resolution: {integrity: sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA==} - - doctrine@2.1.0: - resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} - engines: {node: '>=0.10.0'} - - doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dot-prop@10.1.0: resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} engines: {node: '>=20'} - dotenv-expand@11.0.7: - resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} - engines: {node: '>=12'} - - dotenv@16.4.7: - resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} - engines: {node: '>=12'} - - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} - drizzle-kit@0.31.10: - resolution: {integrity: sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==} - hasBin: true - - drizzle-orm@0.45.2: - resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} - peerDependencies: - '@aws-sdk/client-rds-data': '>=3' - '@cloudflare/workers-types': '>=4' - '@electric-sql/pglite': '>=0.2.0' - '@libsql/client': '>=0.10.0' - '@libsql/client-wasm': '>=0.10.0' - '@neondatabase/serverless': '>=0.10.0' - '@op-engineering/op-sqlite': '>=2' - '@opentelemetry/api': ^1.4.1 - '@planetscale/database': '>=1.13' - '@prisma/client': '*' - '@tidbcloud/serverless': '*' - '@types/better-sqlite3': '*' - '@types/pg': '*' - '@types/sql.js': '*' - '@upstash/redis': '>=1.34.7' - '@vercel/postgres': '>=0.8.0' - '@xata.io/client': '*' - better-sqlite3: '>=7' - bun-types: '*' - expo-sqlite: '>=14.0.0' - gel: '>=2' - knex: '*' - kysely: '*' - mysql2: '>=2' - pg: '>=8' - postgres: '>=3' - prisma: '*' - sql.js: '>=1' - sqlite3: '>=5' - peerDependenciesMeta: - '@aws-sdk/client-rds-data': - optional: true - '@cloudflare/workers-types': - optional: true - '@electric-sql/pglite': - optional: true - '@libsql/client': - optional: true - '@libsql/client-wasm': - optional: true - '@neondatabase/serverless': - optional: true - '@op-engineering/op-sqlite': - optional: true - '@opentelemetry/api': - optional: true - '@planetscale/database': - optional: true - '@prisma/client': - optional: true - '@tidbcloud/serverless': - optional: true - '@types/better-sqlite3': - optional: true - '@types/pg': - optional: true - '@types/sql.js': - optional: true - '@upstash/redis': - optional: true - '@vercel/postgres': - optional: true - '@xata.io/client': - optional: true - better-sqlite3: - optional: true - bun-types: - optional: true - expo-sqlite: - optional: true - gel: - optional: true - knex: - optional: true - kysely: - optional: true - mysql2: - optional: true - pg: - optional: true - postgres: - optional: true - prisma: - optional: true - sql.js: - optional: true - sqlite3: - optional: true - - dset@3.1.4: - resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==} - engines: {node: '>=4'} - - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - duplexify@4.1.3: - resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} - eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - - editorconfig@1.0.7: - resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} - engines: {node: '>=14'} - hasBin: true - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - effect@3.21.0: - resolution: {integrity: sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==} - - electron-to-chromium@1.5.355: - resolution: {integrity: sha512-LUPZhKzZPYSPme1jEYohpkA+ybYCJztr1quAdBd7E7h3+VOBVcKkwwtBJu41nrjawrRzfb8mtMfzWozoaK0ZIQ==} - - emittery@0.13.1: - resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} - engines: {node: '>=12'} + electron-to-chromium@1.5.361: + resolution: {integrity: sha512-Q6Hts7N9FnJc5LeGRINFvLhCI9xZmNtTDe5ZbcVezQz7cU4a8Aua3GH1b8J2XY8Al9PF+OCwYqhgsOOheMdvkA==} emoji-regex-xs@1.0.0: resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} @@ -6205,102 +1742,34 @@ packages: emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - empathic@2.0.0: - resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} - engines: {node: '>=14'} - - encodeurl@1.0.2: - resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} - engines: {node: '>= 0.8'} - encodeurl@2.0.0: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} + enhanced-resolve@5.22.0: + resolution: {integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==} + engines: {node: '>=10.13.0'} entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} - env-editor@0.4.2: - resolution: {integrity: sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==} - engines: {node: '>=8'} - - envinfo@7.21.0: - resolution: {integrity: sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==} - engines: {node: '>=4'} - hasBin: true - - error-ex@1.3.4: - resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} - error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - errorhandler@1.5.2: - resolution: {integrity: sha512-kNAL7hESndBCrWwS72QyV3IVOTrVmj9D062FV5BQswNL5zEdeRmz/WJFyh6Aj/plvvSOrzddkxW57HgkZcR9Fw==} - engines: {node: '>= 0.8'} - - es-abstract@1.24.2: - resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} - engines: {node: '>= 0.4'} - - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - es-errors@1.3.0: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-get-iterator@1.1.3: - resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} - - es-iterator-helpers@1.3.2: - resolution: {integrity: sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==} - engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} es-module-lexer@2.1.0: resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - - es-set-tostringtag@2.1.0: - resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} - engines: {node: '>= 0.4'} - - es-shim-unscopables@1.1.0: - resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} - engines: {node: '>= 0.4'} - - es-to-primitive@1.3.0: - resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} - engines: {node: '>= 0.4'} - - esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} - hasBin: true - - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true - esbuild@0.25.12: resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} @@ -6323,81 +1792,16 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - - escape-string-regexp@2.0.0: - resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} - engines: {node: '>=8'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - escape-string-regexp@5.0.0: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-plugin-react-native-globals@0.1.2: - resolution: {integrity: sha512-9aEPf1JEpiTjcFAmmyw8eiIXmcNZOqaZyHO77wgm0/dWfT/oxC1SrIq8ET38pMxHYrcB6Uew+TzUVsBeczF88g==} - - eslint-plugin-react-native@4.1.0: - resolution: {integrity: sha512-QLo7rzTBOl43FvVqDdq5Ql9IoElIuTdjrz9SKAXCvULvBoRZ44JGSkx9z4999ZusCsb4rK3gjS8gOGyeYqZv2Q==} - peerDependencies: - eslint: ^3.17.0 || ^4 || ^5 || ^6 || ^7 || ^8 - - eslint-plugin-react@7.37.5: - resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} - engines: {node: '>=4'} - peerDependencies: - eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint@8.57.1: - resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. - hasBin: true - - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} - engines: {node: '>=0.10'} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - 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==} estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -6413,238 +1817,13 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} - execa@1.0.0: - resolution: {integrity: sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==} - engines: {node: '>=6'} - - execa@5.1.1: - resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} - engines: {node: '>=10'} - - exit@0.1.2: - resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} - engines: {node: '>= 0.8.0'} - expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} - expect@29.7.0: - resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - expo-application@5.9.1: - resolution: {integrity: sha512-uAfLBNZNahnDZLRU41ZFmNSKtetHUT9Ua557/q189ua0AWV7pQjoVAx49E4953feuvqc9swtU3ScZ/hN1XO/FQ==} - peerDependencies: - expo: '*' - - expo-asset@10.0.10: - resolution: {integrity: sha512-0qoTIihB79k+wGus9wy0JMKq7DdenziVx3iUkGvMAy2azscSgWH6bd2gJ9CGnhC6JRd3qTMFBL0ou/fx7WZl7A==} - peerDependencies: - expo: '*' - - expo-asset@55.0.17: - resolution: {integrity: sha512-pK9HHJuFqjE8kDUcbMFsZj3Cz8WdXpvZHZmYl7ouFQp59P83BvHln6VnqPDGlO+/4929G0Lm8ZUzbONuNRhi9w==} - peerDependencies: - expo: '*' - react: '*' - react-native: '*' - - expo-av@14.0.7: - resolution: {integrity: sha512-FvKZxyy+2/qcCmp+e1GTK3s4zH8ZO1RfjpqNxh7ARlS1oH8HPtk1AyZAMo52tHz3yQ3UIqxQ2YbI9CFb4065lA==} - peerDependencies: - expo: '*' - - expo-constants@16.0.2: - resolution: {integrity: sha512-9tNY3OVO0jfiMzl7ngb6IOyR5VFzNoN5OOazUWoeGfmMqVB5kltTemRvKraK9JRbBKIw+SOYLEmF0sEqgFZ6OQ==} - peerDependencies: - expo: '*' - - expo-constants@55.0.16: - resolution: {integrity: sha512-Z15/No94UHoogD+pulxjudGAeOHTEIWZgb/vnX48Wx5D+apWTeCbnKxQZZtGQlosvduYL5kaic2/W8U+NHfBQQ==} - peerDependencies: - expo: '*' - react-native: '*' - - expo-crypto@13.0.2: - resolution: {integrity: sha512-7f/IMPYJZkBM21LNEMXGrNo/0uXSVfZTwufUdpNKedJR0fm5fH4DCSN79ZddlV26nF90PuXjK2inIbI6lb0qRA==} - peerDependencies: - expo: '*' - - expo-device@6.0.2: - resolution: {integrity: sha512-sCt91CuTmAuMXX4SlFOn4lIos2UIr8vb0jDstDDZXys6kErcj0uynC7bQAMreU5uRUTKMAl4MAMpKt9ufCXPBw==} - peerDependencies: - expo: '*' - - expo-eas-client@0.6.0: - resolution: {integrity: sha512-FSPy0ThcJBvzEzOZVhpOrYyHgQ8U1jJ4v7u7tr1x0KOVRqyf25APEQZFxxRPn3zAYW0tQ+uDTCbrwNymFqhQfw==} - - expo-file-system@17.0.1: - resolution: {integrity: sha512-dYpnZJqTGj6HCYJyXAgpFkQWsiCH3HY1ek2cFZVHFoEc5tLz9gmdEgTF6nFHurvmvfmXqxi7a5CXyVm0aFYJBw==} - peerDependencies: - expo: '*' - - expo-file-system@55.0.20: - resolution: {integrity: sha512-sBCHhNlCT3EiqCcE6xSbyvOLUAlKx7+p0qjo+c+UPyC/gMrXUdva99g25uptM+fEMwy2co25MUQQ0U0guQLOQA==} - peerDependencies: - expo: '*' - react-native: '*' - - expo-font@12.0.10: - resolution: {integrity: sha512-Q1i2NuYri3jy32zdnBaHHCya1wH1yMAsI+3CCmj9zlQzlhsS9Bdwcj2W3c5eU5FvH2hsNQy4O+O1NnM6o/pDaQ==} - peerDependencies: - expo: '*' - - expo-font@55.0.7: - resolution: {integrity: sha512-oH39Xb+3i6Y69b7YRP+P+5WLx7621t+ep/RAgLwJJYpTjs7CnSohUG+873rEtqsTAuQGi63ms7x9ZeHj1E9LYw==} - peerDependencies: - expo: '*' - react: '*' - react-native: '*' - - expo-image-loader@4.7.0: - resolution: {integrity: sha512-cx+MxxsAMGl9AiWnQUzrkJMJH4eNOGlu7XkLGnAXSJrRoIiciGaKqzeaD326IyCTV+Z1fXvIliSgNW+DscvD8g==} - peerDependencies: - expo: '*' - - expo-image-picker@15.0.7: - resolution: {integrity: sha512-u8qiPZNfDb+ap6PJ8pq2iTO7JKX+ikAUQ0K0c7gXGliKLxoXgDdDmXxz9/6QdICTshJBJlBvI0MwY5NWu7A/uw==} - peerDependencies: - expo: '*' - - expo-json-utils@0.7.1: - resolution: {integrity: sha512-L0lyH8diXQtV0q5BLbFlcoxTqPF5im79xDHPhybB0j36xYdm65hjwRJ4yMrPIN5lR18hj48FUZeONiDHRyEvIg==} - - expo-keep-awake@13.0.2: - resolution: {integrity: sha512-kKiwkVg/bY0AJ5q1Pxnm/GvpeB6hbNJhcFsoOWDh2NlpibhCLaHL826KHUM+WsnJRbVRxJ+K9vbPRHEMvFpVyw==} - peerDependencies: - expo: '*' - - expo-keep-awake@55.0.8: - resolution: {integrity: sha512-PfIpMfM+STOBwkR5XOE+yVtER86c44MD+W8QD8JxuO0sT9pF7Y1SJYakWlpvX8xsGA+bjKLxftm9403s9kQhKA==} - peerDependencies: - expo: '*' - react: '*' - - expo-linking@6.3.1: - resolution: {integrity: sha512-xuZCntSBGWCD/95iZ+mTUGTwHdy8Sx+immCqbUBxdvZ2TN61P02kKg7SaLS8A4a/hLrSCwrg5tMMwu5wfKr35g==} - - expo-local-authentication@14.0.1: - resolution: {integrity: sha512-kAwUD1wEqj1fhwQgIHlP4H/JV9AcX+NO3BJwhPM2HuCFS0kgx2wvcHisnKBSTRyl8u5Jt4odzMyQkDJystwUTg==} - peerDependencies: - expo: '*' - - expo-manifests@0.7.2: - resolution: {integrity: sha512-xlhL0XI2zw3foJ0q2Ra4ieBhU0V2yz+Rv6GpVEaaIHFlIC/Dbx+mKrX5dgenZEMERr/MG7sRJaRbAVB2PaAYhA==} - - expo-modules-autolinking@1.11.3: - resolution: {integrity: sha512-oYh8EZEvYF5TYppxEKUTTJmbr8j7eRRnrIxzZtMvxLTXoujThVPMFS/cbnSnf2bFm1lq50TdDNABhmEi7z0ngQ==} - hasBin: true - - expo-modules-autolinking@55.0.22: - resolution: {integrity: sha512-13x32V0HMHJDjND4K/gU2lQIZNxYn5S5rFzujqHmnXvOO6WGrVVELpk/0p5FmBfeuQ7GGFsATbhazQk+FeukUw==} - hasBin: true - - expo-modules-core@1.12.26: - resolution: {integrity: sha512-y8yDWjOi+rQRdO+HY+LnUlz8qzHerUaw/LUjKPU/mX8PRXP4UUPEEp5fjAwBU44xjNmYSHWZDwet4IBBE+yQUA==} - - expo-modules-core@55.0.25: - resolution: {integrity: sha512-yXpfg7aHLbuqoXocK34Vua6Aey5SCyqLygAsXAMbul9P8vfBjLpaOPiTJ5cLVF7Drfq8ownqVJO6qpGEtZ6GOw==} - peerDependencies: - react: '*' - react-native: '*' - react-native-worklets: ^0.7.4 || ^0.8.0 - peerDependenciesMeta: - react-native-worklets: - optional: true - - expo-notifications@0.28.19: - resolution: {integrity: sha512-rKKTnVQQ9XNQyTNwKmI9OlchhVu0XOZfRpImMqPFCJg6IwECM1izdas2SLCbE/GApg2Tw3U5R2fd26OnCtUU/w==} - peerDependencies: - expo: '*' - - expo-secure-store@12.8.1: - resolution: {integrity: sha512-Ju3jmkHby4w7rIzdYAt9kQyQ7HhHJ0qRaiQOInknhOLIltftHjEgF4I1UmzKc7P5RCfGNmVbEH729Pncp/sHXQ==} - peerDependencies: - expo: '*' - - expo-secure-store@13.0.2: - resolution: {integrity: sha512-3QYgoneo8p8yeeBPBiAfokNNc2xq6+n8+Ob4fAlErEcf4H7Y72LH+K/dx0nQyWau2ZKZUXBxyyfuHFyVKrEVLg==} - peerDependencies: - expo: '*' - - expo-server@55.0.9: - resolution: {integrity: sha512-N5Ipn1NwqaJzEm+G97o0Jbe4g/th3R/16N1DabnYryXKCiZwDkK13/w3VfGkQN9LOOaBP+JIRxGf4M8lQKPzyA==} - engines: {node: '>=20.16.0'} - - expo-status-bar@1.12.1: - resolution: {integrity: sha512-/t3xdbS8KB0prj5KG5w7z+wZPFlPtkgs95BsmrP/E7Q0xHXTcDcQ6Cu2FkFuRM+PKTb17cJDnLkawyS5vDLxMA==} - - expo-structured-headers@3.3.0: - resolution: {integrity: sha512-t+h5Zqaukd3Tn97LaWPpibVsmiC/TFP8F+8sAUliwCSMzgcb5TATRs2NcAB+JcIr8EP3JJDyYXJrZle1cjs4mQ==} - - expo-updates-interface@0.10.1: - resolution: {integrity: sha512-I6JMR7EgjXwckrydDmrkBEX/iw750dcqpzQVsjznYWfi0HTEOxajLHB90fBFqQkUV5i5s4Fd3hYQ1Cn0oMzUbA==} - peerDependencies: - expo: '*' - - expo-updates@0.18.19: - resolution: {integrity: sha512-dakYQ7XhZtBKMLcim08wum108ZUQcNigurijb/6PKdg3QHn21IzOr/27n6x54DctcoW8w1B8w8y1Xw2svVsx4w==} - hasBin: true - peerDependencies: - expo: '*' - - expo@51.0.39: - resolution: {integrity: sha512-Cs/9xopyzJrpXWbyVUZnr37rprdFJorRgfSp6cdBfvbjxZeKnw2MEu7wJwV/s626i5lZTPGjZPHUF9uQvt51cg==} - hasBin: true - - expo@55.0.24: - resolution: {integrity: sha512-nU95y+GIfD1dm9CSjsitDdltSU83dDqemxD1UUBxJPH8zKf7B5AdGVNyE6/jLWyCM/p/EmHfCeiqdrWCy9ljZA==} - hasBin: true - peerDependencies: - '@expo/dom-webview': '*' - '@expo/metro-runtime': '*' - react: '*' - react-native: '*' - react-native-webview: '*' - peerDependenciesMeta: - '@expo/dom-webview': - optional: true - '@expo/metro-runtime': - optional: true - react-native-webview: - optional: true - - exponential-backoff@3.1.3: - resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - - express@4.22.2: - resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} - engines: {node: '>= 0.10.0'} - exsolve@1.0.8: resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - - farmhash-modern@1.1.0: - resolution: {integrity: sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==} - engines: {node: '>=18.0.0'} - - fast-check@3.23.2: - resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} - engines: {node: '>=8.0.0'} - - fast-decode-uri-component@1.0.1: - resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} - - fast-deep-equal@2.0.1: - resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} @@ -6652,73 +1831,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-json-stringify@6.4.0: - resolution: {integrity: sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fast-querystring@1.1.2: - resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} - - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} - - fast-xml-builder@1.2.0: - resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} - - fast-xml-parser@4.5.6: - resolution: {integrity: sha512-Yd4vkROfJf8AuJrDIVMVmYfULKmIJszVsMv7Vo71aocsKgFxpdlpSHXSaInvyYfgw2PRuObQSW2GFpVMUjxu9A==} - hasBin: true - - fast-xml-parser@5.7.2: - resolution: {integrity: sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==} - hasBin: true - - fast-xml-parser@5.8.0: - resolution: {integrity: sha512-6bIM7fsJxeo3uXv7OncQYsBAMPJ7V16Slahl/6M98C/i2q+vB1+4a0MtrvYwDFEUrwDSbAmeLDRXsOBwrL7yAg==} - hasBin: true - - fastify-plugin@4.5.1: - resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} - - fastify-plugin@5.1.0: - resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} - - fastify-raw-body@5.0.0: - resolution: {integrity: sha512-2qfoaQ3BQDhZ1gtbkKZd6n0kKxJISJGM6u/skD9ljdWItAscjXrtZ1lnjr7PavmXX9j4EyCPmBDiIsLn07d5vA==} - engines: {node: '>= 10'} - - fastify@5.8.5: - resolution: {integrity: sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==} - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - faye-websocket@0.11.4: - resolution: {integrity: sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==} - engines: {node: '>=0.8.0'} - - fb-dotslash@0.5.8: - resolution: {integrity: sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==} - engines: {node: '>=20'} - hasBin: true - - fb-watchman@2.0.2: - resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - - fbemitter@3.0.0: - resolution: {integrity: sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==} - - fbjs-css-vars@1.0.2: - resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==} - - fbjs@3.0.5: - resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -6728,19 +1843,6 @@ packages: picomatch: optional: true - fetch-nodeshim@0.4.10: - resolution: {integrity: sha512-m6I8ALe4L4XpdETy7MJZWs6L1IVMbjs99bwbpIKphxX+0CTns4IKDWJY0LWfr4YsFjfg+z1TjzTMU8lKl8rG0w==} - - fetch-retry@4.1.1: - resolution: {integrity: sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA==} - - fflate@0.8.1: - resolution: {integrity: sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==} - - file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -6748,136 +1850,14 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - filter-obj@1.1.0: - resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} - engines: {node: '>=0.10.0'} - - finalhandler@1.1.2: - resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} - engines: {node: '>= 0.8'} - - finalhandler@1.3.2: - resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} - engines: {node: '>= 0.8'} - - find-cache-dir@2.1.0: - resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} - engines: {node: '>=6'} - - find-my-way@9.6.0: - resolution: {integrity: sha512-Zf4Xve4RymLl7NgaavNebZ01joJ8MfVerOG43wy7SHLO+r+K0C6d/SE0BiR7AV5V1VOCFlOP7ecdo+I4qmiHrQ==} - engines: {node: '>=20'} - - find-up@3.0.0: - resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} - engines: {node: '>=6'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - find-yarn-workspace-root@2.0.0: - resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} - - firebase-admin@12.7.0: - resolution: {integrity: sha512-raFIrOyTqREbyXsNkSHyciQLfv8AUZazehPaQS1lZBSCDYW74FYXU0nQZa3qHI4K+hawohlDbywZ4+qce9YNxA==} - engines: {node: '>=14'} - - flat-cache@3.2.0: - resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} - engines: {node: ^10.12.0 || >=12.0.0} - - flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - - flow-enums-runtime@0.0.6: - resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} - - flow-parser@0.314.0: - resolution: {integrity: sha512-ayvpVFL/wibphkhjaz6PwL/F+Vz9lZB7qwFIHvsFiPQMfKmrqRXp1UyJgxMqyanW6QQDvsB12MLWFCc2cYBOtw==} - engines: {node: '>=0.4.0'} - - follow-redirects@1.16.0: - resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - - fontfaceobserver@2.3.0: - resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==} - - fontkit@1.9.0: - resolution: {integrity: sha512-HkW/8Lrk8jl18kzQHvAw9aTHe1cqsyx5sDnxncx652+CIfhawokEPkeM3BoIC+z/Xv7a0yMr0f3pRRwhGH455g==} - - for-each@0.3.5: - resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} - engines: {node: '>= 0.4'} - foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} - form-data@2.5.5: - resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} - engines: {node: '>= 0.12'} - - form-data@3.0.4: - resolution: {integrity: sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==} - engines: {node: '>= 6'} - - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} - engines: {node: '>= 6'} - - forwarded-parse@2.1.2: - resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} - - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - freeport-async@2.0.0: - resolution: {integrity: sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==} - engines: {node: '>=8'} - - fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} - fresh@2.0.0: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} - - fs-extra@9.0.0: - resolution: {integrity: sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==} - engines: {node: '>=10'} - - fs-extra@9.1.0: - resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} - engines: {node: '>=10'} - - fs-minipass@2.1.0: - resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} - engines: {node: '>= 8'} - - fs-minipass@3.0.3: - resolution: {integrity: sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -6886,28 +1866,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - function.prototype.name@1.1.8: - resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} - engines: {node: '>= 0.4'} - - functional-red-black-tree@1.0.1: - resolution: {integrity: sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==} - - functions-have-names@1.2.3: - resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - - gaxios@6.7.1: - resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} - engines: {node: '>=14'} - - gcp-metadata@6.1.1: - resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} - engines: {node: '>=14'} - - generator-function@2.0.1: - resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} - engines: {node: '>= 0.4'} - gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -6920,48 +1878,9 @@ packages: resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} engines: {node: '>=18'} - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-package-type@0.1.0: - resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} - engines: {node: '>=8.0.0'} - get-port-please@3.2.0: resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - - get-stream@4.1.0: - resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} - engines: {node: '>=6'} - - get-stream@6.0.1: - resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} - engines: {node: '>=10'} - - get-symbol-description@1.1.0: - resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} - engines: {node: '>= 0.4'} - - get-tsconfig@4.14.0: - resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} - - getenv@1.0.0: - resolution: {integrity: sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==} - engines: {node: '>=6'} - - getenv@2.0.0: - resolution: {integrity: sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==} - engines: {node: '>=6'} - - giget@2.0.0: - resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} - hasBin: true - giget@3.2.0: resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==} hasBin: true @@ -6970,10 +1889,6 @@ packages: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -6983,79 +1898,13 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} - glob@7.1.6: - resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - - glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - - glob@8.1.0: - resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} - engines: {node: '>=12'} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - - globals@13.24.0: - resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} - engines: {node: '>=8'} - - globalthis@1.0.4: - resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} - engines: {node: '>= 0.4'} - - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - globby@16.2.0: resolution: {integrity: sha512-QrJia2qDf5BB/V6HYlDTs0I0lBahyjLzpGQg3KT7FnCdTonAyPy2RtY802m2k4ALx6Dp752f82WsOczEVr3l6Q==} engines: {node: '>=20'} - google-auth-library@9.15.1: - resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} - engines: {node: '>=14'} - - google-gax@4.6.1: - resolution: {integrity: sha512-V6eky/xz2mcKfAd1Ioxyd6nmA61gao3n01C+YeuIwu3vzM9EDR6wcVzMSIbLMDXWeoi9SHYctXuKYC5uJUT3eQ==} - engines: {node: '>=14'} - - google-logging-utils@0.0.2: - resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} - engines: {node: '>=14'} - - googleapis-common@7.2.0: - resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} - engines: {node: '>=14.0.0'} - - googleapis@128.0.0: - resolution: {integrity: sha512-+sLtVYNazcxaSD84N6rihVX4QiGoqRdnlz2SwmQQkadF31XonDfy4ufk3maMg27+FiySrH0rd7V8p+YJG6cknA==} - engines: {node: '>=14.0.0'} - - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - graphql-tag@2.12.6: - resolution: {integrity: sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==} - engines: {node: '>=10'} - peerDependencies: - graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 - - graphql@15.8.0: - resolution: {integrity: sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw==} - engines: {node: '>= 10.x'} - - gtoken@7.1.0: - resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} - engines: {node: '>=14.0.0'} - gzip-size@7.0.0: resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -7072,38 +1921,10 @@ packages: crossws: optional: true - handlebars@4.7.9: - resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} - engines: {node: '>=0.4.7'} - hasBin: true - - has-bigints@1.1.0: - resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} - engines: {node: '>= 0.4'} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-property-descriptors@1.0.2: - resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} - - has-proto@1.2.0: - resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} - engines: {node: '>= 0.4'} - - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - has-tostringtag@1.0.2: - resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} - engines: {node: '>= 0.4'} - hasown@2.0.3: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} @@ -7114,115 +1935,29 @@ packages: hast-util-whitespace@3.0.0: resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - helmet@8.1.0: - resolution: {integrity: sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==} - engines: {node: '>=18.0.0'} - - hermes-compiler@250829098.0.10: - resolution: {integrity: sha512-TcRlZ0/TlyfJqquRFAWoyElVNnkdYRi/sEp4/Qy8/GYxjg8j2cS9D4MjuaQ+qimkmLN7AmO+44IznRf06mAr0w==} - - hermes-estree@0.19.1: - resolution: {integrity: sha512-daLGV3Q2MKk8w4evNMKwS8zBE/rcpA800nu1Q5kM08IKijoSnPe9Uo1iIxzPKRkn95IxxsgBMPeYHt3VG4ej2g==} - - hermes-estree@0.23.1: - resolution: {integrity: sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg==} - - hermes-estree@0.32.0: - resolution: {integrity: sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==} - - hermes-estree@0.32.1: - resolution: {integrity: sha512-ne5hkuDxheNBAikDjqvCZCwihnz0vVu9YsBzAEO1puiyFR4F1+PAz/SiPHSsNTuOveCYGRMX8Xbx4LOubeC0Qg==} - - hermes-estree@0.33.3: - resolution: {integrity: sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg==} - - hermes-estree@0.35.0: - resolution: {integrity: sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==} - - hermes-parser@0.19.1: - resolution: {integrity: sha512-Vp+bXzxYJWrpEuJ/vXxUsLnt0+y4q9zyi4zUlkLqD8FKv4LjIfOvP69R/9Lty3dCyKh0E2BU7Eypqr63/rKT/A==} - - hermes-parser@0.23.1: - resolution: {integrity: sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA==} - - hermes-parser@0.32.0: - resolution: {integrity: sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==} - - hermes-parser@0.32.1: - resolution: {integrity: sha512-175dz634X/W5AiwrpLdoMl/MOb17poLHyIqgyExlE8D9zQ1OPnoORnGMB5ltRKnpvQzBjMYvT2rN/sHeIfZW5Q==} - - hermes-parser@0.33.3: - resolution: {integrity: sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA==} - - hermes-parser@0.35.0: - resolution: {integrity: sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==} - - hermes-profile-transformer@0.0.6: - resolution: {integrity: sha512-cnN7bQUm65UWOy6cbGcCcZ3rpwW8Q/j4OP5aWRhEry4Z2t2aR1cjrbp0BS+KiBN0smvP1caBgAuxutvyvJILzQ==} - engines: {node: '>=8'} - - hoist-non-react-statics@3.3.2: - resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} - hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} - hookified@1.15.1: - resolution: {integrity: sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==} - - hosted-git-info@3.0.8: - resolution: {integrity: sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw==} - engines: {node: '>=10'} - - hosted-git-info@7.0.2: - resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} - engines: {node: ^16.14.0 || >=18.0.0} - html-entities@2.3.3: resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} - html-entities@2.6.0: - resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} - html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} html-to-image@1.11.13: resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} - html-to-text@9.0.5: - resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} - engines: {node: '>=14'} - html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - - http-errors@2.0.0: - resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} - engines: {node: '>= 0.8'} - http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - http-parser-js@0.5.10: - resolution: {integrity: sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==} - - http-proxy-agent@5.0.0: - resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} - engines: {node: '>= 6'} - http-shutdown@1.2.2: resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -7230,193 +1965,40 @@ packages: httpxy@0.5.3: resolution: {integrity: sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw==} - human-signals@2.1.0: - resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} - engines: {node: '>=10.17.0'} - - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - - iconv-lite@0.7.2: - resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} - engines: {node: '>=0.10.0'} - ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - ignore@7.0.5: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} - image-size@1.2.1: - resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==} - engines: {node: '>=16.x'} - hasBin: true - - import-fresh@2.0.0: - resolution: {integrity: sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==} - engines: {node: '>=4'} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - import-in-the-middle@1.15.0: - resolution: {integrity: sha512-bpQy+CrsRmYmoPMAE/0G33iwRqwW4ouqdRg8jgbH3aKuCtOc8lxgmYXg2dMM92CRiGP660EtBcymH/eVUpCSaA==} - - import-in-the-middle@3.0.1: - resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} - engines: {node: '>=18'} - - import-local@3.2.0: - resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} - engines: {node: '>=8'} - hasBin: true - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - indent-string@4.0.0: - resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} - engines: {node: '>=8'} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - ini@1.3.8: - resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - - internal-ip@4.3.0: - resolution: {integrity: sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==} - engines: {node: '>=6'} - - internal-slot@1.1.0: - resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} - engines: {node: '>= 0.4'} - - invariant@2.2.4: - resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} - ioredis@5.10.1: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} - ip-regex@2.1.0: - resolution: {integrity: sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==} - engines: {node: '>=4'} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - - ipaddr.js@2.4.0: - resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} - engines: {node: '>= 10'} - iron-webcrypto@1.2.1: resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} - is-arguments@1.2.0: - resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} - engines: {node: '>= 0.4'} - - is-array-buffer@3.0.5: - resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} - engines: {node: '>= 0.4'} - - is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - - is-arrayish@0.3.4: - resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} - - is-async-function@2.1.1: - resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} - engines: {node: '>= 0.4'} - - is-bigint@1.1.0: - resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} - engines: {node: '>= 0.4'} - - is-boolean-object@1.2.2: - resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} - engines: {node: '>= 0.4'} - - is-buffer@1.1.6: - resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} - - is-callable@1.2.7: - resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} - engines: {node: '>= 0.4'} - is-core-module@2.16.2: resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} engines: {node: '>= 0.4'} - is-data-view@1.0.2: - resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} - engines: {node: '>= 0.4'} - - is-date-object@1.1.0: - resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} - engines: {node: '>= 0.4'} - - is-directory@0.3.1: - resolution: {integrity: sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==} - engines: {node: '>=0.10.0'} - - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - is-docker@3.0.0: resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true - is-extglob@1.0.0: - resolution: {integrity: sha512-7Q+VbVafe6x2T+Tu6NcOf6sRklazEPmBoB3IWk3WdGZM2iGUwU/Oe3Wtq5lSEkDTTlpp8yx+5t4pzO/i9Ty1ww==} - engines: {node: '>=0.10.0'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-finalizationregistry@1.1.1: - resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} - engines: {node: '>= 0.4'} - - is-fullwidth-code-point@2.0.0: - resolution: {integrity: sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==} - engines: {node: '>=4'} - is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-generator-fn@2.1.0: - resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} - engines: {node: '>=6'} - - is-generator-function@1.1.2: - resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} - engines: {node: '>= 0.4'} - - is-glob@2.0.1: - resolution: {integrity: sha512-a1dBeB19NXsf/E0+FHqkagizel/LQw2DjSQpvQrj3zT+jYPpaUCryPnrQajXKFLCMuf4I6FhRpaGtw4lPrG6Eg==} - engines: {node: '>=0.10.0'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -7430,124 +2012,28 @@ packages: engines: {node: '>=14.16'} hasBin: true - is-interactive@1.0.0: - resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} - engines: {node: '>=8'} - - is-invalid-path@0.1.0: - resolution: {integrity: sha512-aZMG0T3F34mTg4eTdszcGXx54oiZ4NtHSft3hWNJMGJXUUqdIj3cOZuHcU0nCWWcY3jd7yRe/3AEm3vSNTpBGQ==} - engines: {node: '>=0.10.0'} - - is-map@2.0.3: - resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} - engines: {node: '>= 0.4'} - is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - is-nan@1.3.2: - resolution: {integrity: sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==} - engines: {node: '>= 0.4'} - - is-negative-zero@2.0.3: - resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} - engines: {node: '>= 0.4'} - - is-number-object@1.1.1: - resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} - engines: {node: '>= 0.4'} - is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-cwd@2.2.0: - resolution: {integrity: sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==} - engines: {node: '>=6'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-path-inside@4.0.0: resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} engines: {node: '>=12'} - is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - - is-plain-object@2.0.4: - resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} - engines: {node: '>=0.10.0'} - is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - is-regex@1.2.1: - resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} - engines: {node: '>= 0.4'} - - is-set@2.0.3: - resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} - engines: {node: '>= 0.4'} - - is-shared-array-buffer@1.0.4: - resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} - engines: {node: '>= 0.4'} - - is-stream@1.1.0: - resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==} - engines: {node: '>=0.10.0'} - is-stream@2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} - is-string@1.1.1: - resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} - engines: {node: '>= 0.4'} - - is-symbol@1.1.1: - resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} - engines: {node: '>= 0.4'} - - is-typed-array@1.1.15: - resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} - engines: {node: '>= 0.4'} - - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - - is-valid-path@0.1.1: - resolution: {integrity: sha512-+kwPrVDu9Ms03L90Qaml+79+6DZHqHyRoANI6IsZJ/g8frhnfchDOBCa0RbQ6/kdHt5CS5OeIEyrYznNuVN+8A==} - engines: {node: '>=0.10.0'} - - is-weakmap@2.0.2: - resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} - engines: {node: '>= 0.4'} - - is-weakref@1.1.1: - resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} - engines: {node: '>= 0.4'} - - is-weakset@2.0.4: - resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} - engines: {node: '>= 0.4'} - is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} - is-wsl@1.1.0: - resolution: {integrity: sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==} - engines: {node: '>=4'} - - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - is-wsl@3.1.1: resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} engines: {node: '>=16'} @@ -7555,208 +2041,28 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - isarray@2.0.5: - resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isobject@3.0.1: - resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} - engines: {node: '>=0.10.0'} - istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} - istanbul-lib-instrument@5.2.1: - resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} - engines: {node: '>=8'} - - istanbul-lib-instrument@6.0.3: - resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} - engines: {node: '>=10'} - istanbul-lib-report@3.0.1: resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} engines: {node: '>=10'} - istanbul-lib-source-maps@4.0.1: - resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} - engines: {node: '>=10'} - istanbul-reports@3.2.0: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} - iterator.prototype@1.1.5: - resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} - engines: {node: '>= 0.4'} - jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jest-changed-files@29.7.0: - resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-circus@29.7.0: - resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-cli@29.7.0: - resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jest-config@29.7.0: - resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - peerDependencies: - '@types/node': '*' - ts-node: '>=9.0.0' - peerDependenciesMeta: - '@types/node': - optional: true - ts-node: - optional: true - - jest-diff@29.7.0: - resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-docblock@29.7.0: - resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-each@29.7.0: - resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-environment-node@29.7.0: - resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-get-type@29.6.3: - resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-haste-map@29.7.0: - resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-leak-detector@29.7.0: - resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-matcher-utils@29.7.0: - resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-message-util@29.7.0: - resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-mock@29.7.0: - resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-pnp-resolver@1.2.3: - resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} - engines: {node: '>=6'} - peerDependencies: - jest-resolve: '*' - peerDependenciesMeta: - jest-resolve: - optional: true - - jest-regex-util@29.6.3: - resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve-dependencies@29.7.0: - resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-resolve@29.7.0: - resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runner@29.7.0: - resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-runtime@29.7.0: - resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-snapshot@29.7.0: - resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-util@29.7.0: - resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-validate@29.7.0: - resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-watcher@29.7.0: - resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest-worker@29.7.0: - resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - jest@29.7.0: - resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 - peerDependenciesMeta: - node-notifier: - optional: true - - jimp-compact@0.16.1: - resolution: {integrity: sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==} - jiti@2.7.0: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true - joi@17.13.3: - resolution: {integrity: sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==} - - join-component@1.1.0: - resolution: {integrity: sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==} - - jose@4.15.9: - resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} - - jpeg-exif@1.1.4: - resolution: {integrity: sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - - js-base64@3.7.8: - resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} - - js-beautify@1.15.4: - resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} - engines: {node: '>=14'} - hasBin: true - - js-cookie@3.0.5: - resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} - engines: {node: '>=14'} - js-tokens@10.0.0: resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} @@ -7766,115 +2072,16 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} - hasBin: true - - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} - hasBin: true - - jsc-android@250231.0.0: - resolution: {integrity: sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw==} - - jsc-safe-url@0.2.4: - resolution: {integrity: sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==} - - jscodeshift@0.14.0: - resolution: {integrity: sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA==} - hasBin: true - peerDependencies: - '@babel/preset-env': ^7.1.6 - - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} hasBin: true - json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-parse-better-errors@1.0.2: - resolution: {integrity: sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==} - - json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - - json-schema-deref-sync@0.13.0: - resolution: {integrity: sha512-YBOEogm5w9Op337yb6pAT6ZXDqlxAsQCanM3grid8lMWNxRJO/zWEJi3ZzqDL8boWfwhTFym5EFrNgWwpqcBRg==} - engines: {node: '>=6.0.0'} - - json-schema-ref-resolver@3.0.0: - resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} - - json-schema-resolver@2.0.0: - resolution: {integrity: sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==} - engines: {node: '>=10'} - - json-schema-resolver@3.0.0: - resolution: {integrity: sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==} - engines: {node: '>=20'} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-schema-traverse@1.0.0: - resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} hasBin: true - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - - jsonfile@6.2.1: - resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} - - jsonwebtoken@9.0.3: - resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} - engines: {node: '>=12', npm: '>=6'} - - jsx-ast-utils@3.3.5: - resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} - engines: {node: '>=4.0'} - - jwa@2.0.1: - resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - - jwks-rsa@3.2.2: - resolution: {integrity: sha512-BqTyEDV+lS8F2trk3A+qJnxV5Q9EqKCBJOPti3W97r7qTympCZjb7h2X6f2kc+0K3rsSTY1/6YG2eaXKoj497w==} - engines: {node: '>=14'} - - jws@4.0.1: - resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - keyv@5.6.0: - resolution: {integrity: sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==} - - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - - kleur@3.0.3: - resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} - engines: {node: '>=6'} - kleur@4.1.5: resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} engines: {node: '>=6'} @@ -7886,63 +2093,22 @@ packages: knitwork@1.3.0: resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} - lan-network@0.2.1: - resolution: {integrity: sha512-ONPnazC96VKDntab9j9JKwIWhZ4ZUceB4A9Epu4Ssg0hYFmtHZSeQ+n15nIwTFmcBUKtExOer8WTJ4GF9MO64A==} - hasBin: true - lazystream@1.0.1: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} - leac@0.6.0: - resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - - leven@3.1.0: - resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} - engines: {node: '>=6'} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - libphonenumber-js@1.13.1: - resolution: {integrity: sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA==} - - libsql@0.5.29: - resolution: {integrity: sha512-8lMP8iMgiBzzoNbAPQ59qdVcj6UaE/Vnm+fiwX4doX4Narook0a4GPKWBEv+CR8a1OwbfkgL18uBfBjWdF0Fzg==} - cpu: [x64, arm64, wasm32, arm] - os: [darwin, linux, win32] - - light-my-request@6.6.0: - resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} - - lighthouse-logger@1.4.2: - resolution: {integrity: sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==} - lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] - lightningcss-darwin-arm64@1.19.0: - resolution: {integrity: sha512-wIJmFtYX0rXHsXHSr4+sC5clwblEMji7HHQ4Ub1/CznVRxtCFha6JIt5JZaNf8vQrfdZnBxLLC6R8pC818jXqg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] - lightningcss-darwin-x64@1.19.0: - resolution: {integrity: sha512-Lif1wD6P4poaw9c/4Uh2z+gmrWhw/HtXFoeZ3bEsv6Ia4tt8rOJBdkfVaUJ6VXmpKHALve+iTyP2+50xY1wKPw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} @@ -7955,60 +2121,30 @@ packages: cpu: [x64] os: [freebsd] - lightningcss-linux-arm-gnueabihf@1.19.0: - resolution: {integrity: sha512-P15VXY5682mTXaiDtbnLYQflc8BYb774j2R84FgDLJTN6Qp0ZjWEFyN1SPqyfTj2B2TFjRHRUvQSSZ7qN4Weig==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] - lightningcss-linux-arm64-gnu@1.19.0: - resolution: {integrity: sha512-zwXRjWqpev8wqO0sv0M1aM1PpjHz6RVIsBcxKszIG83Befuh4yNysjgHVplF9RTU7eozGe3Ts7r6we1+Qkqsww==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-arm64-musl@1.19.0: - resolution: {integrity: sha512-vSCKO7SDnZaFN9zEloKSZM5/kC5gbzUjoJQ43BvUpyTFUX7ACs/mDfl2Eq6fdz2+uWhUh7vf92c4EaaP4udEtA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - lightningcss-linux-x64-gnu@1.19.0: - resolution: {integrity: sha512-0AFQKvVzXf9byrXUq9z0anMGLdZJS+XSDqidyijI5njIwj6MdbvX2UZK/c4FfNmeRa2N/8ngTffoIuOUit5eIQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - lightningcss-linux-x64-musl@1.19.0: - resolution: {integrity: sha512-SJoM8CLPt6ECCgSuWe+g0qo8dqQYVcPiW2s19dxkmSI5+Uu1GIRzyKA0b7QqmEXolA+oSJhQqCmJpzjY4CuZAg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -8021,35 +2157,16 @@ packages: cpu: [arm64] os: [win32] - lightningcss-win32-x64-msvc@1.19.0: - resolution: {integrity: sha512-C+VuUTeSUOAaBZZOPT7Etn/agx/MatzJzGRkeV+zEABmPuntv1zihncsi+AyGmjkkzq3wVedEy7h0/4S84mUtg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] - lightningcss@1.19.0: - resolution: {integrity: sha512-yV5UR7og+Og7lQC+70DA7a8ta1uiOPnWPJfxa0wnxylev5qfo4P+4iMpzWAdYWOca4jdNQZii+bDL/l+4hUXIA==} - engines: {node: '>= 12.0.0'} - lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} - limiter@1.1.5: - resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} - - linebreak@1.1.0: - resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} - - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - listhen@1.10.0: resolution: {integrity: sha512-kfz4C0OrC6IpaVMtYDJtf6PFjurxe9NBBoDAh/o2p587INryFOO4DQ9OetbCdDrWFt1m1CJKvYrzkGsuPHw8nQ==} hasBin: true @@ -8058,367 +2175,46 @@ packages: resolution: {integrity: sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==} engines: {node: '>=14'} - locate-path@3.0.0: - resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} - engines: {node: '>=6'} - - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} - - 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==} - - lodash.clonedeep@4.5.0: - resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} - - lodash.debounce@4.0.8: - resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} - lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - lodash.includes@4.3.0: - resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} - lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - lodash.isboolean@3.0.3: - resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} - - lodash.isinteger@4.0.4: - resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} - - lodash.isnumber@3.0.3: - resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - - lodash.isstring@4.0.1: - resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - - lodash.memoize@4.1.2: - resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - - lodash.throttle@4.1.1: - resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} - lodash@4.18.1: resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} - log-symbols@2.2.0: - resolution: {integrity: sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==} - engines: {node: '>=4'} - - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - - logkitty@0.7.1: - resolution: {integrity: sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ==} - hasBin: true - - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - - loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true - lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.3.6: - resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + lru-cache@11.5.0: + resolution: {integrity: sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==} engines: {node: 20 || >=22} lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - - lru-memoizer@2.3.0: - resolution: {integrity: sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==} - - luxon@3.7.2: - resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==} - engines: {node: '>=12'} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} magicast@0.5.3: resolution: {integrity: sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==} - make-dir@2.1.0: - resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} - engines: {node: '>=6'} - make-dir@4.0.0: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - makeerror@1.0.12: - resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} - - marky@1.3.0: - resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} - - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - - md5-file@3.2.3: - resolution: {integrity: sha512-3Tkp1piAHaworfcCgH0jKbTvj1jWWFgbvh2cXaNCgHwyTCBxxvD1Y04rmfpvdPm1P4oXMOpm6+2H7sr7v9v8Fw==} - engines: {node: '>=0.10'} - hasBin: true - - md5@2.2.1: - resolution: {integrity: sha512-PlGG4z5mBANDGCKsYQe0CaUYHdZYZt8ZPZLmEt+Urf0W4GlpTX4HescwHU+dc9+Z/G/vZKYZYFrwgm9VxK6QOQ==} - - md5@2.3.0: - resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} - - md5hex@1.0.0: - resolution: {integrity: sha512-c2YOUbp33+6thdCUi34xIyOU/a7bvGKj/3DB1iaPMTuPHf/Q2d5s4sn1FaCOO43XkXggnb08y5W2PU8UNYNLKQ==} - mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - mdn-data@2.0.14: - resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} - - media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} - - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - - memory-cache@0.2.0: - resolution: {integrity: sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA==} - merge-anything@5.1.7: resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} engines: {node: '>=12.13'} - merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} - - merge-options@3.0.4: - resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} - engines: {node: '>=10'} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} - methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} - - metro-babel-transformer@0.80.12: - resolution: {integrity: sha512-YZziRs0MgA3pzCkkvOoQRXjIoVjvrpi/yRlJnObyIvMP6lFdtyG4nUGIwGY9VXnBvxmXD6mPY2e+NSw6JAyiRg==} - engines: {node: '>=18'} - - metro-babel-transformer@0.83.7: - resolution: {integrity: sha512-sBqBkt6kNut/88bv+Ucvm4yqdPetbvAEsHzi3MAgJEifOSYYzX5Z5Kgw3TFOrwf/mHJTOBG2ONlaMHoyfP15TA==} - engines: {node: '>=20.19.4'} - - metro-babel-transformer@0.84.4: - resolution: {integrity: sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-cache-key@0.80.12: - resolution: {integrity: sha512-o4BspKnugg/pE45ei0LGHVuBJXwRgruW7oSFAeSZvBKA/sGr0UhOGY3uycOgWInnS3v5yTTfiBA9lHlNRhsvGA==} - engines: {node: '>=18'} - - metro-cache-key@0.83.7: - resolution: {integrity: sha512-W1c2Nmx8MiJTJt+eWhMO08z9VKi3kZOaz99IYGdqeqDgY9j+yZjXl62rUav4Di0heZfh4/n2s722PqRL1OODeg==} - engines: {node: '>=20.19.4'} - - metro-cache-key@0.84.4: - resolution: {integrity: sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-cache@0.80.12: - resolution: {integrity: sha512-p5kNHh2KJ0pbQI/H7ZBPCEwkyNcSz7OUkslzsiIWBMPQGFJ/xArMwkV7I+GJcWh+b4m6zbLxE5fk6fqbVK1xGA==} - engines: {node: '>=18'} - - metro-cache@0.83.7: - resolution: {integrity: sha512-E9SRePXQ1Zvlj79VcOk57q7VC7rMHMFQ+jhmPHBiq+dJ0bJB5BL87lWZF6oh5X76Cci5tpDuQNaDwwuSCToEeg==} - engines: {node: '>=20.19.4'} - - metro-cache@0.84.4: - resolution: {integrity: sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-config@0.80.12: - resolution: {integrity: sha512-4rwOWwrhm62LjB12ytiuR5NgK1ZBNr24/He8mqCsC+HXZ+ATbrewLNztzbAZHtFsrxP4D4GLTGgh96pCpYLSAQ==} - engines: {node: '>=18'} - - metro-config@0.83.7: - resolution: {integrity: sha512-83mjWFbFOt2GeJ6pFIum5mSnc1uTsZJAtD8o4ej0s4NVsYsA7fB+pHvTfHhFrpeMONaobu2riKavkPei05Er/Q==} - engines: {node: '>=20.19.4'} - - metro-config@0.84.4: - resolution: {integrity: sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-core@0.80.12: - resolution: {integrity: sha512-QqdJ/yAK+IpPs2HU/h5v2pKEdANBagSsc6DRSjnwSyJsCoHlmyJKCaCJ7KhWGx+N4OHxh37hoA8fc2CuZbx0Fw==} - engines: {node: '>=18'} - - metro-core@0.83.7: - resolution: {integrity: sha512-6yn3w1wnltT6RQl7p7YES2l95ArC+mWrOssEiH8p5/DDrJS65/szf9LsC9JrBv8c5DdvSY3V3f0GRYg0Ox7hCg==} - engines: {node: '>=20.19.4'} - - metro-core@0.84.4: - resolution: {integrity: sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-file-map@0.80.12: - resolution: {integrity: sha512-sYdemWSlk66bWzW2wp79kcPMzwuG32x1ZF3otI0QZTmrnTaaTiGyhE66P1z6KR4n2Eu5QXiABa6EWbAQv0r8bw==} - engines: {node: '>=18'} - - metro-file-map@0.83.7: - resolution: {integrity: sha512-+j0F1m+FQYVAQ6syf+mwhIPV5GoFQrkInX8bppuc50IzNsZbMrp8R5H/Sx/K2daQ3YEa9F/XwkeZT8gzJfgeCw==} - engines: {node: '>=20.19.4'} - - metro-file-map@0.84.4: - resolution: {integrity: sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-minify-terser@0.80.12: - resolution: {integrity: sha512-muWzUw3y5k+9083ZoX9VaJLWEV2Jcgi+Oan0Mmb/fBNMPqP9xVDuy4pOMn/HOiGndgfh/MK7s4bsjkyLJKMnXQ==} - engines: {node: '>=18'} - - metro-minify-terser@0.83.7: - resolution: {integrity: sha512-MfJar2IS4tBRuLb9svwb0Gu5l9BsH+pcRm8eGcEi/wy8MzZinfinh5dFLt2nWkocnulIgtGB5NkFDdbXqMXKhQ==} - engines: {node: '>=20.19.4'} - - metro-minify-terser@0.84.4: - resolution: {integrity: sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-resolver@0.80.12: - resolution: {integrity: sha512-PR24gYRZnYHM3xT9pg6BdbrGbM/Cu1TcyIFBVlAk7qDAuHkUNQ1nMzWumWs+kwSvtd9eZGzHoucGJpTUEeLZAw==} - engines: {node: '>=18'} - - metro-resolver@0.83.7: - resolution: {integrity: sha512-WSJIENlMcoSsuz66IfBHOkgfp3KJt2UW2TnEHPf1b8pIG2eEXNOVmo2+03A0H17WY2XGXWgxL0CG7FAopqgB1A==} - engines: {node: '>=20.19.4'} - - metro-resolver@0.84.4: - resolution: {integrity: sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-runtime@0.80.12: - resolution: {integrity: sha512-LIx7+92p5rpI0i6iB4S4GBvvLxStNt6fF0oPMaUd1Weku7jZdfkCZzmrtDD9CSQ6EPb0T9NUZoyXIxlBa3wOCw==} - engines: {node: '>=18'} - - metro-runtime@0.83.7: - resolution: {integrity: sha512-9GKkJURaB2iyYoEExKnedzAHzxmKtSi+k0tsZUvMoU27tBZJElchYt7JH/Ai/XzYAI9lCAaV7u5HZSI8J5Z+wQ==} - engines: {node: '>=20.19.4'} - - metro-runtime@0.84.4: - resolution: {integrity: sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-source-map@0.80.12: - resolution: {integrity: sha512-o+AXmE7hpvM8r8MKsx7TI21/eerYYy2DCDkWfoBkv+jNkl61khvDHlQn0cXZa6lrcNZiZkl9oHSMcwLLIrFmpw==} - engines: {node: '>=18'} - - metro-source-map@0.83.7: - resolution: {integrity: sha512-JgA1h7oc1a1jydBe1GhVFsUoMYo3wLPk7oRA32rjlDsq+sP2JLt9x2p2lWbNSxTm/u8NV4VRid3hvEJgcX8tKw==} - engines: {node: '>=20.19.4'} - - metro-source-map@0.84.4: - resolution: {integrity: sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-symbolicate@0.80.12: - resolution: {integrity: sha512-/dIpNdHksXkGHZXARZpL7doUzHqSNxgQ8+kQGxwpJuHnDhGkENxB5PS2QBaTDdEcmyTMjS53CN1rl9n1gR6fmw==} - engines: {node: '>=18'} - hasBin: true - - metro-symbolicate@0.83.7: - resolution: {integrity: sha512-g4suyxw20WOHWI680c+Kq4wC/NF+Hx5pRH9afrMp+sMTxqLeKcPR1Xf4wMhsjlbvx7LbIREdke6q928jEjvJWw==} - engines: {node: '>=20.19.4'} - hasBin: true - - metro-symbolicate@0.84.4: - resolution: {integrity: sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - hasBin: true - - metro-transform-plugins@0.80.12: - resolution: {integrity: sha512-WQWp00AcZvXuQdbjQbx1LzFR31IInlkCDYJNRs6gtEtAyhwpMMlL2KcHmdY+wjDO9RPcliZ+Xl1riOuBecVlPA==} - engines: {node: '>=18'} - - metro-transform-plugins@0.83.7: - resolution: {integrity: sha512-Ss0FpBiZDjX2kwhukMDl5sNdYK8T/06IPqxNE4H6PTlRlfs9q11cef13c/xESY/Pm4VCkp1yJUZO3kXzvMxQFA==} - engines: {node: '>=20.19.4'} - - metro-transform-plugins@0.84.4: - resolution: {integrity: sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro-transform-worker@0.80.12: - resolution: {integrity: sha512-KAPFN1y3eVqEbKLx1I8WOarHPqDMUa8WelWxaJCNKO/yHCP26zELeqTJvhsQup+8uwB6EYi/sp0b6TGoh6lOEA==} - engines: {node: '>=18'} - - metro-transform-worker@0.83.7: - resolution: {integrity: sha512-UegCo7ygB2fT64mRK2nbAjQVJ1zSwIIHy8d96jJv2nKZFDaViYBiughEdu5HM/Ceq0WN3LZrZk3zhl9aoiLYFw==} - engines: {node: '>=20.19.4'} - - metro-transform-worker@0.84.4: - resolution: {integrity: sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - metro@0.80.12: - resolution: {integrity: sha512-1UsH5FzJd9quUsD1qY+zUG4JY3jo3YEMxbMYH9jT6NK3j4iORhlwTK8fYTfAUBhDKjgLfKjAh7aoazNE23oIRA==} - engines: {node: '>=18'} - hasBin: true - - metro@0.83.7: - resolution: {integrity: sha512-SPaPEyvTsTmd0LpT7RaZciQyDw2i/JB7+iY9L5VfBo72+psescFxBqpI1TL9dnL+pmnfkU+l/J1mEEGLeF65EQ==} - engines: {node: '>=20.19.4'} - hasBin: true - - metro@0.84.4: - resolution: {integrity: sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - hasBin: true - micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} @@ -8438,57 +2234,23 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} - mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} - mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} - mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} - mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} - hasBin: true - - mime@2.6.0: - resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} - engines: {node: '>=4.0.0'} - hasBin: true - - mime@3.0.0: - resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} - engines: {node: '>=10.0.0'} - hasBin: true - mime@4.1.0: resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} engines: {node: '>=16'} hasBin: true - mimic-fn@1.2.0: - resolution: {integrity: sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==} - engines: {node: '>=4'} - - mimic-fn@2.1.0: - resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} - engines: {node: '>=6'} - minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} - minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} - minimatch@5.1.9: resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} engines: {node: '>=10'} @@ -8497,142 +2259,25 @@ packages: resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} engines: {node: '>=16 || 14 >=14.17'} - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - minipass-collect@2.0.1: - resolution: {integrity: sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==} - engines: {node: '>=16 || 14 >=14.17'} - - minipass-flush@1.0.7: - resolution: {integrity: sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==} - engines: {node: '>= 8'} - - minipass-pipeline@1.2.4: - resolution: {integrity: sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==} - engines: {node: '>=8'} - - minipass@3.3.6: - resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==} - engines: {node: '>=8'} - - minipass@5.0.0: - resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} - engines: {node: '>=8'} - minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} - minizlib@2.1.2: - resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} - engines: {node: '>= 8'} - minizlib@3.1.0: resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} engines: {node: '>= 18'} - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - - mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - mlly@1.8.2: resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} - mnemonist@0.40.0: - resolution: {integrity: sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==} - - module-details-from-path@1.0.4: - resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} - - ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - msgpackr-extract@3.0.3: - resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} - hasBin: true - - msgpackr@2.0.1: - resolution: {integrity: sha512-9J+tqTEsbHqY8YohazYgty7LgerFIWxvMLpUjqETSmjHojtJm2WnX2kK/2a1fLI7CO7ERP1YSEUXMucz4j+yBA==} - - multitars@1.0.0: - resolution: {integrity: sha512-H/J4fMLedtudftaYMOg7ajzLYgT3/rwbWVJbqr/iUgB8DQztn38ys5HOqI1CzSxx8QhXXwOOnnBvd4v3jG5+Mg==} - - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} - - negotiator@0.6.4: - resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} - engines: {node: '>= 0.6'} - - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - - neo-async@2.6.2: - resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - - nested-error-stacks@2.0.1: - resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} - - next-auth@4.24.14: - resolution: {integrity: sha512-YRz6xFDXKUwiXSMMChbrBEWyFktZ1qZXEgeSHQQ3nsy08B4c/xLk6REeutRsIFwkjY/1+ShHnu07DN3JeJguig==} - peerDependencies: - '@auth/core': 0.34.3 - next: ^12.2.5 || ^13 || ^14 || ^15 || ^16 - nodemailer: ^7.0.7 - react: ^17.0.2 || ^18 || ^19 - react-dom: ^17.0.2 || ^18 || ^19 - peerDependenciesMeta: - '@auth/core': - optional: true - nodemailer: - optional: true - - next@16.2.6: - resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==} - engines: {node: '>=20.9.0'} - hasBin: true - peerDependencies: - '@opentelemetry/api': ^1.1.0 - '@playwright/test': ^1.51.1 - babel-plugin-react-compiler: '*' - react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0 - sass: ^1.3.0 - peerDependenciesMeta: - '@opentelemetry/api': - optional: true - '@playwright/test': - optional: true - babel-plugin-react-compiler: - optional: true - sass: - optional: true - - nice-try@1.0.5: - resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} - nitropack@2.13.4: resolution: {integrity: sha512-tX7bT6zxNeMwkc6hxHiZeUoTOjVrcjoh1Z3cmxOlodIqjl4HISgqfGOmkWSayky3Nv9Z5+KQH52F8nmXJY5AAA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -8643,31 +2288,9 @@ packages: xml2js: optional: true - nocache@3.0.4: - resolution: {integrity: sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==} - engines: {node: '>=12.0.0'} - - node-abort-controller@3.1.1: - resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} - - node-addon-api@6.1.0: - resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} - node-addon-api@7.1.1: resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-cache@5.1.2: - resolution: {integrity: sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==} - engines: {node: '>= 8.0.0'} - - node-dir@0.1.17: - resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} - engines: {node: '>= 0.10.5'} - - node-exports-info@1.6.0: - resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} - engines: {node: '>= 0.4'} - node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} @@ -8684,35 +2307,16 @@ packages: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - - node-gyp-build@3.9.0: - resolution: {integrity: sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==} - hasBin: true - node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true - node-int64@0.4.0: - resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-mock-http@1.0.4: resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} - node-releases@2.0.44: - resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} - - node-stream-zip@1.15.0: - resolution: {integrity: sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==} - engines: {node: '>=0.12.0'} - - nopt@7.2.1: - resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - hasBin: true + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} nopt@8.1.0: resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} @@ -8723,90 +2327,6 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - npm-package-arg@11.0.3: - resolution: {integrity: sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==} - engines: {node: ^16.14.0 || >=18.0.0} - - npm-package-arg@7.0.0: - resolution: {integrity: sha512-xXxr8y5U0kl8dVkz2oK7yZjPBvqM2fwaO5l3Yg13p03v8+E3qQcD0JNhHzjL1vyGgxcKkD0cco+NLR72iuPk3g==} - - npm-run-path@2.0.2: - resolution: {integrity: sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==} - engines: {node: '>=4'} - - npm-run-path@4.0.1: - resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} - engines: {node: '>=8'} - - nth-check@2.1.1: - resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - - nullthrows@1.1.1: - resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} - - nypm@0.6.6: - resolution: {integrity: sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==} - engines: {node: '>=18'} - hasBin: true - - oauth@0.9.15: - resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} - - ob1@0.80.12: - resolution: {integrity: sha512-VMArClVT6LkhUGpnuEoBuyjG9rzUyEzg4PDkav6wK1cLhOK02gPCYFxoiB4mqVnrMhDpIzJcrGNAMVi9P+hXrw==} - engines: {node: '>=18'} - - ob1@0.83.7: - resolution: {integrity: sha512-9M5kpuOLyTPogMtZiQUIxdAZxl7Dxs6tVBbJErSumsqGMuhVSoUbkfeZ3XNPpLpwBBtqY5QDUzGwggLHX3slQg==} - engines: {node: '>=20.19.4'} - - ob1@0.84.4: - resolution: {integrity: sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-hash@2.2.0: - resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} - engines: {node: '>= 6'} - - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - object-is@1.1.6: - resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} - engines: {node: '>= 0.4'} - - object-keys@1.1.1: - resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} - engines: {node: '>= 0.4'} - - object.assign@4.1.7: - resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} - engines: {node: '>= 0.4'} - - object.entries@1.1.9: - resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} - engines: {node: '>= 0.4'} - - object.fromentries@2.0.8: - resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} - engines: {node: '>= 0.4'} - - object.values@1.2.1: - resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} - engines: {node: '>= 0.4'} - - obliterator@2.0.5: - resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} - obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} @@ -8816,37 +2336,10 @@ packages: ohash@2.0.11: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - oidc-token-hash@5.2.0: - resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==} - engines: {node: ^10.13.0 || >=12.0.0} - - on-exit-leak-free@2.1.2: - resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} - engines: {node: '>=14.0.0'} - - on-finished@2.3.0: - resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} - engines: {node: '>= 0.8'} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} - on-headers@1.1.0: - resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - onetime@2.0.1: - resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} - engines: {node: '>=4'} - - onetime@5.1.2: - resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} - engines: {node: '>=6'} - oniguruma-to-es@2.3.0: resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==} @@ -8854,143 +2347,16 @@ packages: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} - open@6.4.0: - resolution: {integrity: sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg==} - engines: {node: '>=8'} - - open@7.4.2: - resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} - engines: {node: '>=8'} - - open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} - - openapi-types@12.1.3: - resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} - - openid-client@5.7.1: - resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - ora@3.4.0: - resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} - engines: {node: '>=6'} - - ora@5.4.1: - resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} - engines: {node: '>=10'} - - os-homedir@1.0.2: - resolution: {integrity: sha512-B5JU3cabzk8c67mRRd3ECmROafjYMXbuzlwtqdM8IbS8ktlTix8aFGb2bAGKrSRIlnfKwovGUUr72JUPyOb6kQ==} - engines: {node: '>=0.10.0'} - - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - - osenv@0.1.5: - resolution: {integrity: sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==} - deprecated: This package is no longer supported. - - own-keys@1.0.1: - resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} - engines: {node: '>= 0.4'} - - oxc-parser@0.129.0: - resolution: {integrity: sha512-S6eFI+VLkpyA+/Lf8z6qURjDV6Mgo74SLNznNopHTlQW3hedv2MB/z31kBRuBCCTqZN9HHdva0ojljEhPnBKFA==} - engines: {node: ^20.19.0 || >=22.12.0} - - p-finally@1.0.0: - resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} - engines: {node: '>=4'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@3.0.0: - resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} - engines: {node: '>=6'} - - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - p-map@4.0.0: - resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} - engines: {node: '>=10'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - pako@0.2.9: - resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} - - pako@1.0.11: - resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-json@4.0.0: - resolution: {integrity: sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==} - engines: {node: '>=4'} - - parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} - - parse-png@2.1.0: - resolution: {integrity: sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==} - engines: {node: '>=10'} - parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - parseley@0.12.1: - resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} - parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - path-exists@3.0.0: - resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} - engines: {node: '>=4'} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-expression-matcher@1.5.0: - resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} - engines: {node: '>=14.0.0'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - path-key@2.0.1: - resolution: {integrity: sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==} - engines: {node: '>=4'} - path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -9006,45 +2372,18 @@ packages: resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} engines: {node: 18 || 20 || >=22} - path-to-regexp@0.1.13: - resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} - path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - pdfkit@0.15.2: - resolution: {integrity: sha512-s3GjpdBFSCaeDSX/v73MI5UsPqH1kjKut2AXCgxQ5OH10lPVOu5q5vLAG0OCpz/EYqKsTSw1WHpENqMvp43RKg==} - - peberminta@0.9.0: - resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} - - perfect-debounce@1.0.0: - resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} - perfect-debounce@2.1.0: resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-protocol@1.13.0: - resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -9052,247 +2391,44 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@3.0.2: - resolution: {integrity: sha512-cfDHL6LStTEKlNilboNtobT/kEa30PtAf2Q1OgszfrG/rpVl1xaFWT9ktfkS306GmHgmnad1Sw4wabhlvFtsTw==} - engines: {node: '>=10'} - picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} - - pino-abstract-transport@3.0.0: - resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} - - pino-std-serializers@7.1.0: - resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} - - pino@10.3.1: - resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} - hasBin: true - - pirates@4.0.7: - resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} - engines: {node: '>= 6'} - - pkg-dir@3.0.0: - resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} - engines: {node: '>=6'} - - pkg-dir@4.2.0: - resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} - engines: {node: '>=8'} - pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} pkg-types@2.3.1: resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} - plist@3.1.1: - resolution: {integrity: sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==} - engines: {node: '>=10.4.0'} - - png-js@1.1.0: - resolution: {integrity: sha512-PM/uYGzGdNSzqeOgly68+6wKQDL1SY0a/N+OEa/+br6LnHWOAJB0Npiamnodfq3jd2LS/i2fMeOKSAILjA+m5Q==} - - pngjs@3.4.0: - resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} - engines: {node: '>=4.0.0'} - - possible-typed-array-names@1.1.0: - resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} - engines: {node: '>= 0.4'} - - postcss@8.4.31: - resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - postcss@8.4.49: - resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} - engines: {node: ^10 || ^12 || >=14} - - postcss@8.5.14: - resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} - engines: {node: ^10 || ^12 || >=14} - - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-bytea@1.0.1: - resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} - engines: {node: '>=0.10.0'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - powershell-utils@0.1.0: resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} engines: {node: '>=20'} - pprof-format@2.2.1: - resolution: {integrity: sha512-p4tVN7iK19ccDqQv8heyobzUmbHyds4N2FI6aBMcXz6y99MglTWDxIyhFkNaLeEXs6IFUEzT0zya0icbSLLY0g==} - - preact-render-to-string@5.2.6: - resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} - peerDependencies: - preact: '>=10' - - preact@10.29.1: - resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - pretty-bytes@5.6.0: - resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} - engines: {node: '>=6'} - pretty-bytes@7.1.0: resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} engines: {node: '>=20'} - pretty-format@24.9.0: - resolution: {integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==} - engines: {node: '>= 6'} - - pretty-format@26.6.2: - resolution: {integrity: sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==} - engines: {node: '>= 10'} - - pretty-format@29.7.0: - resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - - pretty-format@3.8.0: - resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} - - prisma@5.22.0: - resolution: {integrity: sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==} - engines: {node: '>=16.13'} - hasBin: true - - prisma@6.19.3: - resolution: {integrity: sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==} - engines: {node: '>=18.18'} - hasBin: true - peerDependencies: - typescript: '>=5.1.0' - peerDependenciesMeta: - typescript: - optional: true - - proc-log@4.2.0: - resolution: {integrity: sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - process-warning@4.0.1: - resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} - - process-warning@5.0.0: - resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} - process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - - promise-limit@2.7.0: - resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==} - - promise@7.3.1: - resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} - - promise@8.3.0: - resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==} - - prompts@2.4.2: - resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} - engines: {node: '>= 6'} - - prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - proto-list@1.2.4: - resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} - - proto3-json-serializer@2.0.2: - resolution: {integrity: sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==} - engines: {node: '>=14.0.0'} - - protobufjs@7.5.8: - resolution: {integrity: sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==} - engines: {node: '>=12.0.0'} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} - - proxy-from-env@2.1.0: - resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} - engines: {node: '>=10'} - - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - pure-rand@6.1.0: - resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - - qrcode-terminal@0.11.0: - resolution: {integrity: sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==} - hasBin: true - - qs@6.15.1: - resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} - engines: {node: '>=0.6'} - quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - query-string@7.1.3: - resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} - engines: {node: '>=6'} - - querystring@0.2.1: - resolution: {integrity: sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg==} - engines: {node: '>=0.4.x'} - deprecated: The querystring API is considered Legacy. new code should use the URLSearchParams API instead. - - querystringify@2.2.0: - resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - queue@6.0.2: - resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} - - quick-format-unescaped@4.0.4: - resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} - radix3@1.1.2: resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} @@ -9300,141 +2436,12 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - rate-limiter-flexible@5.0.5: - resolution: {integrity: sha512-+/dSQfo+3FYwYygUs/V2BBdwGa9nFtakDwKt4l0bnvNB53TNT++QSFewwHX9qXrZJuMe9j+TUaU21lm5ARgqdQ==} - - raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} - engines: {node: '>= 0.8'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - - rc9@2.1.2: - resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - rc9@3.0.1: resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} - rc@1.2.8: - resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} - hasBin: true - - react-devtools-core@5.3.2: - resolution: {integrity: sha512-crr9HkVrDiJ0A4zot89oS0Cgv0Oa4OG1Em4jit3P3ZxZSKPMYyMjfwMqgcJna9o625g8oN87rBm8SWWrSTBZxg==} - - react-devtools-core@6.1.5: - resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==} - - react-dom@19.2.6: - resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} - peerDependencies: - react: ^19.2.6 - - react-freeze@1.0.4: - resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==} - engines: {node: '>=10'} - peerDependencies: - react: '>=17.0.0' - - react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - - react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} - - react-is@18.3.1: - resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - - react-native-gesture-handler@2.16.2: - resolution: {integrity: sha512-vGFlrDKlmyI+BT+FemqVxmvO7nqxU33cgXVsn6IKAFishvlG3oV2Ds67D5nPkHMea8T+s1IcuMm0bF8ntZtAyg==} - peerDependencies: - react: '*' - react-native: '*' - - react-native-reanimated@3.10.1: - resolution: {integrity: sha512-sfxg6vYphrDc/g4jf/7iJ7NRi+26z2+BszPmvmk0Vnrz6FL7HYljJqTf531F1x6tFmsf+FEAmuCtTUIXFLVo9w==} - peerDependencies: - '@babel/core': ^7.0.0-0 - react: '*' - react-native: '*' - - react-native-safe-area-context@4.10.5: - resolution: {integrity: sha512-Wyb0Nqw2XJ6oZxW/cK8k5q7/UAhg/wbEG6UVf89rQqecDZTDA5ic//P9J6VvJRVZerzGmxWQpVuM7f+PRYUM4g==} - peerDependencies: - react: '*' - react-native: '*' - - react-native-screens@3.31.0: - resolution: {integrity: sha512-TzA52rgh64gdMjcv+rnRSFbAocQpkKjUUgXmhz8ZJHfdzz36SSBJ6kmqVY3qF1si32oOyMDjUdHscPK7xfKCgQ==} - peerDependencies: - react: '*' - react-native: '*' - - react-native-svg@15.2.0: - resolution: {integrity: sha512-R0E6IhcJfVLsL0lRmnUSm72QO+mTqcAOM5Jb8FVGxJqX3NfJMlMP0YyvcajZiaRR8CqQUpEoqrY25eyZb006kw==} - peerDependencies: - react: '*' - react-native: '*' - - react-native@0.74.5: - resolution: {integrity: sha512-Bgg2WvxaGODukJMTZFTZBNMKVaROHLwSb8VAGEdrlvKwfb1hHg/3aXTUICYk7dwgAnb+INbGMwnF8yeAgIUmqw==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - '@types/react': ^18.2.6 - react: 18.2.0 - peerDependenciesMeta: - '@types/react': - optional: true - - react-native@0.85.3: - resolution: {integrity: sha512-HN/fGC+3nZVcDNcw7gfbM/DuqZAvI9Mz+/SxuhODaua4JY0BPzhfTzWXRyTR4mRgMHmShTPpH2PYMTxvZrsdZA==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - hasBin: true - peerDependencies: - '@react-native/jest-preset': 0.85.3 - '@types/react': ^19.1.1 - react: ^19.2.3 - peerDependenciesMeta: - '@react-native/jest-preset': - optional: true - '@types/react': - optional: true - - react-promise-suspense@0.3.4: - resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} - - react-refresh@0.14.2: - resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} - engines: {node: '>=0.10.0'} - - react-shallow-renderer@16.15.0: - resolution: {integrity: sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==} - peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 - - react-test-renderer@18.2.0: - resolution: {integrity: sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==} - peerDependencies: - react: ^18.2.0 - - react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} - - react@19.2.6: - resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} - engines: {node: '>=0.10.0'} - readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - readable-stream@4.7.0: resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -9442,28 +2449,10 @@ packages: readdir-glob@1.1.3: resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - readdirp@5.0.0: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} - readline@1.3.0: - resolution: {integrity: sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==} - - real-require@0.2.0: - resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} - engines: {node: '>= 12.13.0'} - - real-require@1.0.0: - resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} - - recast@0.21.5: - resolution: {integrity: sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==} - engines: {node: '>= 4'} - redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -9472,20 +2461,6 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} - reflect.getprototypeof@1.0.10: - resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} - engines: {node: '>= 0.4'} - - regenerate-unicode-properties@10.2.2: - resolution: {integrity: sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==} - engines: {node: '>=4'} - - regenerate@1.4.2: - resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==} - - regenerator-runtime@0.13.11: - resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} - regex-recursion@5.1.1: resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} @@ -9495,129 +2470,19 @@ packages: regex@5.1.1: resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==} - regexp.prototype.flags@1.5.4: - resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} - engines: {node: '>= 0.4'} - - regexpu-core@6.4.0: - resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==} - engines: {node: '>=4'} - - regjsgen@0.8.0: - resolution: {integrity: sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==} - - regjsparser@0.13.1: - resolution: {integrity: sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==} - hasBin: true - - remove-trailing-slash@0.1.1: - resolution: {integrity: sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} - - require-in-the-middle@7.5.2: - resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} - engines: {node: '>=8.6.0'} - - require-main-filename@2.0.0: - resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} - - requireg@0.2.2: - resolution: {integrity: sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==} - engines: {node: '>= 4.0.0'} - - requires-port@1.0.0: - resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} - - resend@3.5.0: - resolution: {integrity: sha512-bKu4LhXSecP6krvhfDzyDESApYdNfjirD5kykkT1xO0Cj9TKSiGh5Void4pGTs3Am+inSnp4dg0B5XzdwHBJOQ==} - engines: {node: '>=18'} - - resolve-cwd@3.0.0: - resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} - engines: {node: '>=8'} - - resolve-from@3.0.0: - resolution: {integrity: sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==} - engines: {node: '>=4'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - resolve-workspace-root@2.0.1: - resolution: {integrity: sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==} - - resolve.exports@2.0.3: - resolution: {integrity: sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==} - engines: {node: '>=10'} - resolve@1.22.12: resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} engines: {node: '>= 0.4'} hasBin: true - resolve@1.7.1: - resolution: {integrity: sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==} - - resolve@2.0.0-next.7: - resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} - engines: {node: '>= 0.4'} - hasBin: true - - restore-cursor@2.0.0: - resolution: {integrity: sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==} - engines: {node: '>=4'} - - restore-cursor@3.1.0: - resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} - engines: {node: '>=8'} - - restructure@2.0.1: - resolution: {integrity: sha512-e0dOpjm5DseomnXx2M5lpdZ5zoHqF1+bqdMJUohoYVVQa7cBdnk7fdmeI6byNWP/kiME72EeTiSypTCVnpLiDg==} - - ret@0.5.0: - resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} - engines: {node: '>=10'} - - retry-request@7.0.2: - resolution: {integrity: sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==} - engines: {node: '>=14'} - - retry@0.13.1: - resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} - engines: {node: '>= 4'} - reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - - rimraf@2.6.3: - resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - - rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - deprecated: Rimraf versions prior to v4 are no longer supported - hasBin: true - rollup-plugin-visualizer@7.0.1: resolution: {integrity: sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==} engines: {node: '>=22'} @@ -9646,102 +2511,28 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - safe-array-concat@1.1.4: - resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} - engines: {node: '>=0.4'} - safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safe-push-apply@1.0.0: - resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} - engines: {node: '>= 0.4'} - - safe-regex-test@1.1.0: - resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} - engines: {node: '>= 0.4'} - - safe-regex2@5.1.1: - resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} - hasBin: true - - safe-stable-stringify@2.5.0: - resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} - engines: {node: '>=10'} - - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - - sax@1.6.0: - resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} - engines: {node: '>=11.0.0'} - - scheduler@0.23.2: - resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} - - scheduler@0.24.0-canary-efb381bbf-20230505: - resolution: {integrity: sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==} - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - scmp@2.1.0: - resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==} - deprecated: Just use Node.js's crypto.timingSafeEqual() - scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} - secure-json-parse@2.7.0: - resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} - - secure-json-parse@4.1.0: - resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} - - selderee@0.11.0: - resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} - - selfsigned@2.4.1: - resolution: {integrity: sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==} - engines: {node: '>=10'} - - semver@5.7.2: - resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} - hasBin: true - semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.5.3: - resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} hasBin: true - semver@7.8.0: - resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} - engines: {node: '>=10'} - hasBin: true - - send@0.18.0: - resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} - engines: {node: '>= 0.8.0'} - - send@0.19.2: - resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} - engines: {node: '>= 0.8.0'} - send@1.2.1: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} - serialize-error@2.1.0: - resolution: {integrity: sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==} - engines: {node: '>=0.10.0'} - serialize-javascript@7.0.5: resolution: {integrity: sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==} engines: {node: '>=20.0.0'} @@ -9759,129 +2550,41 @@ packages: serve-placeholder@2.0.2: resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} - serve-static@1.16.3: - resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} - engines: {node: '>= 0.8.0'} - serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} - set-blocking@2.0.0: - resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} - - set-cookie-parser@2.7.2: - resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - - set-function-length@1.2.2: - resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} - engines: {node: '>= 0.4'} - - set-function-name@2.0.2: - resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} - engines: {node: '>= 0.4'} - - set-proto@1.0.0: - resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} - engines: {node: '>= 0.4'} - - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shallow-clone@3.0.1: - resolution: {integrity: sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==} - engines: {node: '>=8'} - - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - - shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} - shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.8.3: - resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==} - engines: {node: '>= 0.4'} - shiki@1.29.2: resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==} - shimmer@1.2.1: - resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} - - side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - simple-plist@1.3.1: - resolution: {integrity: sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==} - - simple-swizzle@0.2.4: - resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} - - sisteransi@1.0.5: - resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - slash@5.1.0: resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} engines: {node: '>=14.16'} - slice-ansi@2.1.0: - resolution: {integrity: sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==} - engines: {node: '>=6'} - - slugify@1.6.9: - resolution: {integrity: sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==} - engines: {node: '>=8.0.0'} - smob@1.6.2: resolution: {integrity: sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==} engines: {node: '>=20.0.0'} - solid-js@1.9.12: - resolution: {integrity: sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==} + solid-js@1.9.13: + resolution: {integrity: sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==} solid-refresh@0.6.3: resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} @@ -9894,23 +2597,13 @@ packages: peerDependencies: solid-js: ^1.7 - sonic-boom@4.2.1: - resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - source-map-support@0.5.13: - resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} - source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - source-map@0.5.7: - resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} - engines: {node: '>=0.10.0'} - source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} @@ -9922,54 +2615,20 @@ packages: space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - spark-md5@3.0.2: - resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==} - - split-on-first@1.1.0: - resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} - engines: {node: '>=6'} - - split2@4.2.0: - resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} - engines: {node: '>= 10.x'} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - srvx@0.9.8: resolution: {integrity: sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ==} engines: {node: '>=20.16.0'} hasBin: true - ssri@10.0.6: - resolution: {integrity: sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - stack-utils@2.0.6: - resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} - engines: {node: '>=10'} - stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} - stacktrace-parser@0.1.11: - resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} - engines: {node: '>=6'} - standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - statuses@1.5.0: - resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} - engines: {node: '>= 0.6'} - - statuses@2.0.1: - resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} - engines: {node: '>= 0.8'} - statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -9977,35 +2636,9 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} - stop-iteration-iterator@1.1.0: - resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} - engines: {node: '>= 0.4'} - - stream-buffers@2.2.0: - resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} - engines: {node: '>= 0.10.0'} - - stream-events@1.0.5: - resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==} - - stream-shift@1.0.3: - resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} - - stream-wormhole@1.1.0: - resolution: {integrity: sha512-gHFfL3px0Kctd6Po0M8TzEvt3De/xu6cnRrjlfYNhwbhLPLwigI2t1nc6jrzNuaYg5C4YF78PPFuQPzRiqn9ew==} - engines: {node: '>=4.0.0'} - streamx@2.25.0: resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} - strict-uri-encode@2.0.0: - resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} - engines: {node: '>=4'} - - string-length@4.0.2: - resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} - engines: {node: '>=10'} - string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -10018,25 +2651,6 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} - string.prototype.matchall@4.0.12: - resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} - engines: {node: '>= 0.4'} - - string.prototype.repeat@1.0.0: - resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} - - string.prototype.trim@1.2.10: - resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} - engines: {node: '>= 0.4'} - - string.prototype.trimend@1.0.9: - resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} - engines: {node: '>= 0.4'} - - string.prototype.trimstart@1.0.8: - resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} - engines: {node: '>= 0.4'} - string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -10046,10 +2660,6 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - strip-ansi@5.2.0: - resolution: {integrity: sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==} - engines: {node: '>=6'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -10058,92 +2668,17 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} - strip-bom@4.0.0: - resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} - engines: {node: '>=8'} - - strip-eof@1.0.0: - resolution: {integrity: sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==} - engines: {node: '>=0.10.0'} - - strip-final-newline@2.0.0: - resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} - engines: {node: '>=6'} - - strip-json-comments@2.0.1: - resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} - engines: {node: '>=0.10.0'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - stripe@14.25.0: - resolution: {integrity: sha512-wQS3GNMofCXwH8TSje8E1SE8zr6ODiGtHQgPtO95p9Mb4FhKC9jvXR2NUTpZ9ZINlckJcFidCmaTFV4P6vsb9g==} - engines: {node: '>=12.*'} - - strnum@1.1.2: - resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} - - strnum@2.3.0: - resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} - - structured-headers@0.4.1: - resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==} - - stubs@3.0.0: - resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==} - - styled-jsx@5.1.6: - resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} - engines: {node: '>= 12.0.0'} - peerDependencies: - '@babel/core': '*' - babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0' - peerDependenciesMeta: - '@babel/core': - optional: true - babel-plugin-macros: - optional: true - - sucrase@3.34.0: - resolution: {integrity: sha512-70/LQEZ07TEcxiU2dz51FKaE6hCTWC6vr7FOk3Gr0U60C3shtAN+H+BFr9XlYe5xqf3RA8nrc+VIwzCfnxuXJw==} - engines: {node: '>=8'} - hasBin: true - - sucrase@3.35.1: - resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - - sudo-prompt@9.2.1: - resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==} - deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. - supports-color@10.2.2: resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} engines: {node: '>=18'} - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - supports-hyperlinks@2.3.0: - resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==} - engines: {node: '>=8'} - supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} @@ -10152,93 +2687,37 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tar-stream@3.2.0: resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} - tar@6.2.1: - resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} - engines: {node: '>=10'} - deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - tar@7.5.15: resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==} engines: {node: '>=18'} - teeny-request@9.0.0: - resolution: {integrity: sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==} - engines: {node: '>=14'} - teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - temp-dir@1.0.0: - resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} - engines: {node: '>=4'} - - temp-dir@2.0.0: - resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==} - engines: {node: '>=8'} - - temp@0.8.4: - resolution: {integrity: sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==} - engines: {node: '>=6.0.0'} - - tempy@0.3.0: - resolution: {integrity: sha512-WrH/pui8YCwmeiAoxV+lpRH9HpRtgBhSR2ViBPgpGb/wnYDzp21R4MN45fsCGvLROvY67o3byhJRYRONJyImVQ==} - engines: {node: '>=8'} - - tempy@0.7.1: - resolution: {integrity: sha512-vXPxwOyaNVi9nyczO16mxmHGpl6ASC5/TVhRRHpqeYHvKQm58EaWNvZXxAhR0lYYnBOQFjXjhzeLsaXdjxLjRg==} - engines: {node: '>=10'} - - terminal-link@2.1.1: - resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} - engines: {node: '>=8'} - terracotta@1.1.0: resolution: {integrity: sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww==} engines: {node: '>=10'} peerDependencies: solid-js: ^1.8 - terser@5.47.1: - resolution: {integrity: sha512-tPbLXTI6ohPASb/1YViL428oEHu6/qv1OxqYnfaonVCFHqx4+wCd95pHrQWsL5X4pl90CTyW9piSAsS2L0VoMw==} + terser@5.48.0: + resolution: {integrity: sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==} engines: {node: '>=10'} hasBin: true - test-exclude@6.0.0: - resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} - engines: {node: '>=8'} - text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} - text-decoding@1.0.0: - resolution: {integrity: sha512-/0TJD42KDnVwKmDK6jj3xP7E2MG7SHAOG4tyTgyUCRPdHwvkquYNLEQltmdMa3owq3TkddCVcTsoctJI8VQNKA==} - - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - - thread-stream@4.1.0: - resolution: {integrity: sha512-Bw6h2iBDt16v6iHLChBIoVYU8CBo9GPsW8TG7h1hRVhqKhIkH6N8qkxNSmiOZTKsCLPbtWG4ViWLkU6KeKXpig==} - engines: {node: '>=20'} - - throat@5.0.0: - resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} - - through2@2.0.5: - resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} - - tiny-inflate@1.0.3: - resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -10249,8 +2728,8 @@ packages: resolution: {integrity: sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==} engines: {node: ^16.14.0 || >= 17.3.0} - tinyexec@1.1.2: - resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + tinyexec@1.2.2: + resolution: {integrity: sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==} engines: {node: '>=18'} tinyglobby@0.2.16: @@ -10261,230 +2740,51 @@ packages: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tmpl@1.0.5: - resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} - to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - toad-cache@3.7.0: - resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} - engines: {node: '>=12'} - toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - toqr@0.1.1: - resolution: {integrity: sha512-FWAPzCIHZHnrE/5/w9MPk0kK25hSQSH2IKhYh9PyjS3SG/+IEMvlwIHbhz+oF7xl54I+ueZlVnMjyzdSwLmAwA==} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - traverse@0.6.11: - resolution: {integrity: sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==} - engines: {node: '>= 0.4'} - trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - trim-right@1.0.1: - resolution: {integrity: sha512-WZGXGstmCWgeevgTL54hrCuw1dyMQIzWy7ZfqRJfSmJZBwklI15egmQytFP6bPidmw3M8d5yEowl1niq4vmqZw==} - engines: {node: '>=0.10.0'} - - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - - ts-jest@29.4.9: - resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==} - engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + turbo@2.9.14: + resolution: {integrity: sha512-BQqXRr4UoWI3UPFrtznCLykYHxwxWh53iCB57x092jPMjIlW1wnm3N895g5irpiXmnxUhREBB0n6+y8BHhs4nw==} hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/transform': ^29.0.0 || ^30.0.0 - '@jest/types': ^29.0.0 || ^30.0.0 - babel-jest: ^29.0.0 || ^30.0.0 - esbuild: '*' - jest: ^29.0.0 || ^30.0.0 - jest-util: ^29.0.0 || ^30.0.0 - typescript: '>=4.3 <7' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/transform': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - jest-util: - optional: true - - ts-node@10.9.2: - resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} - hasBin: true - peerDependencies: - '@swc/core': '>=1.2.50' - '@swc/wasm': '>=1.2.50' - '@types/node': '*' - typescript: '>=2.7' - peerDependenciesMeta: - '@swc/core': - optional: true - '@swc/wasm': - optional: true - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - tsx@4.22.0: - resolution: {integrity: sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==} - engines: {node: '>=18.0.0'} - hasBin: true - - turbo@2.9.12: - resolution: {integrity: sha512-lCPgus1NuTiBdaITWqzSH/Ff6HVL8HHGBtOXHg1dHRfcshN79XkygSdh0M6g8b0td91ILLG5MTkLOkp5UvyPJw==} - hasBin: true - - twilio@4.23.0: - resolution: {integrity: sha512-LdNBQfOe0dY2oJH2sAsrxazpgfFQo5yXGxe96QA8UWB5uu+433PrUbkv8gQ5RmrRCqUTPQ0aOrIyAdBr1aB03Q==} - engines: {node: '>=14.0'} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - type-fest@0.16.0: - resolution: {integrity: sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==} - engines: {node: '>=10'} - - type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - - type-fest@0.21.3: - resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} - engines: {node: '>=10'} - - type-fest@0.3.1: - resolution: {integrity: sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==} - engines: {node: '>=6'} - - type-fest@0.7.1: - resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} - engines: {node: '>=8'} - - type-fest@4.41.0: - resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} - engines: {node: '>=16'} type-fest@5.6.0: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} - type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} - - type-is@2.1.0: - resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} - engines: {node: '>= 18'} - - typed-array-buffer@1.0.3: - resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} - engines: {node: '>= 0.4'} - - typed-array-byte-length@1.0.3: - resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} - engines: {node: '>= 0.4'} - - typed-array-byte-offset@1.0.4: - resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} - engines: {node: '>= 0.4'} - - typed-array-length@1.0.7: - resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} - engines: {node: '>= 0.4'} - - typedarray.prototype.slice@1.0.5: - resolution: {integrity: sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==} - engines: {node: '>= 0.4'} - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - ua-parser-js@0.7.41: - resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==} - hasBin: true - - ua-parser-js@1.0.41: - resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} - hasBin: true - ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} - uglify-js@3.19.3: - resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} - engines: {node: '>=0.8.0'} - hasBin: true - ultrahtml@1.6.0: resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} - unbox-primitive@1.1.0: - resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} - engines: {node: '>= 0.4'} - uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} unctx@2.5.0: resolution: {integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} unenv@2.0.0-rc.24: resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} - unicode-canonical-property-names-ecmascript@2.0.1: - resolution: {integrity: sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==} - engines: {node: '>=4'} - - unicode-match-property-ecmascript@2.0.0: - resolution: {integrity: sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==} - engines: {node: '>=4'} - - unicode-match-property-value-ecmascript@2.2.1: - resolution: {integrity: sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==} - engines: {node: '>=4'} - - unicode-properties@1.4.1: - resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} - - unicode-property-aliases-ecmascript@2.2.0: - resolution: {integrity: sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==} - engines: {node: '>=4'} - - unicode-trie@2.0.0: - resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} - unicorn-magic@0.4.0: resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} engines: {node: '>=20'} @@ -10501,22 +2801,6 @@ packages: rolldown: optional: true - unique-filename@3.0.0: - resolution: {integrity: sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - unique-slug@4.0.0: - resolution: {integrity: sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - unique-string@1.0.0: - resolution: {integrity: sha512-ODgiYu03y5g76A1I9Gt0/chLCzQjvzDy7DsZGsLOE/1MrF6wriEskSncj1+/C58Xk/kPZDppSctDybCwOSaGAg==} - engines: {node: '>=4'} - - unique-string@2.0.0: - resolution: {integrity: sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==} - engines: {node: '>=8'} - unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -10532,22 +2816,6 @@ packages: unist-util-visit@5.1.0: resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - - universalify@1.0.0: - resolution: {integrity: sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==} - engines: {node: '>= 10.0.0'} - - universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - unplugin-utils@0.3.1: resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} engines: {node: '>=20.19.0'} @@ -10642,90 +2910,12 @@ packages: uqr@0.1.3: resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==} - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - url-join@4.0.0: - resolution: {integrity: sha512-EGXjXJZhIHiQMK2pQukuFcL303nskqIRzWvPvV5O8miOfwoUb9G+a/Cld60kUyeaybEI94wvVClT10DtfeAExA==} - - url-parse@1.5.10: - resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - - url-template@2.0.8: - resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} - - use-latest-callback@0.2.6: - resolution: {integrity: sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==} - peerDependencies: - react: '>=16.8' - - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - util@0.12.5: - resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} - - utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} - - uuid@10.0.0: - resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - - uuid@11.1.1: - resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} - hasBin: true - - uuid@14.0.0: - resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} - hasBin: true - - uuid@7.0.3: - resolution: {integrity: sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). - hasBin: true - - v8-compile-cache-lib@3.0.1: - resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} - - v8-to-istanbul@9.3.0: - resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} - engines: {node: '>=10.12.0'} - valibot@0.29.0: resolution: {integrity: sha512-JhZn08lwZPhAamOCfBwBkv/btQt4KeQhekULPH8crH053zUCLSOGEF2zKExu3bFf245tsj6J1dY0ysd/jUiMIQ==} - valid-url@1.0.9: - resolution: {integrity: sha512-QQDsV8OnSf5Uc30CKSwG9lnhMPe6exHtTXLRYX8uMwKENy640pU+2BgBL0LRbDh/eYRahNCS7aewCx0wf3NYVA==} - - validate-npm-package-name@3.0.0: - resolution: {integrity: sha512-M6w37eVCMMouJ9V/sdPGnC5H4uDr73/+xdq0FBLO3TFFX1+7wiUY6Es328NN+y43tmY+doUdN9g9J21vqB7iLw==} - - validate-npm-package-name@5.0.1: - resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} - engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} - - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -10742,37 +2932,6 @@ packages: '@testing-library/jest-dom': optional: true - vite@5.4.21: - resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - vite@7.3.3: resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -10821,20 +2980,20 @@ packages: vite: optional: true - vitest@4.1.6: - resolution: {integrity: sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==} + vitest@4.1.7: + resolution: {integrity: sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.6 - '@vitest/browser-preview': 4.1.6 - '@vitest/browser-webdriverio': 4.1.6 - '@vitest/coverage-istanbul': 4.1.6 - '@vitest/coverage-v8': 4.1.6 - '@vitest/ui': 4.1.6 + '@vitest/browser-playwright': 4.1.7 + '@vitest/browser-preview': 4.1.7 + '@vitest/browser-webdriverio': 4.1.7 + '@vitest/coverage-istanbul': 4.1.7 + '@vitest/coverage-v8': 4.1.7 + '@vitest/ui': 4.1.7 happy-dom: '*' jsdom: '*' vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -10862,72 +3021,15 @@ packages: jsdom: optional: true - vlq@1.0.1: - resolution: {integrity: sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==} - - walker@1.0.8: - resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} - - warn-once@0.1.1: - resolution: {integrity: sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==} - - wcwidth@1.0.1: - resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - webidl-conversions@5.0.0: - resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==} - engines: {node: '>=8'} - webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} - websocket-driver@0.7.4: - resolution: {integrity: sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==} - engines: {node: '>=0.8.0'} - - websocket-extensions@0.1.4: - resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} - engines: {node: '>=0.8.0'} - - whatwg-fetch@3.6.20: - resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} - - whatwg-url-minimum@0.1.2: - resolution: {integrity: sha512-XPEm0XFQWNVG292lII1PrRRJl3sItrs7CettZ4ncYxuDVpLyy+NwlGyut2hXI0JswcJUxeCH+CyOJK0ZzAXD6A==} - - whatwg-url-without-unicode@8.0.0-3: - resolution: {integrity: sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==} - engines: {node: '>=10'} - whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which-boxed-primitive@1.1.1: - resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} - engines: {node: '>= 0.4'} - - which-builtin-type@1.2.1: - resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} - engines: {node: '>= 0.4'} - - which-collection@1.0.2: - resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} - engines: {node: '>= 0.4'} - - which-module@2.0.1: - resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} - - which-typed-array@1.1.20: - resolution: {integrity: sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==} - engines: {node: '>= 0.4'} - - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -10938,20 +3040,6 @@ packages: engines: {node: '>=8'} hasBin: true - wonka@4.0.15: - resolution: {integrity: sha512-U0IUQHKXXn6PFo9nqsHphVCE5m3IntqZNB9Jjn7EB1lrR7YTDY3YWgFvEvwniTzXSvOH/XMzAZaIfJF/LvHYXg==} - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - wordwrap@1.0.0: - resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - - wrap-ansi@6.2.0: - resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} - engines: {node: '>=8'} - wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -10964,90 +3052,10 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - write-file-atomic@2.4.3: - resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} - - write-file-atomic@4.0.2: - resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} - - ws@6.2.3: - resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - wsl-utils@0.3.1: resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} engines: {node: '>=20'} - xcode@3.0.1: - resolution: {integrity: sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==} - engines: {node: '>=10.0.0'} - - xml-naming@0.1.0: - resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} - engines: {node: '>=16.0.0'} - - xml2js@0.6.0: - resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==} - engines: {node: '>=4.0.0'} - - xmlbuilder@11.0.1: - resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} - engines: {node: '>=4.0'} - - xmlbuilder@13.0.2: - resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==} - engines: {node: '>=6.0'} - - xmlbuilder@14.0.0: - resolution: {integrity: sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg==} - engines: {node: '>=8.0'} - - xmlbuilder@15.1.1: - resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} - engines: {node: '>=8.0'} - - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - - y18n@4.0.3: - resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} - y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -11055,50 +3063,18 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} - hasBin: true - - yargs-parser@18.1.3: - resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} - engines: {node: '>=6'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - yargs-parser@22.0.0: resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} - yargs@15.4.1: - resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} - engines: {node: '>=8'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - yargs@18.0.0: resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} - yn@3.1.1: - resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} - engines: {node: '>=6'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - youch-core@0.3.3: resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} @@ -11109,345 +3085,36 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} - zod-validation-error@2.1.0: - resolution: {integrity: sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==} - engines: {node: '>=18.0.0'} - peerDependencies: - zod: ^3.18.0 - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} - - zod@4.4.3: - resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - - zustand@4.5.7: - resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} - engines: {node: '>=12.7.0'} - peerDependencies: - '@types/react': '>=16.8' - immer: '>=9.0.6' - react: '>=16.8' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} snapshots: - '@aws-crypto/crc32@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.8 - tslib: 2.8.1 - - '@aws-crypto/sha256-browser@5.2.0': - dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.8 - '@aws-sdk/util-locate-window': 3.965.5 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-crypto/sha256-js@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.8 - tslib: 2.8.1 - - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: - tslib: 2.8.1 - - '@aws-crypto/util@5.2.0': - dependencies: - '@aws-sdk/types': 3.973.8 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-sdk/client-cloudwatch@3.1046.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.9 - '@aws-sdk/credential-provider-node': 3.972.40 - '@aws-sdk/middleware-host-header': 3.972.11 - '@aws-sdk/middleware-logger': 3.972.10 - '@aws-sdk/middleware-recursion-detection': 3.972.12 - '@aws-sdk/middleware-user-agent': 3.972.39 - '@aws-sdk/region-config-resolver': 3.972.14 - '@aws-sdk/types': 3.973.8 - '@aws-sdk/util-endpoints': 3.996.9 - '@aws-sdk/util-user-agent-browser': 3.972.11 - '@aws-sdk/util-user-agent-node': 3.973.25 - '@smithy/core': 3.24.2 - '@smithy/fetch-http-handler': 5.4.2 - '@smithy/middleware-compression': 4.4.2 - '@smithy/node-http-handler': 4.7.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.974.9': - dependencies: - '@aws-sdk/types': 3.973.8 - '@aws-sdk/xml-builder': 3.972.23 - '@smithy/core': 3.24.2 - '@smithy/signature-v4': 5.4.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-env@3.972.35': - dependencies: - '@aws-sdk/core': 3.974.9 - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-http@3.972.37': - dependencies: - '@aws-sdk/core': 3.974.9 - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/fetch-http-handler': 5.4.2 - '@smithy/node-http-handler': 4.7.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.972.39': - dependencies: - '@aws-sdk/core': 3.974.9 - '@aws-sdk/credential-provider-env': 3.972.35 - '@aws-sdk/credential-provider-http': 3.972.37 - '@aws-sdk/credential-provider-login': 3.972.39 - '@aws-sdk/credential-provider-process': 3.972.35 - '@aws-sdk/credential-provider-sso': 3.972.39 - '@aws-sdk/credential-provider-web-identity': 3.972.39 - '@aws-sdk/nested-clients': 3.997.7 - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/credential-provider-imds': 4.3.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-login@3.972.39': - dependencies: - '@aws-sdk/core': 3.974.9 - '@aws-sdk/nested-clients': 3.997.7 - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-node@3.972.40': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.35 - '@aws-sdk/credential-provider-http': 3.972.37 - '@aws-sdk/credential-provider-ini': 3.972.39 - '@aws-sdk/credential-provider-process': 3.972.35 - '@aws-sdk/credential-provider-sso': 3.972.39 - '@aws-sdk/credential-provider-web-identity': 3.972.39 - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/credential-provider-imds': 4.3.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-process@3.972.35': - dependencies: - '@aws-sdk/core': 3.974.9 - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-sso@3.972.39': - dependencies: - '@aws-sdk/core': 3.974.9 - '@aws-sdk/nested-clients': 3.997.7 - '@aws-sdk/token-providers': 3.1046.0 - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-web-identity@3.972.39': - dependencies: - '@aws-sdk/core': 3.974.9 - '@aws-sdk/nested-clients': 3.997.7 - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/middleware-host-header@3.972.11': - dependencies: - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-logger@3.972.10': - dependencies: - '@aws-sdk/types': 3.973.8 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-recursion-detection@3.972.12': - dependencies: - '@aws-sdk/types': 3.973.8 - '@aws/lambda-invoke-store': 0.2.4 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-user-agent@3.972.39': - dependencies: - '@aws-sdk/core': 3.974.9 - '@aws-sdk/types': 3.973.8 - '@aws-sdk/util-endpoints': 3.996.9 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.997.7': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.974.9 - '@aws-sdk/middleware-host-header': 3.972.11 - '@aws-sdk/middleware-logger': 3.972.10 - '@aws-sdk/middleware-recursion-detection': 3.972.12 - '@aws-sdk/middleware-user-agent': 3.972.39 - '@aws-sdk/region-config-resolver': 3.972.14 - '@aws-sdk/signature-v4-multi-region': 3.996.26 - '@aws-sdk/types': 3.973.8 - '@aws-sdk/util-endpoints': 3.996.9 - '@aws-sdk/util-user-agent-browser': 3.972.11 - '@aws-sdk/util-user-agent-node': 3.973.25 - '@smithy/core': 3.24.2 - '@smithy/fetch-http-handler': 5.4.2 - '@smithy/node-http-handler': 4.7.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/region-config-resolver@3.972.14': - dependencies: - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/signature-v4-multi-region@3.996.26': - dependencies: - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/signature-v4': 5.4.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.1046.0': - dependencies: - '@aws-sdk/core': 3.974.9 - '@aws-sdk/nested-clients': 3.997.7 - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/types@3.973.8': - dependencies: - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/util-endpoints@3.996.9': - dependencies: - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/util-locate-window@3.965.5': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-browser@3.972.11': - dependencies: - '@aws-sdk/types': 3.973.8 - '@smithy/types': 4.14.1 - bowser: 2.14.1 - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-node@3.973.25': - dependencies: - '@aws-sdk/middleware-user-agent': 3.972.39 - '@aws-sdk/types': 3.973.8 - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.972.23': - dependencies: - '@nodable/entities': 2.1.0 - '@smithy/types': 4.14.1 - fast-xml-parser: 5.7.2 - tslib: 2.8.1 - - '@aws/lambda-invoke-store@0.2.4': {} - - '@babel/code-frame@7.10.4': - dependencies: - '@babel/highlight': 7.25.9 - '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/code-frame@7.29.0': + '@babel/code-frame@7.29.7': dependencies: - '@babel/helper-validator-identifier': 7.28.5 + '@babel/helper-validator-identifier': 7.29.7 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.29.3': {} + '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.0': + '@babel/core@7.29.7': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.29.2 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@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 + '@babel/types': 7.29.7 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3 @@ -11457,1035 +3124,172 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.2.0': + '@babel/generator@7.29.7': dependencies: - '@babel/types': 7.29.0 - jsesc: 2.5.2 - lodash: 4.18.1 - source-map: 0.5.7 - trim-right: 1.0.1 - - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@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/helper-annotate-as-pure@7.27.3': + '@babel/helper-annotate-as-pure@7.29.7': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/helper-compilation-targets@7.28.6': + '@babel/helper-compilation-targets@7.29.7': dependencies: - '@babel/compat-data': 7.29.3 - '@babel/helper-validator-option': 7.27.1 + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)': + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.7 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - regexpu-core: 6.4.0 - semver: 6.3.1 + '@babel/helper-globals@7.29.7': {} - '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + '@babel/helper-member-expression-to-functions@7.29.7': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - debug: 4.4.3 - lodash.debounce: 4.0.8 - resolve: 1.22.12 - transitivePeerDependencies: - - supports-color - - '@babel/helper-environment-visitor@7.24.7': - dependencies: - '@babel/types': 7.29.0 - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-member-expression-to-functions@7.28.5': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color '@babel/helper-module-imports@7.18.6': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.29.7': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-optimise-call-expression@7.27.1': + '@babel/helper-optimise-call-expression@7.29.7': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 - '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helper-wrap-function@7.28.6': + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/helpers@7.29.2': + '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - - '@babel/highlight@7.25.9': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/parser@7.29.3': - dependencies: - '@babel/types': 7.29.0 - - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + '@babel/template@7.29.7': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + '@babel/traverse@7.29.7': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-environment-visitor': 7.24.7 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-class-properties@7.18.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-proposal-logical-assignment-operators@7.20.7(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) - - '@babel/plugin-proposal-nullish-coalescing-operator@7.18.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - - '@babel/plugin-proposal-numeric-separator@7.18.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) - - '@babel/plugin-proposal-object-rest-spread@7.20.7(@babel/core@7.29.0)': - dependencies: - '@babel/compat-data': 7.29.3 - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - - '@babel/plugin-proposal-optional-catch-binding@7.18.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) - - '@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-export-default-from@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-globals': 7.28.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/template': 7.28.6 - - '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) - - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) - babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/preset-env@7.29.5(@babel/core@7.29.0)': - dependencies: - '@babel/compat-data': 7.29.3 - '@babel/core': 7.29.0 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.3(@babel/core@7.29.0) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) - '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.29.0) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) - babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) - babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0) - babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) - core-js-compat: 3.49.0 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/preset-flow@7.27.1(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) - - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/types': 7.29.0 - esutils: 2.0.3 - - '@babel/preset-react@7.28.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/register@7.29.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - clone-deep: 4.0.1 - find-cache-dir: 2.1.0 - make-dir: 2.1.0 - pirates: 4.0.7 - source-map-support: 0.5.21 - - '@babel/runtime@7.29.2': {} - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 + '@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 transitivePeerDependencies: - supports-color - '@babel/types@7.29.0': + '@babel/types@7.29.7': dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@bcoe/v8-coverage@0.2.3': {} + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 '@bcoe/v8-coverage@1.0.2': {} '@cloudflare/kv-asset-handler@0.4.2': {} - '@cspotcode/source-map-support@0.8.1': - dependencies: - '@jridgewell/trace-mapping': 0.3.9 - - '@datadog/flagging-core@0.3.3': - dependencies: - spark-md5: 3.0.2 - optional: true - - '@datadog/libdatadog@0.9.3': - optional: true - - '@datadog/native-appsec@11.0.1': - dependencies: - node-gyp-build: 3.9.0 - optional: true - - '@datadog/native-iast-taint-tracking@4.1.0': - dependencies: - node-gyp-build: 3.9.0 - optional: true - - '@datadog/native-metrics@3.1.2': - dependencies: - node-addon-api: 6.1.0 - node-gyp-build: 3.9.0 - optional: true - - '@datadog/openfeature-node-server@1.1.2(@openfeature/server-sdk@1.21.0(@openfeature/core@1.10.0))': - dependencies: - '@datadog/flagging-core': 0.3.3 - '@openfeature/server-sdk': 1.21.0(@openfeature/core@1.10.0) - optional: true - - '@datadog/pprof@5.14.1': - dependencies: - node-gyp-build: 3.9.0 - pprof-format: 2.2.1 - source-map: 0.7.6 - optional: true - - '@datadog/wasm-js-rewriter@5.0.1': - dependencies: - js-yaml: 4.1.1 - lru-cache: 7.18.3 - module-details-from-path: 1.0.4 - node-gyp-build: 4.8.4 - optional: true - - '@drizzle-team/brocli@0.10.2': {} - - '@egjs/hammerjs@2.0.17': - dependencies: - '@types/hammerjs': 2.0.46 - - '@emnapi/core@1.10.0': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.10.0': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@esbuild-kit/core-utils@3.3.2': - dependencies: - esbuild: 0.18.20 - source-map-support: 0.5.21 - - '@esbuild-kit/esm-loader@2.6.5': - dependencies: - '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.14.0 - - '@esbuild/aix-ppc64@0.21.5': - optional: true - '@esbuild/aix-ppc64@0.25.12': optional: true @@ -12495,12 +3299,6 @@ snapshots: '@esbuild/aix-ppc64@0.28.0': optional: true - '@esbuild/android-arm64@0.18.20': - optional: true - - '@esbuild/android-arm64@0.21.5': - optional: true - '@esbuild/android-arm64@0.25.12': optional: true @@ -12510,12 +3308,6 @@ snapshots: '@esbuild/android-arm64@0.28.0': optional: true - '@esbuild/android-arm@0.18.20': - optional: true - - '@esbuild/android-arm@0.21.5': - optional: true - '@esbuild/android-arm@0.25.12': optional: true @@ -12525,12 +3317,6 @@ snapshots: '@esbuild/android-arm@0.28.0': optional: true - '@esbuild/android-x64@0.18.20': - optional: true - - '@esbuild/android-x64@0.21.5': - optional: true - '@esbuild/android-x64@0.25.12': optional: true @@ -12540,12 +3326,6 @@ snapshots: '@esbuild/android-x64@0.28.0': optional: true - '@esbuild/darwin-arm64@0.18.20': - optional: true - - '@esbuild/darwin-arm64@0.21.5': - optional: true - '@esbuild/darwin-arm64@0.25.12': optional: true @@ -12555,12 +3335,6 @@ snapshots: '@esbuild/darwin-arm64@0.28.0': optional: true - '@esbuild/darwin-x64@0.18.20': - optional: true - - '@esbuild/darwin-x64@0.21.5': - optional: true - '@esbuild/darwin-x64@0.25.12': optional: true @@ -12570,12 +3344,6 @@ snapshots: '@esbuild/darwin-x64@0.28.0': optional: true - '@esbuild/freebsd-arm64@0.18.20': - optional: true - - '@esbuild/freebsd-arm64@0.21.5': - optional: true - '@esbuild/freebsd-arm64@0.25.12': optional: true @@ -12585,12 +3353,6 @@ snapshots: '@esbuild/freebsd-arm64@0.28.0': optional: true - '@esbuild/freebsd-x64@0.18.20': - optional: true - - '@esbuild/freebsd-x64@0.21.5': - optional: true - '@esbuild/freebsd-x64@0.25.12': optional: true @@ -12600,12 +3362,6 @@ snapshots: '@esbuild/freebsd-x64@0.28.0': optional: true - '@esbuild/linux-arm64@0.18.20': - optional: true - - '@esbuild/linux-arm64@0.21.5': - optional: true - '@esbuild/linux-arm64@0.25.12': optional: true @@ -12615,12 +3371,6 @@ snapshots: '@esbuild/linux-arm64@0.28.0': optional: true - '@esbuild/linux-arm@0.18.20': - optional: true - - '@esbuild/linux-arm@0.21.5': - optional: true - '@esbuild/linux-arm@0.25.12': optional: true @@ -12630,12 +3380,6 @@ snapshots: '@esbuild/linux-arm@0.28.0': optional: true - '@esbuild/linux-ia32@0.18.20': - optional: true - - '@esbuild/linux-ia32@0.21.5': - optional: true - '@esbuild/linux-ia32@0.25.12': optional: true @@ -12645,12 +3389,6 @@ snapshots: '@esbuild/linux-ia32@0.28.0': optional: true - '@esbuild/linux-loong64@0.18.20': - optional: true - - '@esbuild/linux-loong64@0.21.5': - optional: true - '@esbuild/linux-loong64@0.25.12': optional: true @@ -12660,12 +3398,6 @@ snapshots: '@esbuild/linux-loong64@0.28.0': optional: true - '@esbuild/linux-mips64el@0.18.20': - optional: true - - '@esbuild/linux-mips64el@0.21.5': - optional: true - '@esbuild/linux-mips64el@0.25.12': optional: true @@ -12675,12 +3407,6 @@ snapshots: '@esbuild/linux-mips64el@0.28.0': optional: true - '@esbuild/linux-ppc64@0.18.20': - optional: true - - '@esbuild/linux-ppc64@0.21.5': - optional: true - '@esbuild/linux-ppc64@0.25.12': optional: true @@ -12690,12 +3416,6 @@ snapshots: '@esbuild/linux-ppc64@0.28.0': optional: true - '@esbuild/linux-riscv64@0.18.20': - optional: true - - '@esbuild/linux-riscv64@0.21.5': - optional: true - '@esbuild/linux-riscv64@0.25.12': optional: true @@ -12705,12 +3425,6 @@ snapshots: '@esbuild/linux-riscv64@0.28.0': optional: true - '@esbuild/linux-s390x@0.18.20': - optional: true - - '@esbuild/linux-s390x@0.21.5': - optional: true - '@esbuild/linux-s390x@0.25.12': optional: true @@ -12720,12 +3434,6 @@ snapshots: '@esbuild/linux-s390x@0.28.0': optional: true - '@esbuild/linux-x64@0.18.20': - optional: true - - '@esbuild/linux-x64@0.21.5': - optional: true - '@esbuild/linux-x64@0.25.12': optional: true @@ -12744,12 +3452,6 @@ snapshots: '@esbuild/netbsd-arm64@0.28.0': optional: true - '@esbuild/netbsd-x64@0.18.20': - optional: true - - '@esbuild/netbsd-x64@0.21.5': - optional: true - '@esbuild/netbsd-x64@0.25.12': optional: true @@ -12768,12 +3470,6 @@ snapshots: '@esbuild/openbsd-arm64@0.28.0': optional: true - '@esbuild/openbsd-x64@0.18.20': - optional: true - - '@esbuild/openbsd-x64@0.21.5': - optional: true - '@esbuild/openbsd-x64@0.25.12': optional: true @@ -12792,12 +3488,6 @@ snapshots: '@esbuild/openharmony-arm64@0.28.0': optional: true - '@esbuild/sunos-x64@0.18.20': - optional: true - - '@esbuild/sunos-x64@0.21.5': - optional: true - '@esbuild/sunos-x64@0.25.12': optional: true @@ -12807,12 +3497,6 @@ snapshots: '@esbuild/sunos-x64@0.28.0': optional: true - '@esbuild/win32-arm64@0.18.20': - optional: true - - '@esbuild/win32-arm64@0.21.5': - optional: true - '@esbuild/win32-arm64@0.25.12': optional: true @@ -12822,12 +3506,6 @@ snapshots: '@esbuild/win32-arm64@0.28.0': optional: true - '@esbuild/win32-ia32@0.18.20': - optional: true - - '@esbuild/win32-ia32@0.21.5': - optional: true - '@esbuild/win32-ia32@0.25.12': optional: true @@ -12837,12 +3515,6 @@ snapshots: '@esbuild/win32-ia32@0.28.0': optional: true - '@esbuild/win32-x64@0.18.20': - optional: true - - '@esbuild/win32-x64@0.21.5': - optional: true - '@esbuild/win32-x64@0.25.12': optional: true @@ -12852,1014 +3524,6 @@ snapshots: '@esbuild/win32-x64@0.28.0': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@8.57.1)': - dependencies: - eslint: 8.57.1 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.2': {} - - '@eslint/eslintrc@2.1.4': - dependencies: - ajv: 6.15.0 - debug: 4.4.3 - espree: 9.6.1 - globals: 13.24.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.1 - minimatch: 3.1.5 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@8.57.1': {} - - '@expo/bunyan@4.0.1': - dependencies: - uuid: 8.3.2 - - '@expo/cli@0.18.31(expo-modules-autolinking@1.11.3)': - dependencies: - '@babel/runtime': 7.29.2 - '@expo/code-signing-certificates': 0.0.5 - '@expo/config': 9.0.4 - '@expo/config-plugins': 8.0.11 - '@expo/devcert': 1.2.1 - '@expo/env': 0.3.0 - '@expo/image-utils': 0.5.1 - '@expo/json-file': 8.3.3 - '@expo/metro-config': 0.18.11 - '@expo/osascript': 2.4.3 - '@expo/package-manager': 1.10.5 - '@expo/plist': 0.1.3 - '@expo/prebuild-config': 7.0.9(expo-modules-autolinking@1.11.3) - '@expo/rudder-sdk-node': 1.1.1 - '@expo/spawn-async': 1.7.2 - '@expo/xcpretty': 4.4.4 - '@react-native/dev-middleware': 0.74.85 - '@urql/core': 2.3.6(graphql@15.8.0) - '@urql/exchange-retry': 0.3.0(graphql@15.8.0) - accepts: 1.3.8 - arg: 5.0.2 - better-opn: 3.0.2 - bplist-creator: 0.0.7 - bplist-parser: 0.3.2 - cacache: 18.0.4 - chalk: 4.1.2 - ci-info: 3.9.0 - connect: 3.7.0 - debug: 4.4.3 - env-editor: 0.4.2 - fast-glob: 3.3.3 - find-yarn-workspace-root: 2.0.0 - form-data: 3.0.4 - freeport-async: 2.0.0 - fs-extra: 8.1.0 - getenv: 1.0.0 - glob: 7.2.3 - graphql: 15.8.0 - graphql-tag: 2.12.6(graphql@15.8.0) - https-proxy-agent: 5.0.1 - internal-ip: 4.3.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - js-yaml: 3.14.2 - json-schema-deref-sync: 0.13.0 - lodash.debounce: 4.0.8 - md5hex: 1.0.0 - minimatch: 3.1.5 - node-fetch: 2.7.0 - node-forge: 1.4.0 - npm-package-arg: 7.0.0 - open: 8.4.2 - ora: 3.4.0 - picomatch: 3.0.2 - pretty-bytes: 5.6.0 - progress: 2.0.3 - prompts: 2.4.2 - qrcode-terminal: 0.11.0 - require-from-string: 2.0.2 - requireg: 0.2.2 - resolve: 1.22.12 - resolve-from: 5.0.0 - resolve.exports: 2.0.3 - semver: 7.8.0 - send: 0.18.0 - slugify: 1.6.9 - source-map-support: 0.5.21 - stacktrace-parser: 0.1.11 - structured-headers: 0.4.1 - tar: 6.2.1 - temp-dir: 2.0.0 - tempy: 0.7.1 - terminal-link: 2.1.1 - text-table: 0.2.0 - url-join: 4.0.0 - wrap-ansi: 7.0.0 - ws: 8.20.1 - transitivePeerDependencies: - - bufferutil - - encoding - - expo-modules-autolinking - - supports-color - - utf-8-validate - - '@expo/cli@55.0.30(@expo/dom-webview@55.0.6)(expo-constants@55.0.16(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)))(expo-font@55.0.7(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6))(expo@55.0.24)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3)': - dependencies: - '@expo/code-signing-certificates': 0.0.6 - '@expo/config': 55.0.17(typescript@5.9.3) - '@expo/config-plugins': 55.0.9 - '@expo/devcert': 1.2.1 - '@expo/env': 2.1.2 - '@expo/image-utils': 0.8.14(typescript@5.9.3) - '@expo/json-file': 10.0.14 - '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - '@expo/metro': 55.1.1 - '@expo/metro-config': 55.0.21(expo@55.0.24)(typescript@5.9.3) - '@expo/osascript': 2.4.3 - '@expo/package-manager': 1.10.5 - '@expo/plist': 0.5.3 - '@expo/prebuild-config': 55.0.18(expo@55.0.24)(typescript@5.9.3) - '@expo/require-utils': 55.0.5(typescript@5.9.3) - '@expo/router-server': 55.0.16(expo-constants@55.0.16(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)))(expo-font@55.0.7(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6))(expo-server@55.0.9)(expo@55.0.24)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@expo/schema-utils': 55.0.4 - '@expo/spawn-async': 1.7.2 - '@expo/ws-tunnel': 1.0.6 - '@expo/xcpretty': 4.4.4 - '@react-native/dev-middleware': 0.83.6 - accepts: 1.3.8 - arg: 5.0.2 - better-opn: 3.0.2 - bplist-creator: 0.1.0 - bplist-parser: 0.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - compression: 1.8.1 - connect: 3.7.0 - debug: 4.4.3 - dnssd-advertise: 1.1.4 - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - expo-server: 55.0.9 - fetch-nodeshim: 0.4.10 - getenv: 2.0.0 - glob: 13.0.6 - lan-network: 0.2.1 - multitars: 1.0.0 - node-forge: 1.4.0 - npm-package-arg: 11.0.3 - ora: 3.4.0 - picomatch: 4.0.4 - pretty-format: 29.7.0 - progress: 2.0.3 - prompts: 2.4.2 - resolve-from: 5.0.0 - semver: 7.8.0 - send: 0.19.2 - slugify: 1.6.9 - source-map-support: 0.5.21 - stacktrace-parser: 0.1.11 - structured-headers: 0.4.1 - terminal-link: 2.1.1 - toqr: 0.1.1 - wrap-ansi: 7.0.0 - ws: 8.20.1 - zod: 3.25.76 - optionalDependencies: - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - transitivePeerDependencies: - - '@expo/dom-webview' - - '@expo/metro-runtime' - - bufferutil - - expo-constants - - expo-font - - react - - react-dom - - react-server-dom-webpack - - supports-color - - typescript - - utf-8-validate - - '@expo/code-signing-certificates@0.0.5': - dependencies: - node-forge: 1.4.0 - nullthrows: 1.1.1 - - '@expo/code-signing-certificates@0.0.6': - dependencies: - node-forge: 1.4.0 - - '@expo/config-plugins@55.0.9': - dependencies: - '@expo/config-types': 55.0.5 - '@expo/json-file': 10.0.14 - '@expo/plist': 0.5.3 - '@expo/sdk-runtime-versions': 1.0.0 - chalk: 4.1.2 - debug: 4.4.3 - getenv: 2.0.0 - glob: 13.0.6 - resolve-from: 5.0.0 - semver: 7.8.0 - slugify: 1.6.9 - xcode: 3.0.1 - xml2js: 0.6.0 - transitivePeerDependencies: - - supports-color - - '@expo/config-plugins@7.2.5': - dependencies: - '@expo/config-types': 49.0.0 - '@expo/json-file': 8.2.37 - '@expo/plist': 0.0.20 - '@expo/sdk-runtime-versions': 1.0.0 - '@react-native/normalize-color': 2.1.0 - chalk: 4.1.2 - debug: 4.4.3 - find-up: 5.0.0 - getenv: 1.0.0 - glob: 7.1.6 - resolve-from: 5.0.0 - semver: 7.8.0 - slash: 3.0.0 - xcode: 3.0.1 - xml2js: 0.6.0 - transitivePeerDependencies: - - supports-color - - '@expo/config-plugins@8.0.11': - dependencies: - '@expo/config-types': 51.0.3 - '@expo/json-file': 8.3.3 - '@expo/plist': 0.1.3 - '@expo/sdk-runtime-versions': 1.0.0 - chalk: 4.1.2 - debug: 4.4.3 - find-up: 5.0.0 - getenv: 1.0.0 - glob: 7.1.6 - resolve-from: 5.0.0 - semver: 7.8.0 - slash: 3.0.0 - slugify: 1.6.9 - xcode: 3.0.1 - xml2js: 0.6.0 - transitivePeerDependencies: - - supports-color - - '@expo/config-types@49.0.0': {} - - '@expo/config-types@51.0.3': {} - - '@expo/config-types@55.0.5': {} - - '@expo/config@55.0.17(typescript@5.9.3)': - dependencies: - '@expo/config-plugins': 55.0.9 - '@expo/config-types': 55.0.5 - '@expo/json-file': 10.0.14 - '@expo/require-utils': 55.0.5(typescript@5.9.3) - deepmerge: 4.3.1 - getenv: 2.0.0 - glob: 13.0.6 - resolve-workspace-root: 2.0.1 - semver: 7.8.0 - slugify: 1.6.9 - transitivePeerDependencies: - - supports-color - - typescript - - '@expo/config@8.1.2': - dependencies: - '@babel/code-frame': 7.10.4 - '@expo/config-plugins': 7.2.5 - '@expo/config-types': 49.0.0 - '@expo/json-file': 8.3.3 - getenv: 1.0.0 - glob: 7.1.6 - require-from-string: 2.0.2 - resolve-from: 5.0.0 - semver: 7.5.3 - slugify: 1.6.9 - sucrase: 3.35.1 - transitivePeerDependencies: - - supports-color - - '@expo/config@9.0.4': - dependencies: - '@babel/code-frame': 7.10.4 - '@expo/config-plugins': 8.0.11 - '@expo/config-types': 51.0.3 - '@expo/json-file': 8.3.3 - getenv: 1.0.0 - glob: 7.1.6 - require-from-string: 2.0.2 - resolve-from: 5.0.0 - semver: 7.8.0 - slugify: 1.6.9 - sucrase: 3.34.0 - transitivePeerDependencies: - - supports-color - - '@expo/devcert@1.2.1': - dependencies: - '@expo/sudo-prompt': 9.3.2 - debug: 3.2.7 - transitivePeerDependencies: - - supports-color - - '@expo/devtools@55.0.3(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)': - dependencies: - chalk: 4.1.2 - optionalDependencies: - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - - '@expo/dom-webview@55.0.6(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)': - dependencies: - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - - '@expo/env@0.3.0': - dependencies: - chalk: 4.1.2 - debug: 4.4.3 - dotenv: 16.4.7 - dotenv-expand: 11.0.7 - getenv: 1.0.0 - transitivePeerDependencies: - - supports-color - - '@expo/env@2.1.2': - dependencies: - chalk: 4.1.2 - debug: 4.4.3 - getenv: 2.0.0 - transitivePeerDependencies: - - supports-color - - '@expo/fingerprint@0.16.7': - dependencies: - '@expo/env': 2.1.2 - '@expo/spawn-async': 1.7.2 - arg: 5.0.2 - chalk: 4.1.2 - debug: 4.4.3 - getenv: 2.0.0 - glob: 13.0.6 - ignore: 5.3.2 - minimatch: 10.2.5 - resolve-from: 5.0.0 - semver: 7.8.0 - transitivePeerDependencies: - - supports-color - - '@expo/image-utils@0.5.1': - dependencies: - '@expo/spawn-async': 1.7.2 - chalk: 4.1.2 - fs-extra: 9.0.0 - getenv: 1.0.0 - jimp-compact: 0.16.1 - node-fetch: 2.7.0 - parse-png: 2.1.0 - resolve-from: 5.0.0 - semver: 7.8.0 - tempy: 0.3.0 - transitivePeerDependencies: - - encoding - - '@expo/image-utils@0.8.14(typescript@5.9.3)': - dependencies: - '@expo/require-utils': 55.0.5(typescript@5.9.3) - '@expo/spawn-async': 1.7.2 - chalk: 4.1.2 - getenv: 2.0.0 - jimp-compact: 0.16.1 - parse-png: 2.1.0 - semver: 7.8.0 - transitivePeerDependencies: - - supports-color - - typescript - - '@expo/json-file@10.0.14': - dependencies: - '@babel/code-frame': 7.29.0 - json5: 2.2.3 - - '@expo/json-file@8.2.37': - dependencies: - '@babel/code-frame': 7.10.4 - json5: 2.2.3 - write-file-atomic: 2.4.3 - - '@expo/json-file@8.3.3': - dependencies: - '@babel/code-frame': 7.10.4 - json5: 2.2.3 - write-file-atomic: 2.4.3 - - '@expo/local-build-cache-provider@55.0.13(typescript@5.9.3)': - dependencies: - '@expo/config': 55.0.17(typescript@5.9.3) - chalk: 4.1.2 - transitivePeerDependencies: - - supports-color - - typescript - - '@expo/log-box@55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)': - dependencies: - '@expo/dom-webview': 55.0.6(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - anser: 1.4.10 - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - stacktrace-parser: 0.1.11 - - '@expo/metro-config@0.18.11': - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - '@expo/config': 9.0.4 - '@expo/env': 0.3.0 - '@expo/json-file': 8.3.3 - '@expo/spawn-async': 1.7.2 - chalk: 4.1.2 - debug: 4.4.3 - find-yarn-workspace-root: 2.0.0 - fs-extra: 9.1.0 - getenv: 1.0.0 - glob: 7.2.3 - jsc-safe-url: 0.2.4 - lightningcss: 1.19.0 - postcss: 8.4.49 - resolve-from: 5.0.0 - transitivePeerDependencies: - - supports-color - - '@expo/metro-config@55.0.21(expo@55.0.24)(typescript@5.9.3)': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@expo/config': 55.0.17(typescript@5.9.3) - '@expo/env': 2.1.2 - '@expo/json-file': 10.0.14 - '@expo/metro': 55.1.1 - '@expo/spawn-async': 1.7.2 - browserslist: 4.28.2 - chalk: 4.1.2 - debug: 4.4.3 - getenv: 2.0.0 - glob: 13.0.6 - hermes-parser: 0.32.1 - jsc-safe-url: 0.2.4 - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.4.49 - resolve-from: 5.0.0 - optionalDependencies: - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - transitivePeerDependencies: - - bufferutil - - supports-color - - typescript - - utf-8-validate - - '@expo/metro@55.1.1': - dependencies: - metro: 0.83.7 - metro-babel-transformer: 0.83.7 - metro-cache: 0.83.7 - metro-cache-key: 0.83.7 - metro-config: 0.83.7 - metro-core: 0.83.7 - metro-file-map: 0.83.7 - metro-minify-terser: 0.83.7 - metro-resolver: 0.83.7 - metro-runtime: 0.83.7 - metro-source-map: 0.83.7 - metro-symbolicate: 0.83.7 - metro-transform-plugins: 0.83.7 - metro-transform-worker: 0.83.7 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@expo/osascript@2.4.3': - dependencies: - '@expo/spawn-async': 1.7.2 - - '@expo/package-manager@1.10.5': - dependencies: - '@expo/json-file': 10.0.14 - '@expo/spawn-async': 1.7.2 - chalk: 4.1.2 - npm-package-arg: 11.0.3 - ora: 3.4.0 - resolve-workspace-root: 2.0.1 - - '@expo/plist@0.0.20': - dependencies: - '@xmldom/xmldom': 0.7.13 - base64-js: 1.5.1 - xmlbuilder: 14.0.0 - - '@expo/plist@0.1.3': - dependencies: - '@xmldom/xmldom': 0.7.13 - base64-js: 1.5.1 - xmlbuilder: 14.0.0 - - '@expo/plist@0.5.3': - dependencies: - '@xmldom/xmldom': 0.8.13 - base64-js: 1.5.1 - xmlbuilder: 15.1.1 - - '@expo/prebuild-config@55.0.18(expo@55.0.24)(typescript@5.9.3)': - dependencies: - '@expo/config': 55.0.17(typescript@5.9.3) - '@expo/config-plugins': 55.0.9 - '@expo/config-types': 55.0.5 - '@expo/image-utils': 0.8.14(typescript@5.9.3) - '@expo/json-file': 10.0.14 - '@react-native/normalize-colors': 0.83.6 - debug: 4.4.3 - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - resolve-from: 5.0.0 - semver: 7.8.0 - xml2js: 0.6.0 - transitivePeerDependencies: - - supports-color - - typescript - - '@expo/prebuild-config@7.0.9(expo-modules-autolinking@1.11.3)': - dependencies: - '@expo/config': 9.0.4 - '@expo/config-plugins': 8.0.11 - '@expo/config-types': 51.0.3 - '@expo/image-utils': 0.5.1 - '@expo/json-file': 8.3.3 - '@react-native/normalize-colors': 0.74.85 - debug: 4.4.3 - expo-modules-autolinking: 1.11.3 - fs-extra: 9.1.0 - resolve-from: 5.0.0 - semver: 7.8.0 - xml2js: 0.6.0 - transitivePeerDependencies: - - encoding - - supports-color - - '@expo/require-utils@55.0.5(typescript@5.9.3)': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0 - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - supports-color - - '@expo/router-server@55.0.16(expo-constants@55.0.16(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)))(expo-font@55.0.7(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6))(expo-server@55.0.9)(expo@55.0.24)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - debug: 4.4.3 - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - expo-constants: 55.0.16(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)) - expo-font: 55.0.7(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - expo-server: 55.0.9 - react: 19.2.6 - optionalDependencies: - react-dom: 19.2.6(react@19.2.6) - transitivePeerDependencies: - - supports-color - - '@expo/rudder-sdk-node@1.1.1': - dependencies: - '@expo/bunyan': 4.0.1 - '@segment/loosely-validate-event': 2.0.0 - fetch-retry: 4.1.1 - md5: 2.3.0 - node-fetch: 2.7.0 - remove-trailing-slash: 0.1.1 - uuid: 8.3.2 - transitivePeerDependencies: - - encoding - - '@expo/schema-utils@55.0.4': {} - - '@expo/sdk-runtime-versions@1.0.0': {} - - '@expo/spawn-async@1.7.2': - dependencies: - cross-spawn: 7.0.6 - - '@expo/sudo-prompt@9.3.2': {} - - '@expo/vector-icons@14.1.0(expo-font@12.0.10(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)': - dependencies: - expo-font: 12.0.10(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - react: 18.2.0 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - - '@expo/vector-icons@15.1.1(expo-font@55.0.7(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)': - dependencies: - expo-font: 55.0.7(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - - '@expo/ws-tunnel@1.0.6': {} - - '@expo/xcpretty@4.4.4': - dependencies: - '@babel/code-frame': 7.29.0 - chalk: 4.1.2 - js-yaml: 4.1.1 - - '@fastify/accept-negotiator@1.1.0': {} - - '@fastify/accept-negotiator@2.0.1': {} - - '@fastify/ajv-compiler@4.0.5': - dependencies: - ajv: 8.20.0 - ajv-formats: 3.0.1(ajv@8.20.0) - fast-uri: 3.1.2 - - '@fastify/busboy@1.2.1': - dependencies: - text-decoding: 1.0.0 - - '@fastify/busboy@3.2.0': {} - - '@fastify/cors@10.1.0': - dependencies: - fastify-plugin: 5.1.0 - mnemonist: 0.40.0 - - '@fastify/deepmerge@1.3.0': {} - - '@fastify/error@3.4.1': {} - - '@fastify/error@4.2.0': {} - - '@fastify/fast-json-stringify-compiler@5.0.3': - dependencies: - fast-json-stringify: 6.4.0 - - '@fastify/forwarded@3.0.1': {} - - '@fastify/helmet@13.0.2': - dependencies: - fastify-plugin: 5.1.0 - helmet: 8.1.0 - - '@fastify/merge-json-schemas@0.2.1': - dependencies: - dequal: 2.0.3 - - '@fastify/multipart@7.7.3': - dependencies: - '@fastify/busboy': 1.2.1 - '@fastify/deepmerge': 1.3.0 - '@fastify/error': 3.4.1 - '@fastify/swagger': 8.15.0 - '@fastify/swagger-ui': 1.10.2 - end-of-stream: 1.4.5 - fastify-plugin: 4.5.1 - secure-json-parse: 2.7.0 - stream-wormhole: 1.1.0 - transitivePeerDependencies: - - supports-color - - '@fastify/proxy-addr@5.1.0': - dependencies: - '@fastify/forwarded': 3.0.1 - ipaddr.js: 2.4.0 - - '@fastify/rate-limit@9.1.0': - dependencies: - '@lukeed/ms': 2.0.2 - fastify-plugin: 4.5.1 - toad-cache: 3.7.0 - - '@fastify/send@2.1.0': - dependencies: - '@lukeed/ms': 2.0.2 - escape-html: 1.0.3 - fast-decode-uri-component: 1.0.1 - http-errors: 2.0.0 - mime: 3.0.0 - - '@fastify/send@4.1.0': - dependencies: - '@lukeed/ms': 2.0.2 - escape-html: 1.0.3 - fast-decode-uri-component: 1.0.1 - http-errors: 2.0.1 - mime: 3.0.0 - - '@fastify/sensible@6.0.4': - dependencies: - '@lukeed/ms': 2.0.2 - dequal: 2.0.3 - fastify-plugin: 5.1.0 - forwarded: 0.2.0 - http-errors: 2.0.1 - type-is: 2.1.0 - vary: 1.1.2 - - '@fastify/static@6.12.0': - dependencies: - '@fastify/accept-negotiator': 1.1.0 - '@fastify/send': 2.1.0 - content-disposition: 0.5.4 - fastify-plugin: 4.5.1 - glob: 8.1.0 - p-limit: 3.1.0 - - '@fastify/static@9.1.3': - dependencies: - '@fastify/accept-negotiator': 2.0.1 - '@fastify/send': 4.1.0 - content-disposition: 1.1.0 - fastify-plugin: 5.1.0 - fastq: 1.20.1 - glob: 13.0.6 - - '@fastify/swagger-ui@1.10.2': - dependencies: - '@fastify/static': 6.12.0 - fastify-plugin: 4.5.1 - openapi-types: 12.1.3 - rfdc: 1.4.1 - yaml: 2.9.0 - - '@fastify/swagger-ui@5.2.6': - dependencies: - '@fastify/static': 9.1.3 - fastify-plugin: 5.1.0 - openapi-types: 12.1.3 - rfdc: 1.4.1 - yaml: 2.9.0 - - '@fastify/swagger@8.15.0': - dependencies: - fastify-plugin: 4.5.1 - json-schema-resolver: 2.0.0 - openapi-types: 12.1.3 - rfdc: 1.4.1 - yaml: 2.9.0 - transitivePeerDependencies: - - supports-color - - '@fastify/swagger@9.7.0': - dependencies: - fastify-plugin: 5.1.0 - json-schema-resolver: 3.0.0 - openapi-types: 12.1.3 - rfdc: 1.4.1 - yaml: 2.9.0 - transitivePeerDependencies: - - supports-color - - '@firebase/app-check-interop-types@0.3.2': {} - - '@firebase/app-types@0.9.2': {} - - '@firebase/auth-interop-types@0.2.3': {} - - '@firebase/component@0.6.9': - dependencies: - '@firebase/util': 1.10.0 - tslib: 2.8.1 - - '@firebase/database-compat@1.0.8': - dependencies: - '@firebase/component': 0.6.9 - '@firebase/database': 1.0.8 - '@firebase/database-types': 1.0.5 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.10.0 - tslib: 2.8.1 - - '@firebase/database-types@1.0.5': - dependencies: - '@firebase/app-types': 0.9.2 - '@firebase/util': 1.10.0 - - '@firebase/database@1.0.8': - dependencies: - '@firebase/app-check-interop-types': 0.3.2 - '@firebase/auth-interop-types': 0.2.3 - '@firebase/component': 0.6.9 - '@firebase/logger': 0.4.2 - '@firebase/util': 1.10.0 - faye-websocket: 0.11.4 - tslib: 2.8.1 - - '@firebase/logger@0.4.2': - dependencies: - tslib: 2.8.1 - - '@firebase/util@1.10.0': - dependencies: - tslib: 2.8.1 - - '@google-cloud/firestore@7.11.6': - dependencies: - '@opentelemetry/api': 1.9.1 - fast-deep-equal: 3.1.3 - functional-red-black-tree: 1.0.1 - google-gax: 4.6.1 - protobufjs: 7.5.8 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - - '@google-cloud/paginator@5.0.2': - dependencies: - arrify: 2.0.1 - extend: 3.0.2 - optional: true - - '@google-cloud/projectify@4.0.0': - optional: true - - '@google-cloud/promisify@4.0.0': - optional: true - - '@google-cloud/storage@7.19.0': - dependencies: - '@google-cloud/paginator': 5.0.2 - '@google-cloud/projectify': 4.0.0 - '@google-cloud/promisify': 4.0.0 - abort-controller: 3.0.0 - async-retry: 1.3.3 - duplexify: 4.1.3 - fast-xml-parser: 5.8.0 - gaxios: 6.7.1 - google-auth-library: 9.15.1 - html-entities: 2.6.0 - mime: 3.0.0 - p-limit: 3.1.0 - retry-request: 7.0.2 - teeny-request: 9.0.0 - uuid: 8.3.2 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - - '@graphql-typed-document-node/core@3.2.0(graphql@15.8.0)': - dependencies: - graphql: 15.8.0 - - '@grpc/grpc-js@1.14.3': - dependencies: - '@grpc/proto-loader': 0.8.1 - '@js-sdsl/ordered-map': 4.4.2 - optional: true - - '@grpc/proto-loader@0.7.15': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.5.8 - yargs: 17.7.2 - optional: true - - '@grpc/proto-loader@0.8.1': - dependencies: - lodash.camelcase: 4.3.0 - long: 5.3.2 - protobufjs: 7.5.8 - yargs: 17.7.2 - optional: true - - '@hapi/hoek@9.3.0': {} - - '@hapi/topo@5.1.0': - dependencies: - '@hapi/hoek': 9.3.0 - - '@humanwhocodes/config-array@0.13.0': - dependencies: - '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.3 - minimatch: 3.1.5 - transitivePeerDependencies: - - supports-color - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/object-schema@2.0.3': {} - - '@ide/backoff@1.0.0': {} - - '@img/colour@1.1.0': - optional: true - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true - - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true - - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true - - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true - - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true - - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true - - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true - - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.10.0 - optional: true - - '@img/sharp-win32-arm64@0.34.5': - optional: true - - '@img/sharp-win32-ia32@0.34.5': - optional: true - - '@img/sharp-win32-x64@0.34.5': - optional: true - '@ioredis/commands@1.5.1': {} '@isaacs/cliui@8.0.2': @@ -13875,233 +3539,6 @@ snapshots: dependencies: minipass: 7.1.3 - '@isaacs/ttlcache@1.4.1': {} - - '@istanbuljs/load-nyc-config@1.1.0': - dependencies: - camelcase: 5.3.1 - find-up: 4.1.0 - get-package-type: 0.1.0 - js-yaml: 3.14.2 - resolve-from: 5.0.0 - - '@istanbuljs/schema@0.1.6': {} - - '@jest/console@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@types/node': 25.8.0 - chalk: 4.1.2 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 25.8.0 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/core@29.7.0(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3))': - dependencies: - '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 25.8.0 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 3.9.0 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)) - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - jest-watcher: 29.7.0 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-ansi: 6.0.1 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - ts-node - - '@jest/create-cache-key-function@29.7.0': - dependencies: - '@jest/types': 29.6.3 - - '@jest/environment@29.7.0': - dependencies: - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 20.19.41 - jest-mock: 29.7.0 - - '@jest/expect-utils@29.7.0': - dependencies: - jest-get-type: 29.6.3 - - '@jest/expect@29.7.0': - dependencies: - expect: 29.7.0 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/fake-timers@29.7.0': - dependencies: - '@jest/types': 29.6.3 - '@sinonjs/fake-timers': 10.3.0 - '@types/node': 25.8.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - '@jest/globals@29.7.0': - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/types': 29.6.3 - jest-mock: 29.7.0 - transitivePeerDependencies: - - supports-color - - '@jest/reporters@29.7.0': - dependencies: - '@bcoe/v8-coverage': 0.2.3 - '@jest/console': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - '@types/node': 25.8.0 - chalk: 4.1.2 - collect-v8-coverage: 1.0.3 - exit: 0.1.2 - glob: 7.2.3 - graceful-fs: 4.2.11 - istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 - istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 - istanbul-reports: 3.2.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - jest-worker: 29.7.0 - slash: 3.0.0 - string-length: 4.0.2 - strip-ansi: 6.0.1 - v8-to-istanbul: 9.3.0 - transitivePeerDependencies: - - supports-color - - '@jest/schemas@29.6.3': - dependencies: - '@sinclair/typebox': 0.27.10 - - '@jest/source-map@29.6.3': - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - callsites: 3.1.0 - graceful-fs: 4.2.11 - - '@jest/test-result@29.7.0': - dependencies: - '@jest/console': 29.7.0 - '@jest/types': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - collect-v8-coverage: 1.0.3 - - '@jest/test-sequencer@29.7.0': - dependencies: - '@jest/test-result': 29.7.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - slash: 3.0.0 - - '@jest/transform@29.7.0': - dependencies: - '@babel/core': 7.29.0 - '@jest/types': 29.6.3 - '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 6.1.1 - chalk: 4.1.2 - convert-source-map: 2.0.0 - fast-json-stable-stringify: 2.1.0 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - micromatch: 4.0.8 - pirates: 4.0.7 - slash: 3.0.0 - write-file-atomic: 4.0.2 - transitivePeerDependencies: - - supports-color - - '@jest/types@24.9.0': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 1.1.2 - '@types/yargs': 13.0.12 - - '@jest/types@26.6.2': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 25.8.0 - '@types/yargs': 15.0.20 - chalk: 4.1.2 - - '@jest/types@29.6.3': - dependencies: - '@jest/schemas': 29.6.3 - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-reports': 3.0.4 - '@types/node': 25.8.0 - '@types/yargs': 17.0.35 - chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -14126,82 +3563,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.9': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@js-sdsl/ordered-map@4.4.2': - optional: true - - '@keyv/serialize@1.1.1': {} - - '@libsql/client@0.17.3': - dependencies: - '@libsql/core': 0.17.3 - '@libsql/hrana-client': 0.10.0 - js-base64: 3.7.8 - libsql: 0.5.29 - promise-limit: 2.7.0 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/core@0.17.3': - dependencies: - js-base64: 3.7.8 - - '@libsql/darwin-arm64@0.5.29': - optional: true - - '@libsql/darwin-x64@0.5.29': - optional: true - - '@libsql/hrana-client@0.10.0': - dependencies: - '@libsql/isomorphic-ws': 0.1.5 - js-base64: 3.7.8 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/isomorphic-ws@0.1.5': - dependencies: - '@types/ws': 8.18.1 - ws: 8.20.1 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@libsql/linux-arm-gnueabihf@0.5.29': - optional: true - - '@libsql/linux-arm-musleabihf@0.5.29': - optional: true - - '@libsql/linux-arm64-gnu@0.5.29': - optional: true - - '@libsql/linux-arm64-musl@0.5.29': - optional: true - - '@libsql/linux-x64-gnu@0.5.29': - optional: true - - '@libsql/linux-x64-musl@0.5.29': - optional: true - - '@libsql/win32-x64-msvc@0.5.29': - optional: true - - '@lukeed/csprng@1.1.0': {} - - '@lukeed/ms@2.0.2': {} - - '@lukeed/uuid@2.0.1': - dependencies: - '@lukeed/csprng': 1.1.0 - '@mapbox/node-pre-gyp@2.0.3': dependencies: consola: 3.4.2 @@ -14209,67 +3570,12 @@ snapshots: https-proxy-agent: 7.0.6 node-fetch: 2.7.0 nopt: 8.1.0 - semver: 7.8.0 + semver: 7.8.1 tar: 7.5.15 transitivePeerDependencies: - encoding - supports-color - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3': - optional: true - - '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@tybys/wasm-util': 0.10.2 - optional: true - - '@neon-rs/load@0.0.4': {} - - '@next/env@16.2.6': {} - - '@next/swc-darwin-arm64@16.2.6': - optional: true - - '@next/swc-darwin-x64@16.2.6': - optional: true - - '@next/swc-linux-arm64-gnu@16.2.6': - optional: true - - '@next/swc-linux-arm64-musl@16.2.6': - optional: true - - '@next/swc-linux-x64-gnu@16.2.6': - optional: true - - '@next/swc-linux-x64-musl@16.2.6': - optional: true - - '@next/swc-win32-arm64-msvc@16.2.6': - optional: true - - '@next/swc-win32-x64-msvc@16.2.6': - optional: true - - '@nodable/entities@2.1.0': {} - '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -14282,387 +3588,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@npmcli/fs@3.1.1': - dependencies: - semver: 7.8.0 - - '@one-ini/wasm@0.1.1': {} - - '@openfeature/core@1.10.0': - optional: true - - '@openfeature/server-sdk@1.21.0(@openfeature/core@1.10.0)': - dependencies: - '@openfeature/core': 1.10.0 - optional: true - - '@opentelemetry/api-logs@0.218.0': - dependencies: - '@opentelemetry/api': 1.9.1 - optional: true - - '@opentelemetry/api-logs@0.53.0': - dependencies: - '@opentelemetry/api': 1.9.1 - - '@opentelemetry/api-logs@0.57.1': - dependencies: - '@opentelemetry/api': 1.9.1 - - '@opentelemetry/api-logs@0.57.2': - dependencies: - '@opentelemetry/api': 1.9.1 - - '@opentelemetry/api@1.9.1': {} - - '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - - '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/semantic-conventions': 1.28.0 - - '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-connect@0.43.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - '@types/connect': 3.4.36 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-dataloader@0.16.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-express@0.47.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-fastify@0.44.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-fs@0.19.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-generic-pool@0.43.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-graphql@0.47.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-hapi@0.45.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-http@0.57.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.28.0 - forwarded-parse: 2.1.2 - semver: 7.8.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-ioredis@0.47.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-kafkajs@0.7.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-knex@0.44.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-koa@0.47.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-lru-memoizer@0.44.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mongodb@0.51.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mongoose@0.46.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mysql2@0.45.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.1) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mysql@0.45.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - '@types/mysql': 2.15.26 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-nestjs-core@0.44.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-pg@0.50.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.27.0 - '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.1) - '@types/pg': 8.6.1 - '@types/pg-pool': 2.0.6 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-redis-4@0.46.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.41.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-tedious@0.18.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - '@types/tedious': 4.0.14 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-undici@0.10.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation@0.53.0(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.53.0 - '@types/shimmer': 1.2.0 - import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 - semver: 7.8.0 - shimmer: 1.2.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation@0.57.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.57.1 - '@types/shimmer': 1.2.0 - import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 - semver: 7.8.0 - shimmer: 1.2.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.57.2 - '@types/shimmer': 1.2.0 - import-in-the-middle: 1.15.0 - require-in-the-middle: 7.5.2 - semver: 7.8.0 - shimmer: 1.2.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/redis-common@0.36.2': {} - - '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.28.0 - - '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.28.0 - - '@opentelemetry/semantic-conventions@1.27.0': {} - - '@opentelemetry/semantic-conventions@1.28.0': {} - - '@opentelemetry/semantic-conventions@1.41.1': {} - - '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - - '@oxc-parser/binding-android-arm-eabi@0.129.0': - optional: true - - '@oxc-parser/binding-android-arm64@0.129.0': - optional: true - - '@oxc-parser/binding-darwin-arm64@0.129.0': - optional: true - - '@oxc-parser/binding-darwin-x64@0.129.0': - optional: true - - '@oxc-parser/binding-freebsd-x64@0.129.0': - optional: true - - '@oxc-parser/binding-linux-arm-gnueabihf@0.129.0': - optional: true - - '@oxc-parser/binding-linux-arm-musleabihf@0.129.0': - optional: true - - '@oxc-parser/binding-linux-arm64-gnu@0.129.0': - optional: true - - '@oxc-parser/binding-linux-arm64-musl@0.129.0': - optional: true - - '@oxc-parser/binding-linux-ppc64-gnu@0.129.0': - optional: true - - '@oxc-parser/binding-linux-riscv64-gnu@0.129.0': - optional: true - - '@oxc-parser/binding-linux-riscv64-musl@0.129.0': - optional: true - - '@oxc-parser/binding-linux-s390x-gnu@0.129.0': - optional: true - - '@oxc-parser/binding-linux-x64-gnu@0.129.0': - optional: true - - '@oxc-parser/binding-linux-x64-musl@0.129.0': - optional: true - - '@oxc-parser/binding-openharmony-arm64@0.129.0': - optional: true - - '@oxc-parser/binding-wasm32-wasi@0.129.0': - dependencies: - '@emnapi/core': 1.10.0 - '@emnapi/runtime': 1.10.0 - '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) - optional: true - - '@oxc-parser/binding-win32-arm64-msvc@0.129.0': - optional: true - - '@oxc-parser/binding-win32-ia32-msvc@0.129.0': - optional: true - - '@oxc-parser/binding-win32-x64-msvc@0.129.0': - optional: true - - '@oxc-project/types@0.129.0': - optional: true - - '@panva/hkdf@1.2.1': {} - '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -14728,8 +3653,6 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.6 '@parcel/watcher-win32-x64': 2.5.6 - '@pinojs/redact@0.4.0': {} - '@pkgjs/parseargs@0.11.0': optional: true @@ -14745,692 +3668,6 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@prisma/client@5.22.0(prisma@5.22.0)': - optionalDependencies: - prisma: 5.22.0 - - '@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3)': - optionalDependencies: - prisma: 6.19.3(magicast@0.5.3)(typescript@5.9.3) - typescript: 5.9.3 - optional: true - - '@prisma/client@6.19.3(prisma@6.19.3(typescript@5.9.3))(typescript@5.9.3)': - optionalDependencies: - prisma: 6.19.3(typescript@5.9.3) - typescript: 5.9.3 - - '@prisma/config@6.19.3': - dependencies: - c12: 3.1.0 - deepmerge-ts: 7.1.5 - effect: 3.21.0 - empathic: 2.0.0 - transitivePeerDependencies: - - magicast - - '@prisma/config@6.19.3(magicast@0.5.3)': - dependencies: - c12: 3.1.0(magicast@0.5.3) - deepmerge-ts: 7.1.5 - effect: 3.21.0 - empathic: 2.0.0 - transitivePeerDependencies: - - magicast - optional: true - - '@prisma/debug@5.22.0': {} - - '@prisma/debug@6.19.3': {} - - '@prisma/engines-version@5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2': {} - - '@prisma/engines-version@7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7': {} - - '@prisma/engines@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/fetch-engine': 5.22.0 - '@prisma/get-platform': 5.22.0 - - '@prisma/engines@6.19.3': - dependencies: - '@prisma/debug': 6.19.3 - '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 - '@prisma/fetch-engine': 6.19.3 - '@prisma/get-platform': 6.19.3 - - '@prisma/fetch-engine@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - '@prisma/engines-version': 5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2 - '@prisma/get-platform': 5.22.0 - - '@prisma/fetch-engine@6.19.3': - dependencies: - '@prisma/debug': 6.19.3 - '@prisma/engines-version': 7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7 - '@prisma/get-platform': 6.19.3 - - '@prisma/get-platform@5.22.0': - dependencies: - '@prisma/debug': 5.22.0 - - '@prisma/get-platform@6.19.3': - dependencies: - '@prisma/debug': 6.19.3 - - '@prisma/instrumentation@5.22.0': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/instrumentation': 0.53.0(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) - transitivePeerDependencies: - - supports-color - - '@protobufjs/aspromise@1.1.2': - optional: true - - '@protobufjs/base64@1.1.2': - optional: true - - '@protobufjs/codegen@2.0.5': - optional: true - - '@protobufjs/eventemitter@1.1.0': - optional: true - - '@protobufjs/fetch@1.1.0': - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.1 - optional: true - - '@protobufjs/float@1.0.2': - optional: true - - '@protobufjs/inquire@1.1.1': - optional: true - - '@protobufjs/path@1.1.2': - optional: true - - '@protobufjs/pool@1.1.0': - optional: true - - '@protobufjs/utf8@1.1.1': - optional: true - - '@react-email/render@0.0.16(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - html-to-text: 9.0.5 - js-beautify: 1.15.4 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - react-promise-suspense: 0.3.4 - - '@react-native-async-storage/async-storage@1.23.1(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))': - dependencies: - merge-options: 3.0.4 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - - '@react-native-async-storage/async-storage@1.23.1(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))': - dependencies: - merge-options: 3.0.4 - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - - '@react-native-community/cli-clean@13.6.9': - dependencies: - '@react-native-community/cli-tools': 13.6.9 - chalk: 4.1.2 - execa: 5.1.1 - fast-glob: 3.3.3 - transitivePeerDependencies: - - encoding - - '@react-native-community/cli-config@13.6.9': - dependencies: - '@react-native-community/cli-tools': 13.6.9 - chalk: 4.1.2 - cosmiconfig: 5.2.1 - deepmerge: 4.3.1 - fast-glob: 3.3.3 - joi: 17.13.3 - transitivePeerDependencies: - - encoding - - '@react-native-community/cli-debugger-ui@13.6.9': - dependencies: - serve-static: 1.16.3 - transitivePeerDependencies: - - supports-color - - '@react-native-community/cli-doctor@13.6.9': - dependencies: - '@react-native-community/cli-config': 13.6.9 - '@react-native-community/cli-platform-android': 13.6.9 - '@react-native-community/cli-platform-apple': 13.6.9 - '@react-native-community/cli-platform-ios': 13.6.9 - '@react-native-community/cli-tools': 13.6.9 - chalk: 4.1.2 - command-exists: 1.2.9 - deepmerge: 4.3.1 - envinfo: 7.21.0 - execa: 5.1.1 - hermes-profile-transformer: 0.0.6 - node-stream-zip: 1.15.0 - ora: 5.4.1 - semver: 7.8.0 - strip-ansi: 5.2.0 - wcwidth: 1.0.1 - yaml: 2.9.0 - transitivePeerDependencies: - - encoding - - '@react-native-community/cli-hermes@13.6.9': - dependencies: - '@react-native-community/cli-platform-android': 13.6.9 - '@react-native-community/cli-tools': 13.6.9 - chalk: 4.1.2 - hermes-profile-transformer: 0.0.6 - transitivePeerDependencies: - - encoding - - '@react-native-community/cli-platform-android@13.6.9': - dependencies: - '@react-native-community/cli-tools': 13.6.9 - chalk: 4.1.2 - execa: 5.1.1 - fast-glob: 3.3.3 - fast-xml-parser: 4.5.6 - logkitty: 0.7.1 - transitivePeerDependencies: - - encoding - - '@react-native-community/cli-platform-apple@13.6.9': - dependencies: - '@react-native-community/cli-tools': 13.6.9 - chalk: 4.1.2 - execa: 5.1.1 - fast-glob: 3.3.3 - fast-xml-parser: 4.5.6 - ora: 5.4.1 - transitivePeerDependencies: - - encoding - - '@react-native-community/cli-platform-ios@13.6.9': - dependencies: - '@react-native-community/cli-platform-apple': 13.6.9 - transitivePeerDependencies: - - encoding - - '@react-native-community/cli-server-api@13.6.9': - dependencies: - '@react-native-community/cli-debugger-ui': 13.6.9 - '@react-native-community/cli-tools': 13.6.9 - compression: 1.8.1 - connect: 3.7.0 - errorhandler: 1.5.2 - nocache: 3.0.4 - pretty-format: 26.6.2 - serve-static: 1.16.3 - ws: 6.2.3 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - utf-8-validate - - '@react-native-community/cli-tools@13.6.9': - dependencies: - appdirsjs: 1.2.7 - chalk: 4.1.2 - execa: 5.1.1 - find-up: 5.0.0 - mime: 2.6.0 - node-fetch: 2.7.0 - open: 6.4.0 - ora: 5.4.1 - semver: 7.8.0 - shell-quote: 1.8.3 - sudo-prompt: 9.2.1 - transitivePeerDependencies: - - encoding - - '@react-native-community/cli-types@13.6.9': - dependencies: - joi: 17.13.3 - - '@react-native-community/cli@13.6.9': - dependencies: - '@react-native-community/cli-clean': 13.6.9 - '@react-native-community/cli-config': 13.6.9 - '@react-native-community/cli-debugger-ui': 13.6.9 - '@react-native-community/cli-doctor': 13.6.9 - '@react-native-community/cli-hermes': 13.6.9 - '@react-native-community/cli-server-api': 13.6.9 - '@react-native-community/cli-tools': 13.6.9 - '@react-native-community/cli-types': 13.6.9 - chalk: 4.1.2 - commander: 9.5.0 - deepmerge: 4.3.1 - execa: 5.1.1 - find-up: 4.1.0 - fs-extra: 8.1.0 - graceful-fs: 4.2.11 - prompts: 2.4.2 - semver: 7.8.0 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - utf-8-validate - - '@react-native-community/netinfo@11.4.1(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))': - dependencies: - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - - '@react-native/assets-registry@0.74.87': {} - - '@react-native/assets-registry@0.85.3': {} - - '@react-native/babel-plugin-codegen@0.74.87(@babel/preset-env@7.29.5(@babel/core@7.29.0))': - dependencies: - '@react-native/codegen': 0.74.87(@babel/preset-env@7.29.5(@babel/core@7.29.0)) - transitivePeerDependencies: - - '@babel/preset-env' - - supports-color - - '@react-native/babel-plugin-codegen@0.83.6(@babel/core@7.29.0)': - dependencies: - '@babel/traverse': 7.29.0 - '@react-native/codegen': 0.83.6(@babel/core@7.29.0) - transitivePeerDependencies: - - '@babel/core' - - supports-color - - '@react-native/babel-preset@0.74.87(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))': - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-proposal-async-generator-functions': 7.20.7(@babel/core@7.29.0) - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.0) - '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-proposal-logical-assignment-operators': 7.20.7(@babel/core@7.29.0) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.29.0) - '@babel/plugin-proposal-numeric-separator': 7.18.6(@babel/core@7.29.0) - '@babel/plugin-proposal-object-rest-spread': 7.20.7(@babel/core@7.29.0) - '@babel/plugin-proposal-optional-catch-binding': 7.18.6(@babel/core@7.29.0) - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.29.0) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@babel/template': 7.28.6 - '@react-native/babel-plugin-codegen': 0.74.87(@babel/preset-env@7.29.5(@babel/core@7.29.0)) - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) - react-refresh: 0.14.2 - transitivePeerDependencies: - - '@babel/preset-env' - - supports-color - - '@react-native/babel-preset@0.83.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@babel/template': 7.28.6 - '@react-native/babel-plugin-codegen': 0.83.6(@babel/core@7.29.0) - babel-plugin-syntax-hermes-parser: 0.32.0 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) - react-refresh: 0.14.2 - transitivePeerDependencies: - - supports-color - - '@react-native/codegen@0.74.87(@babel/preset-env@7.29.5(@babel/core@7.29.0))': - dependencies: - '@babel/parser': 7.29.3 - '@babel/preset-env': 7.29.5(@babel/core@7.29.0) - glob: 7.2.3 - hermes-parser: 0.19.1 - invariant: 2.2.4 - jscodeshift: 0.14.0(@babel/preset-env@7.29.5(@babel/core@7.29.0)) - mkdirp: 0.5.6 - nullthrows: 1.1.1 - transitivePeerDependencies: - - supports-color - - '@react-native/codegen@0.83.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - glob: 7.2.3 - hermes-parser: 0.32.0 - invariant: 2.2.4 - nullthrows: 1.1.1 - yargs: 17.7.2 - - '@react-native/codegen@0.85.3(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - hermes-parser: 0.33.3 - invariant: 2.2.4 - nullthrows: 1.1.1 - tinyglobby: 0.2.16 - yargs: 17.7.2 - - '@react-native/community-cli-plugin@0.74.87(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))': - dependencies: - '@react-native-community/cli-server-api': 13.6.9 - '@react-native-community/cli-tools': 13.6.9 - '@react-native/dev-middleware': 0.74.87 - '@react-native/metro-babel-transformer': 0.74.87(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0)) - chalk: 4.1.2 - execa: 5.1.1 - metro: 0.80.12 - metro-config: 0.80.12 - metro-core: 0.80.12 - node-fetch: 2.7.0 - querystring: 0.2.1 - readline: 1.3.0 - transitivePeerDependencies: - - '@babel/core' - - '@babel/preset-env' - - bufferutil - - encoding - - supports-color - - utf-8-validate - - '@react-native/community-cli-plugin@0.85.3(@react-native-community/cli@13.6.9)': - dependencies: - '@react-native/dev-middleware': 0.85.3 - debug: 4.4.3 - invariant: 2.2.4 - metro: 0.84.4 - metro-config: 0.84.4 - metro-core: 0.84.4 - semver: 7.8.0 - optionalDependencies: - '@react-native-community/cli': 13.6.9 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@react-native/debugger-frontend@0.74.85': {} - - '@react-native/debugger-frontend@0.74.87': {} - - '@react-native/debugger-frontend@0.83.6': {} - - '@react-native/debugger-frontend@0.85.3': {} - - '@react-native/debugger-shell@0.83.6': - dependencies: - cross-spawn: 7.0.6 - fb-dotslash: 0.5.8 - - '@react-native/debugger-shell@0.85.3': - dependencies: - cross-spawn: 7.0.6 - debug: 4.4.3 - fb-dotslash: 0.5.8 - transitivePeerDependencies: - - supports-color - - '@react-native/dev-middleware@0.74.85': - dependencies: - '@isaacs/ttlcache': 1.4.1 - '@react-native/debugger-frontend': 0.74.85 - '@rnx-kit/chromium-edge-launcher': 1.0.0 - chrome-launcher: 0.15.2 - connect: 3.7.0 - debug: 2.6.9 - node-fetch: 2.7.0 - nullthrows: 1.1.1 - open: 7.4.2 - selfsigned: 2.4.1 - serve-static: 1.16.3 - temp-dir: 2.0.0 - ws: 6.2.3 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - utf-8-validate - - '@react-native/dev-middleware@0.74.87': - dependencies: - '@isaacs/ttlcache': 1.4.1 - '@react-native/debugger-frontend': 0.74.87 - '@rnx-kit/chromium-edge-launcher': 1.0.0 - chrome-launcher: 0.15.2 - connect: 3.7.0 - debug: 2.6.9 - node-fetch: 2.7.0 - nullthrows: 1.1.1 - open: 7.4.2 - selfsigned: 2.4.1 - serve-static: 1.16.3 - temp-dir: 2.0.0 - ws: 6.2.3 - transitivePeerDependencies: - - bufferutil - - encoding - - supports-color - - utf-8-validate - - '@react-native/dev-middleware@0.83.6': - dependencies: - '@isaacs/ttlcache': 1.4.1 - '@react-native/debugger-frontend': 0.83.6 - '@react-native/debugger-shell': 0.83.6 - chrome-launcher: 0.15.2 - chromium-edge-launcher: 0.2.0 - connect: 3.7.0 - debug: 4.4.3 - invariant: 2.2.4 - nullthrows: 1.1.1 - open: 7.4.2 - serve-static: 1.16.3 - ws: 7.5.10 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@react-native/dev-middleware@0.85.3': - dependencies: - '@isaacs/ttlcache': 1.4.1 - '@react-native/debugger-frontend': 0.85.3 - '@react-native/debugger-shell': 0.85.3 - chrome-launcher: 0.15.2 - chromium-edge-launcher: 0.3.0 - connect: 3.7.0 - debug: 4.4.3 - invariant: 2.2.4 - nullthrows: 1.1.1 - open: 7.4.2 - serve-static: 1.16.3 - ws: 7.5.10 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@react-native/gradle-plugin@0.74.87': {} - - '@react-native/gradle-plugin@0.85.3': {} - - '@react-native/js-polyfills@0.74.87': {} - - '@react-native/js-polyfills@0.85.3': {} - - '@react-native/metro-babel-transformer@0.74.87(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))': - dependencies: - '@babel/core': 7.29.0 - '@react-native/babel-preset': 0.74.87(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0)) - hermes-parser: 0.19.1 - nullthrows: 1.1.1 - transitivePeerDependencies: - - '@babel/preset-env' - - supports-color - - '@react-native/normalize-color@2.1.0': {} - - '@react-native/normalize-colors@0.74.85': {} - - '@react-native/normalize-colors@0.74.87': {} - - '@react-native/normalize-colors@0.83.6': {} - - '@react-native/normalize-colors@0.85.3': {} - - '@react-native/virtualized-lists@0.74.87(@types/react@18.3.28)(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)': - dependencies: - invariant: 2.2.4 - nullthrows: 1.1.1 - react: 18.2.0 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - optionalDependencies: - '@types/react': 18.3.28 - - '@react-native/virtualized-lists@0.85.3(@types/react@18.3.28)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)': - dependencies: - invariant: 2.2.4 - nullthrows: 1.1.1 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - optionalDependencies: - '@types/react': 18.3.28 - - '@react-navigation/bottom-tabs@6.6.1(@react-navigation/native@6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-screens@3.31.0(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)': - dependencies: - '@react-navigation/elements': 1.3.31(@react-navigation/native@6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - '@react-navigation/native': 6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - color: 4.2.3 - react: 18.2.0 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - react-native-safe-area-context: 4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - react-native-screens: 3.31.0(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - warn-once: 0.1.1 - - '@react-navigation/core@6.4.17(react@18.2.0)': - dependencies: - '@react-navigation/routers': 6.1.9 - escape-string-regexp: 4.0.0 - nanoid: 3.3.12 - query-string: 7.1.3 - react: 18.2.0 - react-is: 16.13.1 - use-latest-callback: 0.2.6(react@18.2.0) - - '@react-navigation/elements@1.3.31(@react-navigation/native@6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)': - dependencies: - '@react-navigation/native': 6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - react: 18.2.0 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - react-native-safe-area-context: 4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - - '@react-navigation/native@6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)': - dependencies: - '@react-navigation/core': 6.4.17(react@18.2.0) - escape-string-regexp: 4.0.0 - fast-deep-equal: 3.1.3 - nanoid: 3.3.12 - react: 18.2.0 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - - '@react-navigation/routers@6.1.9': - dependencies: - nanoid: 3.3.12 - - '@react-navigation/stack@6.4.1(@react-navigation/native@6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-gesture-handler@2.16.2(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-screens@3.31.0(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)': - dependencies: - '@react-navigation/elements': 1.3.31(@react-navigation/native@6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - '@react-navigation/native': 6.1.18(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - color: 4.2.3 - react: 18.2.0 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - react-native-gesture-handler: 2.16.2(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - react-native-safe-area-context: 4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - react-native-screens: 3.31.0(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - warn-once: 0.1.1 - - '@rnx-kit/chromium-edge-launcher@1.0.0': - dependencies: - '@types/node': 18.19.130 - escape-string-regexp: 4.0.0 - is-wsl: 2.2.0 - lighthouse-logger: 1.4.2 - mkdirp: 1.0.4 - rimraf: 3.0.2 - transitivePeerDependencies: - - supports-color - '@rollup/plugin-alias@6.0.0(rollup@4.60.4)': optionalDependencies: rollup: 4.60.4 @@ -15482,7 +3719,7 @@ snapshots: dependencies: serialize-javascript: 7.0.5 smob: 1.6.2 - terser: 5.47.1 + terser: 5.48.0 optionalDependencies: rollup: 4.60.4 @@ -15569,90 +3806,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true - '@segment/analytics-core@1.4.1': - dependencies: - '@lukeed/uuid': 2.0.1 - '@segment/analytics-generic-utils': 1.1.1 - dset: 3.1.4 - tslib: 2.8.1 - - '@segment/analytics-generic-utils@1.1.1': - dependencies: - tslib: 2.8.1 - - '@segment/analytics-node@1.3.0': - dependencies: - '@lukeed/uuid': 2.0.1 - '@segment/analytics-core': 1.4.1 - '@segment/analytics-generic-utils': 1.1.1 - buffer: 6.0.3 - node-fetch: 2.7.0 - tslib: 2.8.1 - transitivePeerDependencies: - - encoding - - '@segment/loosely-validate-event@2.0.0': - dependencies: - component-type: 1.2.2 - join-component: 1.1.0 - - '@selderee/plugin-htmlparser2@0.11.0': - dependencies: - domhandler: 5.0.3 - selderee: 0.11.0 - - '@sentry/core@8.55.2': {} - - '@sentry/node@8.55.2': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-connect': 0.43.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-dataloader': 0.16.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-express': 0.47.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-fastify': 0.44.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-fs': 0.19.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-generic-pool': 0.43.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-graphql': 0.47.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-hapi': 0.45.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-http': 0.57.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-ioredis': 0.47.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-kafkajs': 0.7.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-knex': 0.44.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-koa': 0.47.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-lru-memoizer': 0.44.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-mongodb': 0.51.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-mongoose': 0.46.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-mysql': 0.45.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-mysql2': 0.45.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-nestjs-core': 0.44.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-pg': 0.50.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-redis-4': 0.46.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-tedious': 0.18.0(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation-undici': 0.10.0(@opentelemetry/api@1.9.1) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - '@prisma/instrumentation': 5.22.0 - '@sentry/core': 8.55.2 - '@sentry/opentelemetry': 8.55.2(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1) - import-in-the-middle: 1.15.0 - transitivePeerDependencies: - - supports-color - - '@sentry/opentelemetry@8.55.2(@opentelemetry/api@1.9.1)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.41.1)': - dependencies: - '@opentelemetry/api': 1.9.1 - '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.1) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.1) - '@opentelemetry/semantic-conventions': 1.41.1 - '@sentry/core': 8.55.2 - '@shikijs/core@1.29.2': dependencies: '@shikijs/engine-javascript': 1.29.2 @@ -15688,102 +3841,25 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} - '@sideway/address@4.1.5': - dependencies: - '@hapi/hoek': 9.3.0 - - '@sideway/formula@3.0.1': {} - - '@sideway/pinpoint@2.0.0': {} - - '@sinclair/typebox@0.27.10': {} - '@sindresorhus/is@7.2.0': {} '@sindresorhus/merge-streams@4.0.0': {} - '@sinonjs/commons@3.0.1': + '@solidjs/meta@0.29.4(solid-js@1.9.13)': dependencies: - type-detect: 4.0.8 + solid-js: 1.9.13 - '@sinonjs/fake-timers@10.3.0': + '@solidjs/router@0.15.4(solid-js@1.9.13)': dependencies: - '@sinonjs/commons': 3.0.1 + solid-js: 1.9.13 - '@smithy/core@3.24.2': + '@solidjs/start@2.0.0-alpha.2(crossws@0.3.5)(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: - '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.3.2': - dependencies: - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.4.2': - dependencies: - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/middleware-compression@4.4.2': - dependencies: - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - fflate: 0.8.1 - tslib: 2.8.1 - - '@smithy/node-http-handler@4.7.2': - dependencies: - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/signature-v4@5.4.2': - dependencies: - '@smithy/core': 3.24.2 - '@smithy/types': 4.14.1 - tslib: 2.8.1 - - '@smithy/types@4.14.1': - dependencies: - tslib: 2.8.1 - - '@smithy/util-buffer-from@2.2.0': - dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-utf8@2.3.0': - dependencies: - '@smithy/util-buffer-from': 2.2.0 - tslib: 2.8.1 - - '@solidjs/meta@0.29.4(solid-js@1.9.12)': - dependencies: - solid-js: 1.9.12 - - '@solidjs/router@0.14.10(solid-js@1.9.12)': - dependencies: - solid-js: 1.9.12 - - '@solidjs/router@0.15.4(solid-js@1.9.12)': - dependencies: - solid-js: 1.9.12 - - '@solidjs/start@2.0.0-alpha.2(crossws@0.3.5)(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0))': - dependencies: - '@babel/core': 7.29.0 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@solidjs/meta': 0.29.4(solid-js@1.9.12) - '@tanstack/server-functions-plugin': 1.134.5(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0)) + '@babel/core': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@solidjs/meta': 0.29.4(solid-js@1.9.13) + '@tanstack/server-functions-plugin': 1.134.5(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) '@types/babel__traverse': 7.28.0 '@types/micromatch': 4.0.10 cookie-es: 2.0.1 @@ -15801,21 +3877,21 @@ snapshots: seroval: 1.5.4 seroval-plugins: 1.5.4(seroval@1.5.4) shiki: 1.29.2 - solid-js: 1.9.12 + solid-js: 1.9.13 source-map-js: 1.2.1 srvx: 0.9.8 - terracotta: 1.1.0(solid-js@1.9.12) - vite: 7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0) - vite-plugin-solid: 2.11.12(solid-js@1.9.12)(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0)) + terracotta: 1.1.0(solid-js@1.9.13) + vite: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + vite-plugin-solid: 2.11.12(solid-js@1.9.13)(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) transitivePeerDependencies: - '@testing-library/jest-dom' - crossws - supports-color - '@solidjs/vite-plugin-nitro-2@0.1.0(@libsql/client@0.17.3)(drizzle-orm@0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3)))(oxc-parser@0.129.0)(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0))(xml2js@0.6.0)': + '@solidjs/vite-plugin-nitro-2@0.1.0(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: - nitropack: 2.13.4(@libsql/client@0.17.3)(drizzle-orm@0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3)))(oxc-parser@0.129.0)(xml2js@0.6.0) - vite: 7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0) + nitropack: 2.13.4 + vite: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -15853,34 +3929,94 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@swc/helpers@0.3.17': + '@tailwindcss/node@4.3.0': dependencies: - tslib: 2.8.1 + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 - '@swc/helpers@0.5.15': + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: - tslib: 2.8.1 + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) - '@tanstack/directive-functions-plugin@1.134.5(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0))': + '@tanstack/directive-functions-plugin@1.134.5(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/core': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 '@tanstack/router-utils': 1.133.19 babel-dead-code-elimination: 1.0.12 pathe: 2.0.3 tiny-invariant: 1.3.3 - vite: 7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0) + vite: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) transitivePeerDependencies: - supports-color '@tanstack/router-utils@1.133.19': dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) ansis: 4.3.0 diff: 8.0.4 pathe: 2.0.3 @@ -15888,183 +4024,84 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/server-functions-plugin@1.134.5(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0))': + '@tanstack/server-functions-plugin@1.134.5(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@tanstack/directive-functions-plugin': 1.134.5(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0)) + '@babel/core': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@tanstack/directive-functions-plugin': 1.134.5(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) babel-dead-code-elimination: 1.0.12 tiny-invariant: 1.3.3 transitivePeerDependencies: - supports-color - vite - '@tootallnate/once@2.0.1': - optional: true - '@trpc/client@10.45.4(@trpc/server@10.45.4)': dependencies: '@trpc/server': 10.45.4 '@trpc/server@10.45.4': {} - '@tsconfig/node10@1.0.12': {} - - '@tsconfig/node12@1.0.11': {} - - '@tsconfig/node14@1.0.3': {} - - '@tsconfig/node16@1.0.4': {} - - '@turbo/darwin-64@2.9.12': + '@turbo/darwin-64@2.9.14': optional: true - '@turbo/darwin-arm64@2.9.12': + '@turbo/darwin-arm64@2.9.14': optional: true - '@turbo/linux-64@2.9.12': + '@turbo/linux-64@2.9.14': optional: true - '@turbo/linux-arm64@2.9.12': + '@turbo/linux-arm64@2.9.14': optional: true - '@turbo/windows-64@2.9.12': + '@turbo/windows-64@2.9.14': optional: true - '@turbo/windows-arm64@2.9.12': - optional: true - - '@tybys/wasm-util@0.10.2': - dependencies: - tslib: 2.8.1 + '@turbo/windows-arm64@2.9.14': optional: true '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.0 - - '@types/body-parser@1.19.6': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 25.8.0 + '@babel/types': 7.29.7 '@types/braces@3.0.5': {} - '@types/caseless@0.12.5': - optional: true - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/chrome@0.0.268': - dependencies: - '@types/filesystem': 0.0.36 - '@types/har-format': 1.2.16 - - '@types/connect@3.4.36': - dependencies: - '@types/node': 25.8.0 - - '@types/connect@3.4.38': - dependencies: - '@types/node': 25.8.0 - '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} '@types/estree@1.0.9': {} - '@types/express-serve-static-core@4.19.8': - dependencies: - '@types/node': 25.8.0 - '@types/qs': 6.15.1 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@types/express@4.17.25': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 4.19.8 - '@types/qs': 6.15.1 - '@types/serve-static': 1.15.10 - - '@types/filesystem@0.0.36': - dependencies: - '@types/filewriter': 0.0.33 - - '@types/filewriter@0.0.33': {} - - '@types/graceful-fs@4.1.9': - dependencies: - '@types/node': 25.8.0 - - '@types/hammerjs@2.0.46': {} - - '@types/handlebars@4.1.0': - dependencies: - handlebars: 4.7.9 - - '@types/har-format@1.2.16': {} - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 - '@types/http-errors@2.0.5': {} - - '@types/istanbul-lib-coverage@2.0.6': {} - - '@types/istanbul-lib-report@3.0.3': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - - '@types/istanbul-reports@1.1.2': - dependencies: - '@types/istanbul-lib-coverage': 2.0.6 - '@types/istanbul-lib-report': 3.0.3 - - '@types/istanbul-reports@3.0.4': - dependencies: - '@types/istanbul-lib-report': 3.0.3 - - '@types/jest@29.5.14': - dependencies: - expect: 29.7.0 - pretty-format: 29.7.0 - - '@types/jsonwebtoken@9.0.10': - dependencies: - '@types/ms': 2.1.0 - '@types/node': 25.8.0 - - '@types/long@4.0.2': - optional: true - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -16073,125 +4110,14 @@ snapshots: dependencies: '@types/braces': 3.0.5 - '@types/mime@1.3.5': {} - - '@types/ms@2.1.0': {} - - '@types/mysql@2.15.26': - dependencies: - '@types/node': 25.8.0 - - '@types/node-forge@1.3.14': - dependencies: - '@types/node': 25.8.0 - - '@types/node@18.19.130': - dependencies: - undici-types: 5.26.5 - - '@types/node@20.19.41': - dependencies: - undici-types: 6.21.0 - - '@types/node@22.19.19': - dependencies: - undici-types: 6.21.0 - - '@types/node@25.8.0': + '@types/node@25.9.1': dependencies: undici-types: 7.24.6 - '@types/pdfkit@0.13.9': - dependencies: - '@types/node': 25.8.0 - - '@types/pg-pool@2.0.6': - dependencies: - '@types/pg': 8.6.1 - - '@types/pg@8.6.1': - dependencies: - '@types/node': 25.8.0 - pg-protocol: 1.13.0 - pg-types: 2.2.0 - - '@types/prop-types@15.7.15': {} - - '@types/qs@6.15.1': {} - - '@types/range-parser@1.2.7': {} - - '@types/react-test-renderer@18.3.1': - dependencies: - '@types/react': 18.3.28 - - '@types/react@18.3.28': - dependencies: - '@types/prop-types': 15.7.15 - csstype: 3.2.3 - - '@types/request@2.48.13': - dependencies: - '@types/caseless': 0.12.5 - '@types/node': 25.8.0 - '@types/tough-cookie': 4.0.5 - form-data: 2.5.5 - optional: true - '@types/resolve@1.20.2': {} - '@types/send@0.17.6': - dependencies: - '@types/mime': 1.3.5 - '@types/node': 25.8.0 - - '@types/send@1.2.1': - dependencies: - '@types/node': 25.8.0 - - '@types/serve-static@1.15.10': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 25.8.0 - '@types/send': 0.17.6 - - '@types/shimmer@1.2.0': {} - - '@types/stack-utils@2.0.3': {} - - '@types/tedious@4.0.14': - dependencies: - '@types/node': 25.8.0 - - '@types/tough-cookie@4.0.5': - optional: true - '@types/unist@3.0.3': {} - '@types/uuid@10.0.0': {} - - '@types/uuid@11.0.0': - dependencies: - uuid: 14.0.0 - - '@types/ws@8.18.1': - dependencies: - '@types/node': 25.8.0 - - '@types/yargs-parser@21.0.3': {} - - '@types/yargs@13.0.12': - dependencies: - '@types/yargs-parser': 21.0.3 - - '@types/yargs@15.0.20': - dependencies: - '@types/yargs-parser': 21.0.3 - - '@types/yargs@17.0.35': - dependencies: - '@types/yargs-parser': 21.0.3 - '@typeschema/core@0.13.2': {} '@typeschema/valibot@0.13.5(valibot@0.29.0)': @@ -16204,18 +4130,6 @@ snapshots: '@ungap/structured-clone@1.3.1': {} - '@urql/core@2.3.6(graphql@15.8.0)': - dependencies: - '@graphql-typed-document-node/core': 3.2.0(graphql@15.8.0) - graphql: 15.8.0 - wonka: 4.0.15 - - '@urql/exchange-retry@0.3.0(graphql@15.8.0)': - dependencies: - '@urql/core': 2.3.6(graphql@15.8.0) - graphql: 15.8.0 - wonka: 4.0.15 - '@vercel/nft@1.5.0(rollup@4.60.4)': dependencies: '@mapbox/node-pre-gyp': 2.0.3 @@ -16235,11 +4149,11 @@ snapshots: - rollup - supports-color - '@vitest/coverage-v8@4.1.6(vitest@4.1.6)': + '@vitest/coverage-v8@4.1.7(vitest@4.1.7)': dependencies: '@bcoe/v8-coverage': 1.0.2 - '@vitest/utils': 4.1.6 - ast-v8-to-istanbul: 1.0.0 + '@vitest/utils': 4.1.7 + ast-v8-to-istanbul: 1.0.2 istanbul-lib-coverage: 3.2.2 istanbul-lib-report: 3.0.1 istanbul-reports: 3.2.0 @@ -16247,161 +4161,80 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) + vitest: 4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) - '@vitest/expect@4.1.6': + '@vitest/expect@4.1.7': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.6(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1))': + '@vitest/mocker@4.1.7(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: - '@vitest/spy': 4.1.6 + '@vitest/spy': 4.1.7 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1) + vite: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) - '@vitest/pretty-format@4.1.6': + '@vitest/pretty-format@4.1.7': dependencies: tinyrainbow: 3.1.0 - '@vitest/runner@4.1.6': + '@vitest/runner@4.1.7': dependencies: - '@vitest/utils': 4.1.6 + '@vitest/utils': 4.1.7 pathe: 2.0.3 - '@vitest/snapshot@4.1.6': + '@vitest/snapshot@4.1.7': dependencies: - '@vitest/pretty-format': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/pretty-format': 4.1.7 + '@vitest/utils': 4.1.7 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.1.6': {} + '@vitest/spy@4.1.7': {} - '@vitest/utils@4.1.6': + '@vitest/utils@4.1.7': dependencies: - '@vitest/pretty-format': 4.1.6 + '@vitest/pretty-format': 4.1.7 convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@xmldom/xmldom@0.7.13': {} - - '@xmldom/xmldom@0.8.13': {} - - '@xmldom/xmldom@0.9.10': {} - - abbrev@2.0.0: {} - abbrev@3.0.1: {} abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 - abstract-logging@2.0.1: {} - - accepts@1.3.8: - dependencies: - mime-types: 2.1.35 - negotiator: 0.6.3 - - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - acorn-import-attributes@1.9.5(acorn@8.16.0): dependencies: acorn: 8.16.0 - acorn-jsx@5.3.2(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - - acorn-walk@8.3.5: - dependencies: - acorn: 8.16.0 - acorn@8.16.0: {} - agent-base@6.0.2: - dependencies: - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - agent-base@7.1.4: {} - aggregate-error@3.1.0: - dependencies: - clean-stack: 2.2.0 - indent-string: 4.0.0 - - ajv-formats@3.0.1(ajv@8.20.0): - optionalDependencies: - ajv: 8.20.0 - - ajv@6.15.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ajv@8.20.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 - json-schema-traverse: 1.0.0 - require-from-string: 2.0.2 - - anser@1.4.10: {} - - ansi-escapes@4.3.2: - dependencies: - type-fest: 0.21.3 - - ansi-fragments@0.2.1: - dependencies: - colorette: 1.4.0 - slice-ansi: 2.1.0 - strip-ansi: 5.2.0 - - ansi-regex@4.1.1: {} - ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@5.2.0: {} - ansi-styles@6.2.3: {} ansis@4.3.0: {} - any-promise@1.3.0: {} - anymatch@3.1.3: dependencies: normalize-path: 3.0.0 picomatch: 2.3.2 - appdirsjs@1.2.7: {} - archiver-utils@5.0.2: dependencies: glob: 10.5.0 @@ -16426,349 +4259,44 @@ snapshots: - bare-buffer - react-native-b4a - arg@4.1.0: {} - - arg@4.1.3: {} - - arg@5.0.2: {} - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - - argparse@2.0.1: {} - - array-buffer-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - is-array-buffer: 3.0.5 - - array-flatten@1.1.1: {} - - array-includes@3.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - is-string: 1.1.1 - math-intrinsics: 1.1.0 - - array-union@2.1.0: {} - - array.prototype.findlast@1.2.5: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-shim-unscopables: 1.1.0 - - array.prototype.flat@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 - - array.prototype.flatmap@1.3.3: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-shim-unscopables: 1.1.0 - - array.prototype.tosorted@1.1.4: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-shim-unscopables: 1.1.0 - - arraybuffer.prototype.slice@1.0.4: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - is-array-buffer: 3.0.5 - - arrify@2.0.1: - optional: true - - asap@2.0.6: {} - - assert@2.1.0: - dependencies: - call-bind: 1.0.9 - is-nan: 1.3.2 - object-is: 1.1.6 - object.assign: 4.1.7 - util: 0.12.5 - assertion-error@2.0.1: {} - ast-types@0.15.2: - dependencies: - tslib: 2.8.1 - - ast-v8-to-istanbul@1.0.0: + ast-v8-to-istanbul@1.0.2: dependencies: '@jridgewell/trace-mapping': 0.3.31 estree-walker: 3.0.3 js-tokens: 10.0.0 - astral-regex@1.0.0: {} - - async-function@1.0.0: {} - - async-limiter@1.0.1: {} - - async-retry@1.3.3: - dependencies: - retry: 0.13.1 - optional: true - async-sema@3.1.1: {} async@3.2.6: {} - asynckit@0.4.0: {} - - at-least-node@1.0.0: {} - - atomic-sleep@1.0.0: {} - - available-typed-arrays@1.0.7: - dependencies: - possible-typed-array-names: 1.1.0 - - avvio@9.2.0: - dependencies: - '@fastify/error': 4.2.0 - fastq: 1.20.1 - - axios@1.16.1: - dependencies: - follow-redirects: 1.16.0 - form-data: 4.0.5 - https-proxy-agent: 5.0.1 - proxy-from-env: 2.1.0 - transitivePeerDependencies: - - debug - - supports-color - b4a@1.8.1: {} - babel-core@7.0.0-bridge.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - babel-dead-code-elimination@1.0.12: dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 transitivePeerDependencies: - supports-color - babel-jest@29.7.0(@babel/core@7.29.0): + babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@7.29.7): dependencies: - '@babel/core': 7.29.0 - '@jest/transform': 29.7.0 - '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.29.0) - chalk: 4.1.2 - graceful-fs: 4.2.11 - slash: 3.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-istanbul@6.1.1: - dependencies: - '@babel/helper-plugin-utils': 7.28.6 - '@istanbuljs/load-nyc-config': 1.1.0 - '@istanbuljs/schema': 0.1.6 - istanbul-lib-instrument: 5.2.1 - test-exclude: 6.0.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jest-hoist@29.6.3: - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - '@types/babel__core': 7.20.5 - '@types/babel__traverse': 7.28.0 - - babel-plugin-jsx-dom-expressions@0.40.6(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@babel/helper-module-imports': 7.18.6 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.7 html-entities: 2.3.3 parse5: 7.3.0 - babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): + babel-preset-solid@1.9.12(@babel/core@7.29.7)(solid-js@1.9.13): dependencies: - '@babel/compat-data': 7.29.3 - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) - core-js-compat: 3.49.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) - core-js-compat: 3.49.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - babel-plugin-react-compiler@0.0.0-experimental-592953e-20240517: - dependencies: - '@babel/generator': 7.2.0 - '@babel/types': 7.29.0 - chalk: 4.1.2 - invariant: 2.2.4 - pretty-format: 24.9.0 - zod: 3.25.76 - zod-validation-error: 2.1.0(zod@3.25.76) - - babel-plugin-react-compiler@1.0.0: - dependencies: - '@babel/types': 7.29.0 - - babel-plugin-react-native-web@0.19.13: {} - - babel-plugin-react-native-web@0.21.2: {} - - babel-plugin-syntax-hermes-parser@0.32.0: - dependencies: - hermes-parser: 0.32.0 - - babel-plugin-syntax-hermes-parser@0.32.1: - dependencies: - hermes-parser: 0.32.1 - - babel-plugin-syntax-hermes-parser@0.33.3: - dependencies: - hermes-parser: 0.33.3 - - babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.0): - dependencies: - '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) - transitivePeerDependencies: - - '@babel/core' - - babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) - - babel-preset-expo@11.0.15(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0)): - dependencies: - '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/preset-react': 7.28.5(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@react-native/babel-preset': 0.74.87(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0)) - babel-plugin-react-compiler: 0.0.0-experimental-592953e-20240517 - babel-plugin-react-native-web: 0.19.13 - react-refresh: 0.14.2 - transitivePeerDependencies: - - '@babel/core' - - '@babel/preset-env' - - supports-color - - babel-preset-expo@55.0.21(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@55.0.24)(react-refresh@0.14.2): - dependencies: - '@babel/generator': 7.29.1 - '@babel/helper-module-imports': 7.28.6 - '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) - '@babel/preset-react': 7.28.5(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@react-native/babel-preset': 0.83.6(@babel/core@7.29.0) - babel-plugin-react-compiler: 1.0.0 - babel-plugin-react-native-web: 0.21.2 - babel-plugin-syntax-hermes-parser: 0.32.1 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) - debug: 4.4.3 - react-refresh: 0.14.2 - resolve-from: 5.0.0 + '@babel/core': 7.29.7 + babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@7.29.7) optionalDependencies: - '@babel/runtime': 7.29.2 - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - transitivePeerDependencies: - - '@babel/core' - - supports-color - - babel-preset-jest@29.6.3(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) - - babel-preset-solid@1.9.12(@babel/core@7.29.0)(solid-js@1.9.12): - dependencies: - '@babel/core': 7.29.0 - babel-plugin-jsx-dom-expressions: 0.40.6(@babel/core@7.29.0) - optionalDependencies: - solid-js: 1.9.12 - - badgin@1.2.3: {} + solid-js: 1.9.13 balanced-match@1.0.2: {} @@ -16806,73 +4334,15 @@ snapshots: dependencies: bare-path: 3.0.0 - base64-js@0.0.8: {} - base64-js@1.5.1: {} - baseline-browser-mapping@2.10.29: {} - - better-opn@3.0.2: - dependencies: - open: 8.4.2 - - big-integer@1.6.52: {} - - bignumber.js@9.3.1: {} + baseline-browser-mapping@2.10.32: {} bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 - bl@4.1.0: - dependencies: - buffer: 5.7.1 - inherits: 2.0.4 - readable-stream: 3.6.2 - - body-parser@1.20.5: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - on-finished: 2.4.1 - qs: 6.15.1 - raw-body: 2.5.3 - type-is: 1.6.18 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - boolbase@1.0.0: {} - - bowser@2.14.1: {} - - bplist-creator@0.0.7: - dependencies: - stream-buffers: 2.2.0 - - bplist-creator@0.1.0: - dependencies: - stream-buffers: 2.2.0 - - bplist-parser@0.3.1: - dependencies: - big-integer: 1.6.52 - - bplist-parser@0.3.2: - dependencies: - big-integer: 1.6.52 - - brace-expansion@1.1.14: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.1.0: + brace-expansion@2.1.1: dependencies: balanced-match: 1.0.2 @@ -16884,107 +4354,27 @@ snapshots: dependencies: fill-range: 7.1.1 - brotli@1.3.3: - dependencies: - base64-js: 1.5.1 - - browserify-zlib@0.2.0: - dependencies: - pako: 1.0.11 - browserslist@4.28.2: dependencies: - baseline-browser-mapping: 2.10.29 - caniuse-lite: 1.0.30001792 - electron-to-chromium: 1.5.355 - node-releases: 2.0.44 + baseline-browser-mapping: 2.10.32 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.361 + node-releases: 2.0.46 update-browserslist-db: 1.2.3(browserslist@4.28.2) - bs-logger@0.2.6: - dependencies: - fast-json-stable-stringify: 2.1.0 - - bser@2.1.1: - dependencies: - node-int64: 0.4.0 - - buffer-alloc-unsafe@1.1.0: {} - - buffer-alloc@1.2.0: - dependencies: - buffer-alloc-unsafe: 1.1.0 - buffer-fill: 1.0.0 - buffer-crc32@1.0.0: {} - buffer-equal-constant-time@1.0.1: {} - - buffer-fill@1.0.0: {} - buffer-from@1.1.2: {} - buffer@5.7.1: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - buffer@6.0.3: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 - builtins@1.0.3: {} - - bullmq@5.76.8: - dependencies: - cron-parser: 4.9.0 - ioredis: 5.10.1 - msgpackr: 2.0.1 - node-abort-controller: 3.1.1 - semver: 7.8.0 - tslib: 2.8.1 - transitivePeerDependencies: - - supports-color - bundle-name@4.1.0: dependencies: run-applescript: 7.1.0 - bytes@3.1.2: {} - - c12@3.1.0: - dependencies: - chokidar: 4.0.3 - confbox: 0.2.4 - defu: 6.1.7 - dotenv: 16.6.1 - exsolve: 1.0.8 - giget: 2.0.0 - jiti: 2.7.0 - ohash: 2.0.11 - pathe: 2.0.3 - perfect-debounce: 1.0.0 - pkg-types: 2.3.1 - rc9: 2.1.2 - - c12@3.1.0(magicast@0.5.3): - dependencies: - chokidar: 4.0.3 - confbox: 0.2.4 - defu: 6.1.7 - dotenv: 16.6.1 - exsolve: 1.0.8 - giget: 2.0.0 - jiti: 2.7.0 - ohash: 2.0.11 - pathe: 2.0.3 - perfect-debounce: 1.0.0 - pkg-types: 2.3.1 - rc9: 2.1.2 - optionalDependencies: - magicast: 0.5.3 - optional: true - c12@3.3.4(magicast@0.5.3): dependencies: chokidar: 5.0.0 @@ -17002,242 +4392,50 @@ snapshots: optionalDependencies: magicast: 0.5.3 - cacache@18.0.4: - dependencies: - '@npmcli/fs': 3.1.1 - fs-minipass: 3.0.3 - glob: 10.5.0 - lru-cache: 10.4.3 - minipass: 7.1.3 - minipass-collect: 2.0.1 - minipass-flush: 1.0.7 - minipass-pipeline: 1.2.4 - p-map: 4.0.0 - ssri: 10.0.6 - tar: 6.2.1 - unique-filename: 3.0.0 - - cache-manager@6.4.3: - dependencies: - keyv: 5.6.0 - - cacheable@1.10.4: - dependencies: - hookified: 1.15.1 - keyv: 5.6.0 - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bind@1.0.9: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - get-intrinsic: 1.3.0 - set-function-length: 1.2.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - - caller-callsite@2.0.0: - dependencies: - callsites: 2.0.0 - - caller-path@2.0.0: - dependencies: - caller-callsite: 2.0.0 - - callsites@2.0.0: {} - - callsites@3.1.0: {} - - camelcase@5.3.1: {} - - camelcase@6.3.0: {} - - caniuse-lite@1.0.30001792: {} + caniuse-lite@1.0.30001793: {} ccount@2.0.1: {} chai@6.2.2: {} - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - char-regex@1.0.2: {} - character-entities-html4@2.1.0: {} character-entities-legacy@3.0.0: {} - charenc@0.0.2: {} - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - chokidar@5.0.0: dependencies: readdirp: 5.0.0 - chownr@2.0.0: {} - chownr@3.0.0: {} - chrome-launcher@0.15.2: - dependencies: - '@types/node': 25.8.0 - escape-string-regexp: 4.0.0 - is-wsl: 2.2.0 - lighthouse-logger: 1.4.2 - transitivePeerDependencies: - - supports-color - - chromium-edge-launcher@0.2.0: - dependencies: - '@types/node': 25.8.0 - escape-string-regexp: 4.0.0 - is-wsl: 2.2.0 - lighthouse-logger: 1.4.2 - mkdirp: 1.0.4 - rimraf: 3.0.2 - transitivePeerDependencies: - - supports-color - - chromium-edge-launcher@0.3.0: - dependencies: - '@types/node': 25.8.0 - escape-string-regexp: 4.0.0 - is-wsl: 2.2.0 - lighthouse-logger: 1.4.2 - mkdirp: 1.0.4 - transitivePeerDependencies: - - supports-color - - ci-info@2.0.0: {} - - ci-info@3.9.0: {} - citty@0.1.6: dependencies: consola: 3.4.2 citty@0.2.2: {} - cjs-module-lexer@1.4.3: {} - - cjs-module-lexer@2.2.0: {} - - clean-stack@2.2.0: {} - - cli-cursor@2.1.0: - dependencies: - restore-cursor: 2.0.0 - - cli-cursor@3.1.0: - dependencies: - restore-cursor: 3.1.0 - - cli-spinners@2.9.2: {} - - client-only@0.0.1: {} - - cliui@6.0.0: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - cliui@9.0.1: dependencies: string-width: 7.2.0 strip-ansi: 7.2.0 wrap-ansi: 9.0.2 - clone-deep@4.0.1: - dependencies: - is-plain-object: 2.0.4 - kind-of: 6.0.3 - shallow-clone: 3.0.1 - - clone@1.0.4: {} - - clone@2.1.2: {} - cluster-key-slot@1.1.2: {} - co@4.6.0: {} - - collect-v8-coverage@1.0.3: {} - - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.4 - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - - colorette@1.4.0: {} - - combined-stream@1.0.8: - dependencies: - delayed-stream: 1.0.0 - comma-separated-tokens@2.0.3: {} - command-exists@1.2.9: {} - - commander@10.0.1: {} - - commander@12.1.0: {} - commander@2.20.3: {} - commander@4.1.1: {} - - commander@7.2.0: {} - - commander@9.5.0: {} - commondir@1.0.1: {} compatx@0.2.0: {} - component-type@1.2.2: {} - compress-commons@6.0.2: dependencies: crc-32: 1.2.2 @@ -17246,54 +4444,12 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.7.0 - compressible@2.0.18: - dependencies: - mime-db: 1.54.0 - - compression@1.8.1: - dependencies: - bytes: 3.1.2 - compressible: 2.0.18 - debug: 2.6.9 - negotiator: 0.6.4 - on-headers: 1.1.0 - safe-buffer: 5.2.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - - concat-map@0.0.1: {} - confbox@0.1.8: {} confbox@0.2.4: {} - config-chain@1.1.13: - dependencies: - ini: 1.3.8 - proto-list: 1.2.4 - - connect@3.7.0: - dependencies: - debug: 2.6.9 - finalhandler: 1.1.2 - parseurl: 1.3.3 - utils-merge: 1.0.1 - transitivePeerDependencies: - - supports-color - consola@3.4.2: {} - content-disposition@0.5.4: - dependencies: - safe-buffer: 5.2.1 - - content-disposition@1.1.0: {} - - content-type@1.0.5: {} - - content-type@2.0.0: {} - convert-source-map@2.0.0: {} cookie-es@1.2.3: {} @@ -17302,25 +4458,8 @@ snapshots: cookie-es@3.1.1: {} - cookie-signature@1.0.7: {} - - cookie@0.7.2: {} - - cookie@1.1.1: {} - - core-js-compat@3.49.0: - dependencies: - browserslist: 4.28.2 - core-util-is@1.0.3: {} - cosmiconfig@5.2.1: - dependencies: - import-fresh: 2.0.0 - is-directory: 0.3.1 - js-yaml: 3.14.2 - parse-json: 4.0.0 - crc-32@1.2.2: {} crc32-stream@6.0.0: @@ -17328,58 +4467,8 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.7.0 - create-jest@29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - create-jest@29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)): - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - exit: 0.1.2 - graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)) - jest-util: 29.7.0 - prompts: 2.4.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - create-require@1.1.1: {} - - cron-parser@4.9.0: - dependencies: - luxon: 3.7.2 - croner@10.0.1: {} - cross-fetch@3.2.0: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - - cross-spawn@6.0.6: - dependencies: - nice-try: 1.0.5 - path-key: 2.0.1 - semver: 5.7.2 - shebang-command: 1.2.0 - which: 1.3.1 - cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -17390,123 +4479,14 @@ snapshots: dependencies: uncrypto: 0.1.3 - crypt@0.0.2: {} - - crypto-js@4.2.0: {} - - crypto-random-string@1.0.0: {} - - crypto-random-string@2.0.0: {} - - css-select@5.2.2: - dependencies: - boolbase: 1.0.0 - css-what: 6.2.2 - domhandler: 5.0.3 - domutils: 3.2.2 - nth-check: 2.1.1 - - css-tree@1.1.3: - dependencies: - mdn-data: 2.0.14 - source-map: 0.6.1 - - css-what@6.2.2: {} - csstype@3.2.3: {} - dag-map@1.0.2: {} - - data-view-buffer@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-length@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - data-view-byte-offset@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-data-view: 1.0.2 - - dayjs@1.11.20: {} - - db0@0.3.4(@libsql/client@0.17.3)(drizzle-orm@0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))): - optionalDependencies: - '@libsql/client': 0.17.3 - drizzle-orm: 0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3)) - - dc-polyfill@0.1.11: {} - - dd-trace@5.103.0(@openfeature/server-sdk@1.21.0(@openfeature/core@1.10.0)): - dependencies: - dc-polyfill: 0.1.11 - import-in-the-middle: 3.0.1 - optionalDependencies: - '@datadog/libdatadog': 0.9.3 - '@datadog/native-appsec': 11.0.1 - '@datadog/native-iast-taint-tracking': 4.1.0 - '@datadog/native-metrics': 3.1.2 - '@datadog/openfeature-node-server': 1.1.2(@openfeature/server-sdk@1.21.0(@openfeature/core@1.10.0)) - '@datadog/pprof': 5.14.1 - '@datadog/wasm-js-rewriter': 5.0.1 - '@opentelemetry/api': 1.9.1 - '@opentelemetry/api-logs': 0.218.0 - oxc-parser: 0.129.0 - transitivePeerDependencies: - - '@openfeature/server-sdk' - - debug@2.6.9: - dependencies: - ms: 2.0.0 - - debug@3.2.7: - dependencies: - ms: 2.1.3 + db0@0.3.4: {} debug@4.4.3: dependencies: ms: 2.1.3 - decamelize@1.2.0: {} - - decode-uri-component@0.2.2: {} - - dedent@1.7.2: {} - - deep-equal@2.2.3: - dependencies: - array-buffer-byte-length: 1.0.2 - call-bind: 1.0.9 - es-get-iterator: 1.1.3 - get-intrinsic: 1.3.0 - is-arguments: 1.2.0 - is-array-buffer: 3.0.5 - is-date-object: 1.1.0 - is-regex: 1.2.1 - is-shared-array-buffer: 1.0.4 - isarray: 2.0.5 - object-is: 1.1.6 - object-keys: 1.1.1 - object.assign: 4.1.7 - regexp.prototype.flags: 1.5.4 - side-channel: 1.1.0 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.20 - - deep-extend@0.6.0: {} - - deep-is@0.1.4: {} - - deepmerge-ts@7.1.5: {} - deepmerge@4.3.1: {} default-browser-id@5.0.1: {} @@ -17516,48 +4496,10 @@ snapshots: bundle-name: 4.1.0 default-browser-id: 5.0.1 - default-gateway@4.2.0: - dependencies: - execa: 1.0.0 - ip-regex: 2.1.0 - - defaults@1.0.4: - dependencies: - clone: 1.0.4 - - define-data-property@1.1.4: - dependencies: - es-define-property: 1.0.1 - es-errors: 1.3.0 - gopd: 1.2.0 - - define-lazy-prop@2.0.0: {} - define-lazy-prop@3.0.0: {} - define-properties@1.2.1: - dependencies: - define-data-property: 1.1.4 - has-property-descriptors: 1.0.2 - object-keys: 1.1.1 - defu@6.1.7: {} - del@6.1.1: - dependencies: - globby: 11.1.0 - graceful-fs: 4.2.11 - is-glob: 4.0.3 - is-path-cwd: 2.2.0 - is-path-inside: 3.0.3 - p-map: 4.0.0 - rimraf: 3.0.2 - slash: 3.0.0 - - delayed-stream@1.0.0: {} - - denodeify@1.2.1: {} - denque@2.1.0: {} depd@2.0.0: {} @@ -17566,130 +4508,27 @@ snapshots: destr@2.0.5: {} - destroy@1.2.0: {} - - detect-libc@1.0.3: {} - - detect-libc@2.0.2: {} - detect-libc@2.1.2: {} - detect-newline@3.1.0: {} - devlop@1.1.0: dependencies: dequal: 2.0.3 - dfa@1.2.0: {} - - diff-sequences@29.6.3: {} - - diff@4.0.4: {} - diff@8.0.4: {} - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 - - dnssd-advertise@1.1.4: {} - - doctrine@2.1.0: - dependencies: - esutils: 2.0.3 - - doctrine@3.0.0: - dependencies: - esutils: 2.0.3 - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dot-prop@10.1.0: dependencies: type-fest: 5.6.0 - dotenv-expand@11.0.7: - dependencies: - dotenv: 16.6.1 - - dotenv@16.4.7: {} - - dotenv@16.6.1: {} - dotenv@17.4.2: {} - drizzle-kit@0.31.10: - dependencies: - '@drizzle-team/brocli': 0.10.2 - '@esbuild-kit/esm-loader': 2.6.5 - esbuild: 0.25.12 - tsx: 4.22.0 - - drizzle-orm@0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3)): - optionalDependencies: - '@libsql/client': 0.17.3 - '@opentelemetry/api': 1.9.1 - '@prisma/client': 6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3) - '@types/pg': 8.6.1 - prisma: 6.19.3(magicast@0.5.3)(typescript@5.9.3) - - dset@3.1.4: {} - - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - duplexer@0.1.2: {} - duplexify@4.1.3: - dependencies: - end-of-stream: 1.4.5 - inherits: 2.0.4 - readable-stream: 3.6.2 - stream-shift: 1.0.3 - optional: true - eastasianwidth@0.2.0: {} - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - - editorconfig@1.0.7: - dependencies: - '@one-ini/wasm': 0.1.1 - commander: 10.0.1 - minimatch: 9.0.9 - semver: 7.8.0 - ee-first@1.1.1: {} - effect@3.21.0: - dependencies: - '@standard-schema/spec': 1.1.0 - fast-check: 3.23.2 - - electron-to-chromium@1.5.355: {} - - emittery@0.13.1: {} + electron-to-chromium@1.5.361: {} emoji-regex-xs@1.0.0: {} @@ -17699,207 +4538,27 @@ snapshots: emoji-regex@9.2.2: {} - empathic@2.0.0: {} - - encodeurl@1.0.2: {} - encodeurl@2.0.0: {} - end-of-stream@1.4.5: + enhanced-resolve@5.22.0: dependencies: - once: 1.4.0 - - entities@4.5.0: {} + graceful-fs: 4.2.11 + tapable: 2.3.3 entities@6.0.1: {} - env-editor@0.4.2: {} - - envinfo@7.21.0: {} - - error-ex@1.3.4: - dependencies: - is-arrayish: 0.2.1 - error-stack-parser-es@1.0.5: {} error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 - errorhandler@1.5.2: - dependencies: - accepts: 1.3.8 - escape-html: 1.0.3 - - es-abstract@1.24.2: - dependencies: - array-buffer-byte-length: 1.0.2 - arraybuffer.prototype.slice: 1.0.4 - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - data-view-buffer: 1.0.2 - data-view-byte-length: 1.0.2 - data-view-byte-offset: 1.0.1 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - es-set-tostringtag: 2.1.0 - es-to-primitive: 1.3.0 - function.prototype.name: 1.1.8 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - get-symbol-description: 1.1.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.3 - internal-slot: 1.1.0 - is-array-buffer: 3.0.5 - is-callable: 1.2.7 - is-data-view: 1.0.2 - is-negative-zero: 2.0.3 - is-regex: 1.2.1 - is-set: 2.0.3 - is-shared-array-buffer: 1.0.4 - is-string: 1.1.1 - is-typed-array: 1.1.15 - is-weakref: 1.1.1 - math-intrinsics: 1.1.0 - object-inspect: 1.13.4 - object-keys: 1.1.1 - object.assign: 4.1.7 - own-keys: 1.0.1 - regexp.prototype.flags: 1.5.4 - safe-array-concat: 1.1.4 - safe-push-apply: 1.0.0 - safe-regex-test: 1.1.0 - set-proto: 1.0.0 - stop-iteration-iterator: 1.1.0 - string.prototype.trim: 1.2.10 - string.prototype.trimend: 1.0.9 - string.prototype.trimstart: 1.0.8 - typed-array-buffer: 1.0.3 - typed-array-byte-length: 1.0.3 - typed-array-byte-offset: 1.0.4 - typed-array-length: 1.0.7 - unbox-primitive: 1.1.0 - which-typed-array: 1.1.20 - - es-define-property@1.0.1: {} - es-errors@1.3.0: {} - es-get-iterator@1.1.3: - dependencies: - call-bind: 1.0.9 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - is-arguments: 1.2.0 - is-map: 2.0.3 - is-set: 2.0.3 - is-string: 1.1.1 - isarray: 2.0.5 - stop-iteration-iterator: 1.1.0 - - es-iterator-helpers@1.3.2: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-set-tostringtag: 2.1.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - globalthis: 1.0.4 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - has-proto: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - iterator.prototype: 1.1.5 - math-intrinsics: 1.1.0 - es-module-lexer@1.7.0: {} es-module-lexer@2.1.0: {} - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - - es-set-tostringtag@2.1.0: - dependencies: - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - has-tostringtag: 1.0.2 - hasown: 2.0.3 - - es-shim-unscopables@1.1.0: - dependencies: - hasown: 2.0.3 - - es-to-primitive@1.3.0: - dependencies: - is-callable: 1.2.7 - is-date-object: 1.1.0 - is-symbol: 1.1.1 - - esbuild@0.18.20: - optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 - - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 - esbuild@0.25.12: optionalDependencies: '@esbuild/aix-ppc64': 0.25.12 @@ -17991,119 +4650,14 @@ snapshots: escape-html@1.0.3: {} - escape-string-regexp@1.0.5: {} - - escape-string-regexp@2.0.0: {} - - escape-string-regexp@4.0.0: {} - escape-string-regexp@5.0.0: {} - eslint-plugin-react-native-globals@0.1.2: {} - - eslint-plugin-react-native@4.1.0(eslint@8.57.1): - dependencies: - eslint: 8.57.1 - eslint-plugin-react-native-globals: 0.1.2 - - eslint-plugin-react@7.37.5(eslint@8.57.1): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.3.2 - eslint: 8.57.1 - estraverse: 5.3.0 - hasown: 2.0.3 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.5 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.7 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint@8.57.1: - dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1) - '@eslint-community/regexpp': 4.12.2 - '@eslint/eslintrc': 2.1.4 - '@eslint/js': 8.57.1 - '@humanwhocodes/config-array': 0.13.0 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.3.1 - ajv: 6.15.0 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.3 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.7.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.24.0 - graphemer: 1.4.0 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.1 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.5 - natural-compare: 1.4.0 - optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - - espree@9.6.1: - dependencies: - acorn: 8.16.0 - acorn-jsx: 5.3.2(acorn@8.16.0) - eslint-visitor-keys: 3.4.3 - - esprima@4.0.1: {} - - esquery@1.7.0: - dependencies: - estraverse: 5.3.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - estree-walker@2.0.2: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.9 - esutils@2.0.3: {} - etag@1.8.1: {} event-target-shim@5.0.1: {} @@ -18116,357 +4670,10 @@ snapshots: events@3.3.0: {} - execa@1.0.0: - dependencies: - cross-spawn: 6.0.6 - get-stream: 4.1.0 - is-stream: 1.1.0 - npm-run-path: 2.0.2 - p-finally: 1.0.0 - signal-exit: 3.0.7 - strip-eof: 1.0.0 - - execa@5.1.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 6.0.1 - human-signals: 2.1.0 - is-stream: 2.0.1 - merge-stream: 2.0.0 - npm-run-path: 4.0.1 - onetime: 5.1.2 - signal-exit: 3.0.7 - strip-final-newline: 2.0.0 - - exit@0.1.2: {} - expect-type@1.3.0: {} - expect@29.7.0: - dependencies: - '@jest/expect-utils': 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - - expo-application@5.9.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - - expo-asset@10.0.10(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - expo-constants: 16.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - invariant: 2.2.4 - md5-file: 3.2.3 - transitivePeerDependencies: - - supports-color - - expo-asset@55.0.17(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3): - dependencies: - '@expo/image-utils': 0.8.14(typescript@5.9.3) - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - expo-constants: 55.0.16(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)) - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - transitivePeerDependencies: - - supports-color - - typescript - - expo-av@14.0.7(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - - expo-constants@16.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - '@expo/config': 9.0.4 - '@expo/env': 0.3.0 - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - transitivePeerDependencies: - - supports-color - - expo-constants@55.0.16(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)): - dependencies: - '@expo/env': 2.1.2 - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - transitivePeerDependencies: - - supports-color - - expo-crypto@13.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - base64-js: 1.5.1 - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - - expo-device@6.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - ua-parser-js: 0.7.41 - - expo-eas-client@0.6.0: {} - - expo-file-system@17.0.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - - expo-file-system@55.0.20(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)): - dependencies: - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - - expo-font@12.0.10(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - fontfaceobserver: 2.3.0 - - expo-font@55.0.7(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6): - dependencies: - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - fontfaceobserver: 2.3.0 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - - expo-image-loader@4.7.0(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - - expo-image-picker@15.0.7(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - expo-image-loader: 4.7.0(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - - expo-json-utils@0.7.1: {} - - expo-keep-awake@13.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - - expo-keep-awake@55.0.8(expo@55.0.24)(react@19.2.6): - dependencies: - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - react: 19.2.6 - - expo-linking@6.3.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo-constants: 16.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - invariant: 2.2.4 - transitivePeerDependencies: - - expo - - supports-color - - expo-local-authentication@14.0.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - invariant: 2.2.4 - - expo-manifests@0.7.2: - dependencies: - expo-json-utils: 0.7.1 - - expo-modules-autolinking@1.11.3: - dependencies: - chalk: 4.1.2 - commander: 7.2.0 - fast-glob: 3.3.3 - find-up: 5.0.0 - fs-extra: 9.1.0 - require-from-string: 2.0.2 - resolve-from: 5.0.0 - - expo-modules-autolinking@55.0.22(typescript@5.9.3): - dependencies: - '@expo/require-utils': 55.0.5(typescript@5.9.3) - '@expo/spawn-async': 1.7.2 - chalk: 4.1.2 - commander: 7.2.0 - transitivePeerDependencies: - - supports-color - - typescript - - expo-modules-core@1.12.26: - dependencies: - invariant: 2.2.4 - - expo-modules-core@55.0.25(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6): - dependencies: - invariant: 2.2.4 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - - expo-notifications@0.28.19(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - '@expo/image-utils': 0.5.1 - '@ide/backoff': 1.0.0 - abort-controller: 3.0.0 - assert: 2.1.0 - badgin: 1.2.3 - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - expo-application: 5.9.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-constants: 16.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - fs-extra: 9.1.0 - transitivePeerDependencies: - - encoding - - supports-color - - expo-secure-store@12.8.1(expo@55.0.24): - dependencies: - expo: 55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - - expo-secure-store@13.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - - expo-server@55.0.9: {} - - expo-status-bar@1.12.1: {} - - expo-structured-headers@3.3.0: {} - - expo-updates-interface@0.10.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - - expo-updates@0.18.19(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)): - dependencies: - '@expo/code-signing-certificates': 0.0.5 - '@expo/config': 8.1.2 - '@expo/config-plugins': 7.2.5 - arg: 4.1.0 - chalk: 4.1.2 - expo: 51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - expo-eas-client: 0.6.0 - expo-manifests: 0.7.2 - expo-structured-headers: 3.3.0 - expo-updates-interface: 0.10.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - fbemitter: 3.0.0 - resolve-from: 5.0.0 - transitivePeerDependencies: - - encoding - - supports-color - - expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0): - dependencies: - '@babel/runtime': 7.29.2 - '@expo/cli': 0.18.31(expo-modules-autolinking@1.11.3) - '@expo/config': 9.0.4 - '@expo/config-plugins': 8.0.11 - '@expo/metro-config': 0.18.11 - '@expo/vector-icons': 14.1.0(expo-font@12.0.10(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - babel-preset-expo: 11.0.15(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0)) - expo-asset: 10.0.10(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-file-system: 17.0.1(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-font: 12.0.10(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-keep-awake: 13.0.2(expo@51.0.39(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0)) - expo-modules-autolinking: 1.11.3 - expo-modules-core: 1.12.26 - fbemitter: 3.0.0 - whatwg-url-without-unicode: 8.0.0-3 - transitivePeerDependencies: - - '@babel/core' - - '@babel/preset-env' - - bufferutil - - encoding - - react - - react-native - - supports-color - - utf-8-validate - - expo@55.0.24(@babel/core@7.29.0)(@expo/dom-webview@55.0.6)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3): - dependencies: - '@babel/runtime': 7.29.2 - '@expo/cli': 55.0.30(@expo/dom-webview@55.0.6)(expo-constants@55.0.16(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)))(expo-font@55.0.7(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6))(expo@55.0.24)(react-dom@19.2.6(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - '@expo/config': 55.0.17(typescript@5.9.3) - '@expo/config-plugins': 55.0.9 - '@expo/devtools': 55.0.3(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - '@expo/fingerprint': 0.16.7 - '@expo/local-build-cache-provider': 55.0.13(typescript@5.9.3) - '@expo/log-box': 55.0.12(@expo/dom-webview@55.0.6)(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - '@expo/metro': 55.1.1 - '@expo/metro-config': 55.0.21(expo@55.0.24)(typescript@5.9.3) - '@expo/vector-icons': 15.1.1(expo-font@55.0.7(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6))(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - '@ungap/structured-clone': 1.3.1 - babel-preset-expo: 55.0.21(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@55.0.24)(react-refresh@0.14.2) - expo-asset: 55.0.17(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6)(typescript@5.9.3) - expo-constants: 55.0.16(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)) - expo-file-system: 55.0.20(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6)) - expo-font: 55.0.7(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - expo-keep-awake: 55.0.8(expo@55.0.24)(react@19.2.6) - expo-modules-autolinking: 55.0.22(typescript@5.9.3) - expo-modules-core: 55.0.25(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - pretty-format: 29.7.0 - react: 19.2.6 - react-native: 0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6) - react-refresh: 0.14.2 - whatwg-url-minimum: 0.1.2 - optionalDependencies: - '@expo/dom-webview': 55.0.6(expo@55.0.24)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - transitivePeerDependencies: - - '@babel/core' - - bufferutil - - expo-router - - expo-widgets - - react-dom - - react-native-worklets - - react-server-dom-webpack - - supports-color - - typescript - - utf-8-validate - - exponential-backoff@3.1.3: {} - - express@4.22.2: - dependencies: - accepts: 1.3.8 - array-flatten: 1.1.1 - body-parser: 1.20.5 - content-disposition: 0.5.4 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.0.7 - debug: 2.6.9 - depd: 2.0.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 1.3.2 - fresh: 0.5.2 - http-errors: 2.0.1 - merge-descriptors: 1.0.3 - methods: 1.1.2 - on-finished: 2.4.1 - parseurl: 1.3.3 - path-to-regexp: 0.1.13 - proxy-addr: 2.0.7 - qs: 6.15.1 - range-parser: 1.2.1 - safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 - setprototypeof: 1.2.0 - statuses: 2.0.2 - type-is: 1.6.18 - utils-merge: 1.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - exsolve@1.0.8: {} - extend@3.0.2: {} - - farmhash-modern@1.1.0: {} - - fast-check@3.23.2: - dependencies: - pure-rand: 6.1.0 - - fast-decode-uri-component@1.0.1: {} - - fast-deep-equal@2.0.1: {} - - fast-deep-equal@3.1.3: {} - fast-fifo@1.3.2: {} fast-glob@3.3.3: @@ -18477,417 +4684,46 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} - - fast-json-stringify@6.4.0: - dependencies: - '@fastify/merge-json-schemas': 0.2.1 - ajv: 8.20.0 - ajv-formats: 3.0.1(ajv@8.20.0) - fast-uri: 3.1.2 - json-schema-ref-resolver: 3.0.0 - rfdc: 1.4.1 - - fast-levenshtein@2.0.6: {} - - fast-querystring@1.1.2: - dependencies: - fast-decode-uri-component: 1.0.1 - - fast-uri@3.1.2: {} - - fast-xml-builder@1.2.0: - dependencies: - path-expression-matcher: 1.5.0 - xml-naming: 0.1.0 - - fast-xml-parser@4.5.6: - dependencies: - strnum: 1.1.2 - - fast-xml-parser@5.7.2: - dependencies: - '@nodable/entities': 2.1.0 - fast-xml-builder: 1.2.0 - path-expression-matcher: 1.5.0 - strnum: 2.3.0 - - fast-xml-parser@5.8.0: - dependencies: - '@nodable/entities': 2.1.0 - fast-xml-builder: 1.2.0 - path-expression-matcher: 1.5.0 - strnum: 2.3.0 - xml-naming: 0.1.0 - optional: true - - fastify-plugin@4.5.1: {} - - fastify-plugin@5.1.0: {} - - fastify-raw-body@5.0.0: - dependencies: - fastify-plugin: 5.1.0 - raw-body: 3.0.2 - secure-json-parse: 2.7.0 - - fastify@5.8.5: - dependencies: - '@fastify/ajv-compiler': 4.0.5 - '@fastify/error': 4.2.0 - '@fastify/fast-json-stringify-compiler': 5.0.3 - '@fastify/proxy-addr': 5.1.0 - abstract-logging: 2.0.1 - avvio: 9.2.0 - fast-json-stringify: 6.4.0 - find-my-way: 9.6.0 - light-my-request: 6.6.0 - pino: 10.3.1 - process-warning: 5.0.0 - rfdc: 1.4.1 - secure-json-parse: 4.1.0 - semver: 7.8.0 - toad-cache: 3.7.0 - fastq@1.20.1: dependencies: reusify: 1.1.0 - faye-websocket@0.11.4: - dependencies: - websocket-driver: 0.7.4 - - fb-dotslash@0.5.8: {} - - fb-watchman@2.0.2: - dependencies: - bser: 2.1.1 - - fbemitter@3.0.0: - dependencies: - fbjs: 3.0.5 - transitivePeerDependencies: - - encoding - - fbjs-css-vars@1.0.2: {} - - fbjs@3.0.5: - dependencies: - cross-fetch: 3.2.0 - fbjs-css-vars: 1.0.2 - loose-envify: 1.4.0 - object-assign: 4.1.1 - promise: 7.3.1 - setimmediate: 1.0.5 - ua-parser-js: 1.0.41 - transitivePeerDependencies: - - encoding - fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 - fetch-nodeshim@0.4.10: {} - - fetch-retry@4.1.1: {} - - fflate@0.8.1: {} - - file-entry-cache@6.0.1: - dependencies: - flat-cache: 3.2.0 - file-uri-to-path@1.0.0: {} fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 - filter-obj@1.1.0: {} - - finalhandler@1.1.2: - dependencies: - debug: 2.6.9 - encodeurl: 1.0.2 - escape-html: 1.0.3 - on-finished: 2.3.0 - parseurl: 1.3.3 - statuses: 1.5.0 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - finalhandler@1.3.2: - dependencies: - debug: 2.6.9 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - unpipe: 1.0.0 - transitivePeerDependencies: - - supports-color - - find-cache-dir@2.1.0: - dependencies: - commondir: 1.0.1 - make-dir: 2.1.0 - pkg-dir: 3.0.0 - - find-my-way@9.6.0: - dependencies: - fast-deep-equal: 3.1.3 - fast-querystring: 1.1.2 - safe-regex2: 5.1.1 - - find-up@3.0.0: - dependencies: - locate-path: 3.0.0 - - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - find-yarn-workspace-root@2.0.0: - dependencies: - micromatch: 4.0.8 - - firebase-admin@12.7.0: - dependencies: - '@fastify/busboy': 3.2.0 - '@firebase/database-compat': 1.0.8 - '@firebase/database-types': 1.0.5 - '@types/node': 22.19.19 - farmhash-modern: 1.1.0 - jsonwebtoken: 9.0.3 - jwks-rsa: 3.2.2 - node-forge: 1.4.0 - uuid: 10.0.0 - optionalDependencies: - '@google-cloud/firestore': 7.11.6 - '@google-cloud/storage': 7.19.0 - transitivePeerDependencies: - - encoding - - supports-color - - flat-cache@3.2.0: - dependencies: - flatted: 3.4.2 - keyv: 4.5.4 - rimraf: 3.0.2 - - flatted@3.4.2: {} - - flow-enums-runtime@0.0.6: {} - - flow-parser@0.314.0: {} - - follow-redirects@1.16.0: {} - - fontfaceobserver@2.3.0: {} - - fontkit@1.9.0: - dependencies: - '@swc/helpers': 0.3.17 - brotli: 1.3.3 - clone: 2.1.2 - deep-equal: 2.2.3 - dfa: 1.2.0 - restructure: 2.0.1 - tiny-inflate: 1.0.3 - unicode-properties: 1.4.1 - unicode-trie: 2.0.0 - - for-each@0.3.5: - dependencies: - is-callable: 1.2.7 - foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 - form-data@2.5.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.3 - mime-types: 2.1.35 - safe-buffer: 5.2.1 - optional: true - - form-data@3.0.4: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.3 - mime-types: 2.1.35 - - form-data@4.0.5: - dependencies: - asynckit: 0.4.0 - combined-stream: 1.0.8 - es-set-tostringtag: 2.1.0 - hasown: 2.0.3 - mime-types: 2.1.35 - - forwarded-parse@2.1.2: {} - - forwarded@0.2.0: {} - - freeport-async@2.0.0: {} - - fresh@0.5.2: {} - fresh@2.0.0: {} - fs-extra@8.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - - fs-extra@9.0.0: - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.2.1 - universalify: 1.0.0 - - fs-extra@9.1.0: - dependencies: - at-least-node: 1.0.0 - graceful-fs: 4.2.11 - jsonfile: 6.2.1 - universalify: 2.0.1 - - fs-minipass@2.1.0: - dependencies: - minipass: 3.3.6 - - fs-minipass@3.0.3: - dependencies: - minipass: 7.1.3 - - fs.realpath@1.0.0: {} - fsevents@2.3.3: optional: true function-bind@1.1.2: {} - function.prototype.name@1.1.8: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - functions-have-names: 1.2.3 - hasown: 2.0.3 - is-callable: 1.2.7 - - functional-red-black-tree@1.0.1: - optional: true - - functions-have-names@1.2.3: {} - - gaxios@6.7.1: - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - is-stream: 2.0.1 - node-fetch: 2.7.0 - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - gcp-metadata@6.1.1: - dependencies: - gaxios: 6.7.1 - google-logging-utils: 0.0.2 - json-bigint: 1.0.0 - transitivePeerDependencies: - - encoding - - supports-color - - generator-function@2.0.1: {} - gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} get-east-asian-width@1.6.0: {} - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.3 - math-intrinsics: 1.1.0 - - get-package-type@0.1.0: {} - get-port-please@3.2.0: {} - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - - get-stream@4.1.0: - dependencies: - pump: 3.0.4 - - get-stream@6.0.1: {} - - get-symbol-description@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - - get-tsconfig@4.14.0: - dependencies: - resolve-pkg-maps: 1.0.0 - - getenv@1.0.0: {} - - getenv@2.0.0: {} - - giget@2.0.0: - dependencies: - citty: 0.1.6 - consola: 3.4.2 - defu: 6.1.7 - node-fetch-native: 1.6.7 - nypm: 0.6.6 - pathe: 2.0.3 - giget@3.2.0: {} glob-parent@5.1.2: dependencies: is-glob: 4.0.3 - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -18903,50 +4739,6 @@ snapshots: minipass: 7.1.3 path-scurry: 2.0.2 - glob@7.1.6: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 - - glob@7.2.3: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.5 - once: 1.4.0 - path-is-absolute: 1.0.1 - - glob@8.1.0: - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 5.1.9 - once: 1.4.0 - - globals@13.24.0: - dependencies: - type-fest: 0.20.2 - - globalthis@1.0.4: - dependencies: - define-properties: 1.2.1 - gopd: 1.2.0 - - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.3 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 - globby@16.2.0: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -18956,80 +4748,8 @@ snapshots: slash: 5.1.0 unicorn-magic: 0.4.0 - google-auth-library@9.15.1: - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 6.7.1 - gcp-metadata: 6.1.1 - gtoken: 7.1.0 - jws: 4.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - google-gax@4.6.1: - dependencies: - '@grpc/grpc-js': 1.14.3 - '@grpc/proto-loader': 0.7.15 - '@types/long': 4.0.2 - abort-controller: 3.0.0 - duplexify: 4.1.3 - google-auth-library: 9.15.1 - node-fetch: 2.7.0 - object-hash: 3.0.0 - proto3-json-serializer: 2.0.2 - protobufjs: 7.5.8 - retry-request: 7.0.2 - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - - google-logging-utils@0.0.2: {} - - googleapis-common@7.2.0: - dependencies: - extend: 3.0.2 - gaxios: 6.7.1 - google-auth-library: 9.15.1 - qs: 6.15.1 - url-template: 2.0.8 - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - googleapis@128.0.0: - dependencies: - google-auth-library: 9.15.1 - googleapis-common: 7.2.0 - transitivePeerDependencies: - - encoding - - supports-color - - gopd@1.2.0: {} - graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - - graphql-tag@2.12.6(graphql@15.8.0): - dependencies: - graphql: 15.8.0 - tslib: 2.8.1 - - graphql@15.8.0: {} - - gtoken@7.1.0: - dependencies: - gaxios: 6.7.1 - jws: 4.0.1 - transitivePeerDependencies: - - encoding - - supports-color - gzip-size@7.0.0: dependencies: duplexer: 0.1.2 @@ -19053,35 +4773,8 @@ snapshots: optionalDependencies: crossws: 0.3.5 - handlebars@4.7.9: - dependencies: - minimist: 1.2.8 - neo-async: 2.6.2 - source-map: 0.6.1 - wordwrap: 1.0.0 - optionalDependencies: - uglify-js: 3.19.3 - - has-bigints@1.1.0: {} - - has-flag@3.0.0: {} - has-flag@4.0.0: {} - has-property-descriptors@1.0.2: - dependencies: - es-define-property: 1.0.1 - - has-proto@1.2.0: - dependencies: - dunder-proto: 1.0.1 - - has-symbols@1.1.0: {} - - has-tostringtag@1.0.2: - dependencies: - has-symbols: 1.1.0 - hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -19104,100 +4797,16 @@ snapshots: dependencies: '@types/hast': 3.0.4 - helmet@8.1.0: {} - - hermes-compiler@250829098.0.10: {} - - hermes-estree@0.19.1: {} - - hermes-estree@0.23.1: {} - - hermes-estree@0.32.0: {} - - hermes-estree@0.32.1: {} - - hermes-estree@0.33.3: {} - - hermes-estree@0.35.0: {} - - hermes-parser@0.19.1: - dependencies: - hermes-estree: 0.19.1 - - hermes-parser@0.23.1: - dependencies: - hermes-estree: 0.23.1 - - hermes-parser@0.32.0: - dependencies: - hermes-estree: 0.32.0 - - hermes-parser@0.32.1: - dependencies: - hermes-estree: 0.32.1 - - hermes-parser@0.33.3: - dependencies: - hermes-estree: 0.33.3 - - hermes-parser@0.35.0: - dependencies: - hermes-estree: 0.35.0 - - hermes-profile-transformer@0.0.6: - dependencies: - source-map: 0.7.6 - - hoist-non-react-statics@3.3.2: - dependencies: - react-is: 16.13.1 - hookable@5.5.3: {} - hookified@1.15.1: {} - - hosted-git-info@3.0.8: - dependencies: - lru-cache: 6.0.0 - - hosted-git-info@7.0.2: - dependencies: - lru-cache: 10.4.3 - html-entities@2.3.3: {} - html-entities@2.6.0: - optional: true - html-escaper@2.0.2: {} html-to-image@1.11.13: {} - html-to-text@9.0.5: - dependencies: - '@selderee/plugin-htmlparser2': 0.11.0 - deepmerge: 4.3.1 - dom-serializer: 2.0.0 - htmlparser2: 8.0.2 - selderee: 0.11.0 - html-void-elements@3.0.0: {} - htmlparser2@8.0.2: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 4.5.0 - - http-errors@2.0.0: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.1 - toidentifier: 1.0.1 - http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -19206,26 +4815,8 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - http-parser-js@0.5.10: {} - - http-proxy-agent@5.0.0: - dependencies: - '@tootallnate/once': 2.0.1 - agent-base: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - optional: true - http-shutdown@1.2.2: {} - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -19235,83 +4826,12 @@ snapshots: httpxy@0.5.3: {} - human-signals@2.1.0: {} - - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - - iconv-lite@0.7.2: - dependencies: - safer-buffer: 2.1.2 - ieee754@1.2.1: {} - ignore@5.3.2: {} - ignore@7.0.5: {} - image-size@1.2.1: - dependencies: - queue: 6.0.2 - - import-fresh@2.0.0: - dependencies: - caller-path: 2.0.0 - resolve-from: 3.0.0 - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - import-in-the-middle@1.15.0: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 1.4.3 - module-details-from-path: 1.0.4 - - import-in-the-middle@3.0.1: - dependencies: - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - cjs-module-lexer: 2.2.0 - module-details-from-path: 1.0.4 - - import-local@3.2.0: - dependencies: - pkg-dir: 4.2.0 - resolve-cwd: 3.0.0 - - imurmurhash@0.1.4: {} - - indent-string@4.0.0: {} - - inflight@1.0.6: - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - inherits@2.0.4: {} - ini@1.3.8: {} - - internal-ip@4.3.0: - dependencies: - default-gateway: 4.2.0 - ipaddr.js: 1.9.1 - - internal-slot@1.1.0: - dependencies: - es-errors: 1.3.0 - hasown: 2.0.3 - side-channel: 1.1.0 - - invariant@2.2.4: - dependencies: - loose-envify: 1.4.0 - ioredis@5.10.1: dependencies: '@ioredis/commands': 1.5.1 @@ -19326,97 +4846,18 @@ snapshots: transitivePeerDependencies: - supports-color - ip-regex@2.1.0: {} - - ipaddr.js@1.9.1: {} - - ipaddr.js@2.4.0: {} - iron-webcrypto@1.2.1: {} - is-arguments@1.2.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-array-buffer@3.0.5: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - - is-arrayish@0.2.1: {} - - is-arrayish@0.3.4: {} - - is-async-function@2.1.1: - dependencies: - async-function: 1.0.0 - call-bound: 1.0.4 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-bigint@1.1.0: - dependencies: - has-bigints: 1.1.0 - - is-boolean-object@1.2.2: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-buffer@1.1.6: {} - - is-callable@1.2.7: {} - is-core-module@2.16.2: dependencies: hasown: 2.0.3 - is-data-view@1.0.2: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-typed-array: 1.1.15 - - is-date-object@1.1.0: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-directory@0.3.1: {} - - is-docker@2.2.1: {} - is-docker@3.0.0: {} - is-extglob@1.0.0: {} - is-extglob@2.1.1: {} - is-finalizationregistry@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-fullwidth-code-point@2.0.0: {} - is-fullwidth-code-point@3.0.0: {} - is-generator-fn@2.1.0: {} - - is-generator-function@1.1.2: - dependencies: - call-bound: 1.0.4 - generator-function: 2.0.1 - get-proto: 1.0.1 - has-tostringtag: 1.0.2 - safe-regex-test: 1.1.0 - - is-glob@2.0.1: - dependencies: - is-extglob: 1.0.0 - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -19427,884 +4868,102 @@ snapshots: dependencies: is-docker: 3.0.0 - is-interactive@1.0.0: {} - - is-invalid-path@0.1.0: - dependencies: - is-glob: 2.0.1 - - is-map@2.0.3: {} - is-module@1.0.0: {} - is-nan@1.3.2: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - - is-negative-zero@2.0.3: {} - - is-number-object@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - is-number@7.0.0: {} - is-path-cwd@2.2.0: {} - - is-path-inside@3.0.3: {} - is-path-inside@4.0.0: {} - is-plain-obj@2.1.0: {} - - is-plain-object@2.0.4: - dependencies: - isobject: 3.0.1 - is-reference@1.2.1: dependencies: '@types/estree': 1.0.9 - is-regex@1.2.1: - dependencies: - call-bound: 1.0.4 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - hasown: 2.0.3 - - is-set@2.0.3: {} - - is-shared-array-buffer@1.0.4: - dependencies: - call-bound: 1.0.4 - - is-stream@1.1.0: {} - is-stream@2.0.1: {} - is-string@1.1.1: - dependencies: - call-bound: 1.0.4 - has-tostringtag: 1.0.2 - - is-symbol@1.1.1: - dependencies: - call-bound: 1.0.4 - has-symbols: 1.1.0 - safe-regex-test: 1.1.0 - - is-typed-array@1.1.15: - dependencies: - which-typed-array: 1.1.20 - - is-unicode-supported@0.1.0: {} - - is-valid-path@0.1.1: - dependencies: - is-invalid-path: 0.1.0 - - is-weakmap@2.0.2: {} - - is-weakref@1.1.1: - dependencies: - call-bound: 1.0.4 - - is-weakset@2.0.4: - dependencies: - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - is-what@4.1.16: {} - is-wsl@1.1.0: {} - - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - is-wsl@3.1.1: dependencies: is-inside-container: 1.0.0 isarray@1.0.0: {} - isarray@2.0.5: {} - isexe@2.0.0: {} - isobject@3.0.1: {} - istanbul-lib-coverage@3.2.2: {} - istanbul-lib-instrument@5.2.1: - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - '@istanbuljs/schema': 0.1.6 - istanbul-lib-coverage: 3.2.2 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - istanbul-lib-instrument@6.0.3: - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - '@istanbuljs/schema': 0.1.6 - istanbul-lib-coverage: 3.2.2 - semver: 7.8.0 - transitivePeerDependencies: - - supports-color - istanbul-lib-report@3.0.1: dependencies: istanbul-lib-coverage: 3.2.2 make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@4.0.1: - dependencies: - debug: 4.4.3 - istanbul-lib-coverage: 3.2.2 - source-map: 0.6.1 - transitivePeerDependencies: - - supports-color - istanbul-reports@3.2.0: dependencies: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 - iterator.prototype@1.1.5: - dependencies: - define-data-property: 1.1.4 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - has-symbols: 1.1.0 - set-function-name: 2.0.2 - jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jest-changed-files@29.7.0: - dependencies: - execa: 5.1.1 - jest-util: 29.7.0 - p-limit: 3.1.0 - - jest-circus@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 25.8.0 - chalk: 4.1.2 - co: 4.6.0 - dedent: 1.7.2 - is-generator-fn: 2.1.0 - jest-each: 29.7.0 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - p-limit: 3.1.0 - pretty-format: 29.7.0 - pure-rand: 6.1.0 - slash: 3.0.0 - stack-utils: 2.0.6 - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-cli@29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest-cli@29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)) - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - chalk: 4.1.2 - create-jest: 29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)) - exit: 0.1.2 - import-local: 3.2.0 - jest-config: 29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)) - jest-util: 29.7.0 - jest-validate: 29.7.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest-config@29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.29.0 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.19.41 - ts-node: 10.9.2(@types/node@20.19.41)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.29.0 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 25.8.0 - ts-node: 10.9.2(@types/node@20.19.41)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.29.0 - '@jest/test-sequencer': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - chalk: 4.1.2 - ci-info: 3.9.0 - deepmerge: 4.3.1 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-circus: 29.7.0 - jest-environment-node: 29.7.0 - jest-get-type: 29.6.3 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-runner: 29.7.0 - jest-util: 29.7.0 - jest-validate: 29.7.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 29.7.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 25.8.0 - ts-node: 10.9.2(@types/node@25.8.0)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-diff@29.7.0: - dependencies: - chalk: 4.1.2 - diff-sequences: 29.6.3 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-docblock@29.7.0: - dependencies: - detect-newline: 3.1.0 - - jest-each@29.7.0: - dependencies: - '@jest/types': 29.6.3 - chalk: 4.1.2 - jest-get-type: 29.6.3 - jest-util: 29.7.0 - pretty-format: 29.7.0 - - jest-environment-node@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 25.8.0 - jest-mock: 29.7.0 - jest-util: 29.7.0 - - jest-get-type@29.6.3: {} - - jest-haste-map@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/graceful-fs': 4.1.9 - '@types/node': 25.8.0 - anymatch: 3.1.3 - fb-watchman: 2.0.2 - graceful-fs: 4.2.11 - jest-regex-util: 29.6.3 - jest-util: 29.7.0 - jest-worker: 29.7.0 - micromatch: 4.0.8 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 - - jest-leak-detector@29.7.0: - dependencies: - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-matcher-utils@29.7.0: - dependencies: - chalk: 4.1.2 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - pretty-format: 29.7.0 - - jest-message-util@29.7.0: - dependencies: - '@babel/code-frame': 7.29.0 - '@jest/types': 29.6.3 - '@types/stack-utils': 2.0.3 - chalk: 4.1.2 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - pretty-format: 29.7.0 - slash: 3.0.0 - stack-utils: 2.0.6 - - jest-mock@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 20.19.41 - jest-util: 29.7.0 - - jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): - optionalDependencies: - jest-resolve: 29.7.0 - - jest-regex-util@29.6.3: {} - - jest-resolve-dependencies@29.7.0: - dependencies: - jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 - transitivePeerDependencies: - - supports-color - - jest-resolve@29.7.0: - dependencies: - chalk: 4.1.2 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) - jest-util: 29.7.0 - jest-validate: 29.7.0 - resolve: 1.22.12 - resolve.exports: 2.0.3 - slash: 3.0.0 - - jest-runner@29.7.0: - dependencies: - '@jest/console': 29.7.0 - '@jest/environment': 29.7.0 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 25.8.0 - chalk: 4.1.2 - emittery: 0.13.1 - graceful-fs: 4.2.11 - jest-docblock: 29.7.0 - jest-environment-node: 29.7.0 - jest-haste-map: 29.7.0 - jest-leak-detector: 29.7.0 - jest-message-util: 29.7.0 - jest-resolve: 29.7.0 - jest-runtime: 29.7.0 - jest-util: 29.7.0 - jest-watcher: 29.7.0 - jest-worker: 29.7.0 - p-limit: 3.1.0 - source-map-support: 0.5.13 - transitivePeerDependencies: - - supports-color - - jest-runtime@29.7.0: - dependencies: - '@jest/environment': 29.7.0 - '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 - '@jest/source-map': 29.6.3 - '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 25.8.0 - chalk: 4.1.2 - cjs-module-lexer: 1.4.3 - collect-v8-coverage: 1.0.3 - glob: 7.2.3 - graceful-fs: 4.2.11 - jest-haste-map: 29.7.0 - jest-message-util: 29.7.0 - jest-mock: 29.7.0 - jest-regex-util: 29.6.3 - jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 - jest-util: 29.7.0 - slash: 3.0.0 - strip-bom: 4.0.0 - transitivePeerDependencies: - - supports-color - - jest-snapshot@29.7.0: - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 - '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) - chalk: 4.1.2 - expect: 29.7.0 - graceful-fs: 4.2.11 - jest-diff: 29.7.0 - jest-get-type: 29.6.3 - jest-matcher-utils: 29.7.0 - jest-message-util: 29.7.0 - jest-util: 29.7.0 - natural-compare: 1.4.0 - pretty-format: 29.7.0 - semver: 7.8.0 - transitivePeerDependencies: - - supports-color - - jest-util@29.7.0: - dependencies: - '@jest/types': 29.6.3 - '@types/node': 25.8.0 - chalk: 4.1.2 - ci-info: 3.9.0 - graceful-fs: 4.2.11 - picomatch: 2.3.2 - - jest-validate@29.7.0: - dependencies: - '@jest/types': 29.6.3 - camelcase: 6.3.0 - chalk: 4.1.2 - jest-get-type: 29.6.3 - leven: 3.1.0 - pretty-format: 29.7.0 - - jest-watcher@29.7.0: - dependencies: - '@jest/test-result': 29.7.0 - '@jest/types': 29.6.3 - '@types/node': 25.8.0 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - emittery: 0.13.1 - jest-util: 29.7.0 - string-length: 4.0.2 - - jest-worker@29.7.0: - dependencies: - '@types/node': 25.8.0 - jest-util: 29.7.0 - merge-stream: 2.0.0 - supports-color: 8.1.1 - - jest@29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jest@29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)): - dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)) - '@jest/types': 29.6.3 - import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@25.8.0)(ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - supports-color - - ts-node - - jimp-compact@0.16.1: {} - jiti@2.7.0: {} - joi@17.13.3: - dependencies: - '@hapi/hoek': 9.3.0 - '@hapi/topo': 5.1.0 - '@sideway/address': 4.1.5 - '@sideway/formula': 3.0.1 - '@sideway/pinpoint': 2.0.0 - - join-component@1.1.0: {} - - jose@4.15.9: {} - - jpeg-exif@1.1.4: {} - - js-base64@3.7.8: {} - - js-beautify@1.15.4: - dependencies: - config-chain: 1.1.13 - editorconfig: 1.0.7 - glob: 10.5.0 - js-cookie: 3.0.5 - nopt: 7.2.1 - - js-cookie@3.0.5: {} - js-tokens@10.0.0: {} js-tokens@4.0.0: {} js-tokens@9.0.1: {} - js-yaml@3.14.2: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - - js-yaml@4.1.1: - dependencies: - argparse: 2.0.1 - - jsc-android@250231.0.0: {} - - jsc-safe-url@0.2.4: {} - - jscodeshift@0.14.0(@babel/preset-env@7.29.5(@babel/core@7.29.0)): - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.3 - '@babel/plugin-proposal-class-properties': 7.18.6(@babel/core@7.29.0) - '@babel/plugin-proposal-nullish-coalescing-operator': 7.18.6(@babel/core@7.29.0) - '@babel/plugin-proposal-optional-chaining': 7.21.0(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/preset-env': 7.29.5(@babel/core@7.29.0) - '@babel/preset-flow': 7.27.1(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@babel/register': 7.29.3(@babel/core@7.29.0) - babel-core: 7.0.0-bridge.0(@babel/core@7.29.0) - chalk: 4.1.2 - flow-parser: 0.314.0 - graceful-fs: 4.2.11 - micromatch: 4.0.8 - neo-async: 2.6.2 - node-dir: 0.1.17 - recast: 0.21.5 - temp: 0.8.4 - write-file-atomic: 2.4.3 - transitivePeerDependencies: - - supports-color - - jsesc@2.5.2: {} - jsesc@3.1.0: {} - json-bigint@1.0.0: - dependencies: - bignumber.js: 9.3.1 - - json-buffer@3.0.1: {} - - json-parse-better-errors@1.0.2: {} - - json-parse-even-better-errors@2.3.1: {} - - json-schema-deref-sync@0.13.0: - dependencies: - clone: 2.1.2 - dag-map: 1.0.2 - is-valid-path: 0.1.1 - lodash: 4.18.1 - md5: 2.2.1 - memory-cache: 0.2.0 - traverse: 0.6.11 - valid-url: 1.0.9 - - json-schema-ref-resolver@3.0.0: - dependencies: - dequal: 2.0.3 - - json-schema-resolver@2.0.0: - dependencies: - debug: 4.4.3 - rfdc: 1.4.1 - uri-js: 4.4.1 - transitivePeerDependencies: - - supports-color - - json-schema-resolver@3.0.0: - dependencies: - debug: 4.4.3 - fast-uri: 3.1.2 - rfdc: 1.4.1 - transitivePeerDependencies: - - supports-color - - json-schema-traverse@0.4.1: {} - - json-schema-traverse@1.0.0: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - json5@2.2.3: {} - jsonfile@4.0.0: - optionalDependencies: - graceful-fs: 4.2.11 - - jsonfile@6.2.1: - dependencies: - universalify: 2.0.1 - optionalDependencies: - graceful-fs: 4.2.11 - - jsonwebtoken@9.0.3: - dependencies: - jws: 4.0.1 - lodash.includes: 4.3.0 - lodash.isboolean: 3.0.3 - lodash.isinteger: 4.0.4 - lodash.isnumber: 3.0.3 - lodash.isplainobject: 4.0.6 - lodash.isstring: 4.0.1 - lodash.once: 4.1.1 - ms: 2.1.3 - semver: 7.8.0 - - jsx-ast-utils@3.3.5: - dependencies: - array-includes: 3.1.9 - array.prototype.flat: 1.3.3 - object.assign: 4.1.7 - object.values: 1.2.1 - - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - jwks-rsa@3.2.2: - dependencies: - '@types/jsonwebtoken': 9.0.10 - debug: 4.4.3 - jose: 4.15.9 - limiter: 1.1.5 - lru-memoizer: 2.3.0 - transitivePeerDependencies: - - supports-color - - jws@4.0.1: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.2.1 - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - keyv@5.6.0: - dependencies: - '@keyv/serialize': 1.1.1 - - kind-of@6.0.3: {} - - kleur@3.0.3: {} - kleur@4.1.5: {} klona@2.0.6: {} knitwork@1.3.0: {} - lan-network@0.2.1: {} - lazystream@1.0.1: dependencies: readable-stream: 2.3.8 - leac@0.6.0: {} - - leven@3.1.0: {} - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - libphonenumber-js@1.13.1: {} - - libsql@0.5.29: - dependencies: - '@neon-rs/load': 0.0.4 - detect-libc: 2.0.2 - optionalDependencies: - '@libsql/darwin-arm64': 0.5.29 - '@libsql/darwin-x64': 0.5.29 - '@libsql/linux-arm-gnueabihf': 0.5.29 - '@libsql/linux-arm-musleabihf': 0.5.29 - '@libsql/linux-arm64-gnu': 0.5.29 - '@libsql/linux-arm64-musl': 0.5.29 - '@libsql/linux-x64-gnu': 0.5.29 - '@libsql/linux-x64-musl': 0.5.29 - '@libsql/win32-x64-msvc': 0.5.29 - - light-my-request@6.6.0: - dependencies: - cookie: 1.1.1 - process-warning: 4.0.1 - set-cookie-parser: 2.7.2 - - lighthouse-logger@1.4.2: - dependencies: - debug: 2.6.9 - marky: 1.3.0 - transitivePeerDependencies: - - supports-color - lightningcss-android-arm64@1.32.0: optional: true - lightningcss-darwin-arm64@1.19.0: - optional: true - lightningcss-darwin-arm64@1.32.0: optional: true - lightningcss-darwin-x64@1.19.0: - optional: true - lightningcss-darwin-x64@1.32.0: optional: true lightningcss-freebsd-x64@1.32.0: optional: true - lightningcss-linux-arm-gnueabihf@1.19.0: - optional: true - lightningcss-linux-arm-gnueabihf@1.32.0: optional: true - lightningcss-linux-arm64-gnu@1.19.0: - optional: true - lightningcss-linux-arm64-gnu@1.32.0: optional: true - lightningcss-linux-arm64-musl@1.19.0: - optional: true - lightningcss-linux-arm64-musl@1.32.0: optional: true - lightningcss-linux-x64-gnu@1.19.0: - optional: true - lightningcss-linux-x64-gnu@1.32.0: optional: true - lightningcss-linux-x64-musl@1.19.0: - optional: true - lightningcss-linux-x64-musl@1.32.0: optional: true lightningcss-win32-arm64-msvc@1.32.0: optional: true - lightningcss-win32-x64-msvc@1.19.0: - optional: true - lightningcss-win32-x64-msvc@1.32.0: optional: true - lightningcss@1.19.0: - dependencies: - detect-libc: 1.0.3 - optionalDependencies: - lightningcss-darwin-arm64: 1.19.0 - lightningcss-darwin-x64: 1.19.0 - lightningcss-linux-arm-gnueabihf: 1.19.0 - lightningcss-linux-arm64-gnu: 1.19.0 - lightningcss-linux-arm64-musl: 1.19.0 - lightningcss-linux-x64-gnu: 1.19.0 - lightningcss-linux-x64-musl: 1.19.0 - lightningcss-win32-x64-msvc: 1.19.0 - lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -20321,15 +4980,6 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 - limiter@1.1.5: {} - - linebreak@1.1.0: - dependencies: - base64-js: 0.0.8 - unicode-trie: 2.0.0 - - lines-and-columns@1.2.4: {} - listhen@1.10.0: dependencies: '@parcel/watcher': 2.5.6 @@ -20357,142 +5007,33 @@ snapshots: pkg-types: 2.3.1 quansync: 0.2.11 - locate-path@3.0.0: - dependencies: - p-locate: 3.0.0 - path-exists: 3.0.0 - - locate-path@5.0.0: - dependencies: - p-locate: 4.1.0 - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.camelcase@4.3.0: - optional: true - - lodash.clonedeep@4.5.0: {} - - lodash.debounce@4.0.8: {} - lodash.defaults@4.2.0: {} - lodash.includes@4.3.0: {} - lodash.isarguments@3.1.0: {} - lodash.isboolean@3.0.3: {} - - lodash.isinteger@4.0.4: {} - - lodash.isnumber@3.0.3: {} - - lodash.isplainobject@4.0.6: {} - - lodash.isstring@4.0.1: {} - - lodash.memoize@4.1.2: {} - - lodash.merge@4.6.2: {} - - lodash.once@4.1.1: {} - - lodash.throttle@4.1.1: {} - lodash@4.18.1: {} - log-symbols@2.2.0: - dependencies: - chalk: 2.4.2 - - log-symbols@4.1.0: - dependencies: - chalk: 4.1.2 - is-unicode-supported: 0.1.0 - - logkitty@0.7.1: - dependencies: - ansi-fragments: 0.2.1 - dayjs: 1.11.20 - yargs: 15.4.1 - - long@5.3.2: - optional: true - - loose-envify@1.4.0: - dependencies: - js-tokens: 4.0.0 - lru-cache@10.4.3: {} - lru-cache@11.3.6: {} + lru-cache@11.5.0: {} lru-cache@5.1.1: dependencies: yallist: 3.1.1 - lru-cache@6.0.0: - dependencies: - yallist: 4.0.0 - - lru-cache@7.18.3: - optional: true - - lru-memoizer@2.3.0: - dependencies: - lodash.clonedeep: 4.5.0 - lru-cache: 6.0.0 - - luxon@3.7.2: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 magicast@0.5.3: dependencies: - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 source-map-js: 1.2.1 - make-dir@2.1.0: - dependencies: - pify: 4.0.1 - semver: 5.7.2 - make-dir@4.0.0: dependencies: - semver: 7.8.0 - - make-error@1.3.6: {} - - makeerror@1.0.12: - dependencies: - tmpl: 1.0.5 - - marky@1.3.0: {} - - math-intrinsics@1.1.0: {} - - md5-file@3.2.3: - dependencies: - buffer-alloc: 1.2.0 - - md5@2.2.1: - dependencies: - charenc: 0.0.2 - crypt: 0.0.2 - is-buffer: 1.1.6 - - md5@2.3.0: - dependencies: - charenc: 0.0.2 - crypt: 0.0.2 - is-buffer: 1.1.6 - - md5hex@1.0.0: {} + semver: 7.8.1 mdast-util-to-hast@13.2.1: dependencies: @@ -20506,558 +5047,12 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 - mdn-data@2.0.14: {} - - media-typer@0.3.0: {} - - media-typer@1.1.0: {} - - memoize-one@5.2.1: {} - - memory-cache@0.2.0: {} - merge-anything@5.1.7: dependencies: is-what: 4.1.16 - merge-descriptors@1.0.3: {} - - merge-options@3.0.4: - dependencies: - is-plain-obj: 2.1.0 - - merge-stream@2.0.0: {} - merge2@1.4.1: {} - methods@1.1.2: {} - - metro-babel-transformer@0.80.12: - dependencies: - '@babel/core': 7.29.0 - flow-enums-runtime: 0.0.6 - hermes-parser: 0.23.1 - nullthrows: 1.1.1 - transitivePeerDependencies: - - supports-color - - metro-babel-transformer@0.83.7: - dependencies: - '@babel/core': 7.29.0 - flow-enums-runtime: 0.0.6 - hermes-parser: 0.35.0 - metro-cache-key: 0.83.7 - nullthrows: 1.1.1 - transitivePeerDependencies: - - supports-color - - metro-babel-transformer@0.84.4: - dependencies: - '@babel/core': 7.29.0 - flow-enums-runtime: 0.0.6 - hermes-parser: 0.35.0 - metro-cache-key: 0.84.4 - nullthrows: 1.1.1 - transitivePeerDependencies: - - supports-color - - metro-cache-key@0.80.12: - dependencies: - flow-enums-runtime: 0.0.6 - - metro-cache-key@0.83.7: - dependencies: - flow-enums-runtime: 0.0.6 - - metro-cache-key@0.84.4: - dependencies: - flow-enums-runtime: 0.0.6 - - metro-cache@0.80.12: - dependencies: - exponential-backoff: 3.1.3 - flow-enums-runtime: 0.0.6 - metro-core: 0.80.12 - - metro-cache@0.83.7: - dependencies: - exponential-backoff: 3.1.3 - flow-enums-runtime: 0.0.6 - https-proxy-agent: 7.0.6 - metro-core: 0.83.7 - transitivePeerDependencies: - - supports-color - - metro-cache@0.84.4: - dependencies: - exponential-backoff: 3.1.3 - flow-enums-runtime: 0.0.6 - https-proxy-agent: 7.0.6 - metro-core: 0.84.4 - transitivePeerDependencies: - - supports-color - - metro-config@0.80.12: - dependencies: - connect: 3.7.0 - cosmiconfig: 5.2.1 - flow-enums-runtime: 0.0.6 - jest-validate: 29.7.0 - metro: 0.80.12 - metro-cache: 0.80.12 - metro-core: 0.80.12 - metro-runtime: 0.80.12 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - metro-config@0.83.7: - dependencies: - connect: 3.7.0 - flow-enums-runtime: 0.0.6 - jest-validate: 29.7.0 - metro: 0.83.7 - metro-cache: 0.83.7 - metro-core: 0.83.7 - metro-runtime: 0.83.7 - yaml: 2.9.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - metro-config@0.84.4: - dependencies: - connect: 3.7.0 - flow-enums-runtime: 0.0.6 - jest-validate: 29.7.0 - metro: 0.84.4 - metro-cache: 0.84.4 - metro-core: 0.84.4 - metro-runtime: 0.84.4 - yaml: 2.9.0 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - metro-core@0.80.12: - dependencies: - flow-enums-runtime: 0.0.6 - lodash.throttle: 4.1.1 - metro-resolver: 0.80.12 - - metro-core@0.83.7: - dependencies: - flow-enums-runtime: 0.0.6 - lodash.throttle: 4.1.1 - metro-resolver: 0.83.7 - - metro-core@0.84.4: - dependencies: - flow-enums-runtime: 0.0.6 - lodash.throttle: 4.1.1 - metro-resolver: 0.84.4 - - metro-file-map@0.80.12: - dependencies: - anymatch: 3.1.3 - debug: 2.6.9 - fb-watchman: 2.0.2 - flow-enums-runtime: 0.0.6 - graceful-fs: 4.2.11 - invariant: 2.2.4 - jest-worker: 29.7.0 - micromatch: 4.0.8 - node-abort-controller: 3.1.1 - nullthrows: 1.1.1 - walker: 1.0.8 - optionalDependencies: - fsevents: 2.3.3 - transitivePeerDependencies: - - supports-color - - metro-file-map@0.83.7: - dependencies: - debug: 4.4.3 - fb-watchman: 2.0.2 - flow-enums-runtime: 0.0.6 - graceful-fs: 4.2.11 - invariant: 2.2.4 - jest-worker: 29.7.0 - micromatch: 4.0.8 - nullthrows: 1.1.1 - walker: 1.0.8 - transitivePeerDependencies: - - supports-color - - metro-file-map@0.84.4: - dependencies: - debug: 4.4.3 - fb-watchman: 2.0.2 - flow-enums-runtime: 0.0.6 - graceful-fs: 4.2.11 - invariant: 2.2.4 - jest-worker: 29.7.0 - micromatch: 4.0.8 - nullthrows: 1.1.1 - walker: 1.0.8 - transitivePeerDependencies: - - supports-color - - metro-minify-terser@0.80.12: - dependencies: - flow-enums-runtime: 0.0.6 - terser: 5.47.1 - - metro-minify-terser@0.83.7: - dependencies: - flow-enums-runtime: 0.0.6 - terser: 5.47.1 - - metro-minify-terser@0.84.4: - dependencies: - flow-enums-runtime: 0.0.6 - terser: 5.47.1 - - metro-resolver@0.80.12: - dependencies: - flow-enums-runtime: 0.0.6 - - metro-resolver@0.83.7: - dependencies: - flow-enums-runtime: 0.0.6 - - metro-resolver@0.84.4: - dependencies: - flow-enums-runtime: 0.0.6 - - metro-runtime@0.80.12: - dependencies: - '@babel/runtime': 7.29.2 - flow-enums-runtime: 0.0.6 - - metro-runtime@0.83.7: - dependencies: - '@babel/runtime': 7.29.2 - flow-enums-runtime: 0.0.6 - - metro-runtime@0.84.4: - dependencies: - '@babel/runtime': 7.29.2 - flow-enums-runtime: 0.0.6 - - metro-source-map@0.80.12: - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - flow-enums-runtime: 0.0.6 - invariant: 2.2.4 - metro-symbolicate: 0.80.12 - nullthrows: 1.1.1 - ob1: 0.80.12 - source-map: 0.5.7 - vlq: 1.0.1 - transitivePeerDependencies: - - supports-color - - metro-source-map@0.83.7: - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - flow-enums-runtime: 0.0.6 - invariant: 2.2.4 - metro-symbolicate: 0.83.7 - nullthrows: 1.1.1 - ob1: 0.83.7 - source-map: 0.5.7 - vlq: 1.0.1 - transitivePeerDependencies: - - supports-color - - metro-source-map@0.84.4: - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - flow-enums-runtime: 0.0.6 - invariant: 2.2.4 - metro-symbolicate: 0.84.4 - nullthrows: 1.1.1 - ob1: 0.84.4 - source-map: 0.5.7 - vlq: 1.0.1 - transitivePeerDependencies: - - supports-color - - metro-symbolicate@0.80.12: - dependencies: - flow-enums-runtime: 0.0.6 - invariant: 2.2.4 - metro-source-map: 0.80.12 - nullthrows: 1.1.1 - source-map: 0.5.7 - through2: 2.0.5 - vlq: 1.0.1 - transitivePeerDependencies: - - supports-color - - metro-symbolicate@0.83.7: - dependencies: - flow-enums-runtime: 0.0.6 - invariant: 2.2.4 - metro-source-map: 0.83.7 - nullthrows: 1.1.1 - source-map: 0.5.7 - vlq: 1.0.1 - transitivePeerDependencies: - - supports-color - - metro-symbolicate@0.84.4: - dependencies: - flow-enums-runtime: 0.0.6 - invariant: 2.2.4 - metro-source-map: 0.84.4 - nullthrows: 1.1.1 - source-map: 0.5.7 - vlq: 1.0.1 - transitivePeerDependencies: - - supports-color - - metro-transform-plugins@0.80.12: - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - flow-enums-runtime: 0.0.6 - nullthrows: 1.1.1 - transitivePeerDependencies: - - supports-color - - metro-transform-plugins@0.83.7: - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - flow-enums-runtime: 0.0.6 - nullthrows: 1.1.1 - transitivePeerDependencies: - - supports-color - - metro-transform-plugins@0.84.4: - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - flow-enums-runtime: 0.0.6 - nullthrows: 1.1.1 - transitivePeerDependencies: - - supports-color - - metro-transform-worker@0.80.12: - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - flow-enums-runtime: 0.0.6 - metro: 0.80.12 - metro-babel-transformer: 0.80.12 - metro-cache: 0.80.12 - metro-cache-key: 0.80.12 - metro-minify-terser: 0.80.12 - metro-source-map: 0.80.12 - metro-transform-plugins: 0.80.12 - nullthrows: 1.1.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - metro-transform-worker@0.83.7: - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - flow-enums-runtime: 0.0.6 - metro: 0.83.7 - metro-babel-transformer: 0.83.7 - metro-cache: 0.83.7 - metro-cache-key: 0.83.7 - metro-minify-terser: 0.83.7 - metro-source-map: 0.83.7 - metro-transform-plugins: 0.83.7 - nullthrows: 1.1.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - metro-transform-worker@0.84.4: - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/types': 7.29.0 - flow-enums-runtime: 0.0.6 - metro: 0.84.4 - metro-babel-transformer: 0.84.4 - metro-cache: 0.84.4 - metro-cache-key: 0.84.4 - metro-minify-terser: 0.84.4 - metro-source-map: 0.84.4 - metro-transform-plugins: 0.84.4 - nullthrows: 1.1.1 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - metro@0.80.12: - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - accepts: 1.3.8 - chalk: 4.1.2 - ci-info: 2.0.0 - connect: 3.7.0 - debug: 2.6.9 - denodeify: 1.2.1 - error-stack-parser: 2.1.4 - flow-enums-runtime: 0.0.6 - graceful-fs: 4.2.11 - hermes-parser: 0.23.1 - image-size: 1.2.1 - invariant: 2.2.4 - jest-worker: 29.7.0 - jsc-safe-url: 0.2.4 - lodash.throttle: 4.1.1 - metro-babel-transformer: 0.80.12 - metro-cache: 0.80.12 - metro-cache-key: 0.80.12 - metro-config: 0.80.12 - metro-core: 0.80.12 - metro-file-map: 0.80.12 - metro-resolver: 0.80.12 - metro-runtime: 0.80.12 - metro-source-map: 0.80.12 - metro-symbolicate: 0.80.12 - metro-transform-plugins: 0.80.12 - metro-transform-worker: 0.80.12 - mime-types: 2.1.35 - nullthrows: 1.1.1 - serialize-error: 2.1.0 - source-map: 0.5.7 - strip-ansi: 6.0.1 - throat: 5.0.0 - ws: 7.5.10 - yargs: 17.7.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - metro@0.83.7: - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - accepts: 2.0.0 - ci-info: 2.0.0 - connect: 3.7.0 - debug: 4.4.3 - error-stack-parser: 2.1.4 - flow-enums-runtime: 0.0.6 - graceful-fs: 4.2.11 - hermes-parser: 0.35.0 - image-size: 1.2.1 - invariant: 2.2.4 - jest-worker: 29.7.0 - jsc-safe-url: 0.2.4 - lodash.throttle: 4.1.1 - metro-babel-transformer: 0.83.7 - metro-cache: 0.83.7 - metro-cache-key: 0.83.7 - metro-config: 0.83.7 - metro-core: 0.83.7 - metro-file-map: 0.83.7 - metro-resolver: 0.83.7 - metro-runtime: 0.83.7 - metro-source-map: 0.83.7 - metro-symbolicate: 0.83.7 - metro-transform-plugins: 0.83.7 - metro-transform-worker: 0.83.7 - mime-types: 3.0.2 - nullthrows: 1.1.1 - serialize-error: 2.1.0 - source-map: 0.5.7 - throat: 5.0.0 - ws: 7.5.10 - yargs: 17.7.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - metro@0.84.4: - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.3 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - accepts: 2.0.0 - ci-info: 2.0.0 - connect: 3.7.0 - debug: 4.4.3 - error-stack-parser: 2.1.4 - flow-enums-runtime: 0.0.6 - graceful-fs: 4.2.11 - hermes-parser: 0.35.0 - image-size: 1.2.1 - invariant: 2.2.4 - jest-worker: 29.7.0 - jsc-safe-url: 0.2.4 - lodash.throttle: 4.1.1 - metro-babel-transformer: 0.84.4 - metro-cache: 0.84.4 - metro-cache-key: 0.84.4 - metro-config: 0.84.4 - metro-core: 0.84.4 - metro-file-map: 0.84.4 - metro-resolver: 0.84.4 - metro-runtime: 0.84.4 - metro-source-map: 0.84.4 - metro-symbolicate: 0.84.4 - metro-transform-plugins: 0.84.4 - metro-transform-worker: 0.84.4 - mime-types: 3.0.2 - nullthrows: 1.1.1 - serialize-error: 2.1.0 - source-map: 0.5.7 - throat: 5.0.0 - ws: 7.5.10 - yargs: 17.7.2 - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 @@ -21080,83 +5075,32 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 - mime-db@1.52.0: {} - mime-db@1.54.0: {} - mime-types@2.1.35: - dependencies: - mime-db: 1.52.0 - mime-types@3.0.2: dependencies: mime-db: 1.54.0 - mime@1.6.0: {} - - mime@2.6.0: {} - - mime@3.0.0: {} - mime@4.1.0: {} - mimic-fn@1.2.0: {} - - mimic-fn@2.1.0: {} - minimatch@10.2.5: dependencies: brace-expansion: 5.0.6 - minimatch@3.1.5: - dependencies: - brace-expansion: 1.1.14 - minimatch@5.1.9: dependencies: - brace-expansion: 2.1.0 + brace-expansion: 2.1.1 minimatch@9.0.9: dependencies: - brace-expansion: 2.1.0 - - minimist@1.2.8: {} - - minipass-collect@2.0.1: - dependencies: - minipass: 7.1.3 - - minipass-flush@1.0.7: - dependencies: - minipass: 3.3.6 - - minipass-pipeline@1.2.4: - dependencies: - minipass: 3.3.6 - - minipass@3.3.6: - dependencies: - yallist: 4.0.0 - - minipass@5.0.0: {} + brace-expansion: 2.1.1 minipass@7.1.3: {} - minizlib@2.1.2: - dependencies: - minipass: 3.3.6 - yallist: 4.0.0 - minizlib@3.1.0: dependencies: minipass: 7.1.3 - mkdirp@0.5.6: - dependencies: - minimist: 1.2.8 - - mkdirp@1.0.4: {} - mlly@1.8.2: dependencies: acorn: 8.16.0 @@ -21164,98 +5108,11 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.4 - mnemonist@0.40.0: - dependencies: - obliterator: 2.0.5 - - module-details-from-path@1.0.4: {} - - ms@2.0.0: {} - ms@2.1.3: {} - msgpackr-extract@3.0.3: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 - optional: true - - msgpackr@2.0.1: - optionalDependencies: - msgpackr-extract: 3.0.3 - - multitars@1.0.0: {} - - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - nanoid@3.3.12: {} - natural-compare@1.4.0: {} - - negotiator@0.6.3: {} - - negotiator@0.6.4: {} - - negotiator@1.0.0: {} - - neo-async@2.6.2: {} - - nested-error-stacks@2.0.1: {} - - next-auth@4.24.14(next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6): - dependencies: - '@babel/runtime': 7.29.2 - '@panva/hkdf': 1.2.1 - cookie: 0.7.2 - jose: 4.15.9 - next: 16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - oauth: 0.9.15 - openid-client: 5.7.1 - preact: 10.29.1 - preact-render-to-string: 5.2.6(preact@10.29.1) - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - uuid: 8.3.2 - - next@16.2.6(@opentelemetry/api@1.9.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): - dependencies: - '@next/env': 16.2.6 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.29 - caniuse-lite: 1.0.30001792 - postcss: 8.4.31 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - styled-jsx: 5.1.6(react@19.2.6) - optionalDependencies: - '@next/swc-darwin-arm64': 16.2.6 - '@next/swc-darwin-x64': 16.2.6 - '@next/swc-linux-arm64-gnu': 16.2.6 - '@next/swc-linux-arm64-musl': 16.2.6 - '@next/swc-linux-x64-gnu': 16.2.6 - '@next/swc-linux-x64-musl': 16.2.6 - '@next/swc-win32-arm64-msvc': 16.2.6 - '@next/swc-win32-x64-msvc': 16.2.6 - '@opentelemetry/api': 1.9.1 - babel-plugin-react-compiler: 1.0.0 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - - nice-try@1.0.5: {} - - nitropack@2.13.4(@libsql/client@0.17.3)(drizzle-orm@0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3)))(oxc-parser@0.129.0)(xml2js@0.6.0): + nitropack@2.13.4: dependencies: '@cloudflare/kv-asset-handler': 0.4.2 '@rollup/plugin-alias': 6.0.0(rollup@4.60.4) @@ -21276,7 +5133,7 @@ snapshots: cookie-es: 2.0.1 croner: 10.0.1 crossws: 0.3.5 - db0: 0.3.4(@libsql/client@0.17.3)(drizzle-orm@0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))) + db0: 0.3.4 defu: 6.1.7 destr: 2.0.5 dot-prop: 10.1.0 @@ -21310,7 +5167,7 @@ snapshots: rollup: 4.60.4 rollup-plugin-visualizer: 7.0.1(rollup@4.60.4) scule: 1.3.0 - semver: 7.8.0 + semver: 7.8.1 serve-placeholder: 2.0.2 serve-static: 2.2.1 source-map: 0.7.6 @@ -21320,15 +5177,13 @@ snapshots: uncrypto: 0.1.3 unctx: 2.5.0 unenv: 2.0.0-rc.24 - unimport: 6.3.0(oxc-parser@0.129.0) + unimport: 6.3.0 unplugin-utils: 0.3.1 - unstorage: 1.17.5(db0@0.3.4(@libsql/client@0.17.3)(drizzle-orm@0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))))(ioredis@5.10.1) + unstorage: 1.17.5(db0@0.3.4)(ioredis@5.10.1) untyped: 2.0.0 unwasm: 0.5.3 youch: 4.1.1 youch-core: 0.3.3 - optionalDependencies: - xml2js: 0.6.0 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -21361,30 +5216,8 @@ snapshots: - supports-color - uploadthing - nocache@3.0.4: {} - - node-abort-controller@3.1.1: {} - - node-addon-api@6.1.0: - optional: true - node-addon-api@7.1.1: {} - node-cache@5.1.2: - dependencies: - clone: 2.1.2 - - node-dir@0.1.17: - dependencies: - minimatch: 3.1.5 - - node-exports-info@1.6.0: - dependencies: - array.prototype.flatmap: 1.3.3 - es-errors: 1.3.0 - object.entries: 1.1.9 - semver: 6.3.1 - node-fetch-native@1.6.7: {} node-fetch@2.7.0: @@ -21393,27 +5226,11 @@ snapshots: node-forge@1.4.0: {} - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.1.2 - optional: true - - node-gyp-build@3.9.0: - optional: true - node-gyp-build@4.8.4: {} - node-int64@0.4.0: {} - node-mock-http@1.0.4: {} - node-releases@2.0.44: {} - - node-stream-zip@1.15.0: {} - - nopt@7.2.1: - dependencies: - abbrev: 2.0.0 + node-releases@2.0.46: {} nopt@8.1.0: dependencies: @@ -21421,102 +5238,6 @@ snapshots: normalize-path@3.0.0: {} - npm-package-arg@11.0.3: - dependencies: - hosted-git-info: 7.0.2 - proc-log: 4.2.0 - semver: 7.8.0 - validate-npm-package-name: 5.0.1 - - npm-package-arg@7.0.0: - dependencies: - hosted-git-info: 3.0.8 - osenv: 0.1.5 - semver: 5.7.2 - validate-npm-package-name: 3.0.0 - - npm-run-path@2.0.2: - dependencies: - path-key: 2.0.1 - - npm-run-path@4.0.1: - dependencies: - path-key: 3.1.1 - - nth-check@2.1.1: - dependencies: - boolbase: 1.0.0 - - nullthrows@1.1.1: {} - - nypm@0.6.6: - dependencies: - citty: 0.2.2 - pathe: 2.0.3 - tinyexec: 1.1.2 - - oauth@0.9.15: {} - - ob1@0.80.12: - dependencies: - flow-enums-runtime: 0.0.6 - - ob1@0.83.7: - dependencies: - flow-enums-runtime: 0.0.6 - - ob1@0.84.4: - dependencies: - flow-enums-runtime: 0.0.6 - - object-assign@4.1.1: {} - - object-hash@2.2.0: {} - - object-hash@3.0.0: - optional: true - - object-inspect@1.13.4: {} - - object-is@1.1.6: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - - object-keys@1.1.1: {} - - object.assign@4.1.7: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - has-symbols: 1.1.0 - object-keys: 1.1.1 - - object.entries@1.1.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - object.fromentries@2.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.1 - - object.values@1.2.1: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - obliterator@2.0.5: {} - obug@2.1.1: {} ofetch@1.5.1: @@ -21527,32 +5248,10 @@ snapshots: ohash@2.0.11: {} - oidc-token-hash@5.2.0: {} - - on-exit-leak-free@2.1.2: {} - - on-finished@2.3.0: - dependencies: - ee-first: 1.1.1 - on-finished@2.4.1: dependencies: ee-first: 1.1.1 - on-headers@1.1.0: {} - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - - onetime@2.0.1: - dependencies: - mimic-fn: 1.2.0 - - onetime@5.1.2: - dependencies: - mimic-fn: 2.1.0 - oniguruma-to-es@2.3.0: dependencies: emoji-regex-xs: 1.0.0 @@ -21568,176 +5267,14 @@ snapshots: powershell-utils: 0.1.0 wsl-utils: 0.3.1 - open@6.4.0: - dependencies: - is-wsl: 1.1.0 - - open@7.4.2: - dependencies: - is-docker: 2.2.1 - is-wsl: 2.2.0 - - open@8.4.2: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - - openapi-types@12.1.3: {} - - openid-client@5.7.1: - dependencies: - jose: 4.15.9 - lru-cache: 6.0.0 - object-hash: 2.2.0 - oidc-token-hash: 5.2.0 - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - ora@3.4.0: - dependencies: - chalk: 2.4.2 - cli-cursor: 2.1.0 - cli-spinners: 2.9.2 - log-symbols: 2.2.0 - strip-ansi: 5.2.0 - wcwidth: 1.0.1 - - ora@5.4.1: - dependencies: - bl: 4.1.0 - chalk: 4.1.2 - cli-cursor: 3.1.0 - cli-spinners: 2.9.2 - is-interactive: 1.0.0 - is-unicode-supported: 0.1.0 - log-symbols: 4.1.0 - strip-ansi: 6.0.1 - wcwidth: 1.0.1 - - os-homedir@1.0.2: {} - - os-tmpdir@1.0.2: {} - - osenv@0.1.5: - dependencies: - os-homedir: 1.0.2 - os-tmpdir: 1.0.2 - - own-keys@1.0.1: - dependencies: - get-intrinsic: 1.3.0 - object-keys: 1.1.1 - safe-push-apply: 1.0.0 - - oxc-parser@0.129.0: - dependencies: - '@oxc-project/types': 0.129.0 - optionalDependencies: - '@oxc-parser/binding-android-arm-eabi': 0.129.0 - '@oxc-parser/binding-android-arm64': 0.129.0 - '@oxc-parser/binding-darwin-arm64': 0.129.0 - '@oxc-parser/binding-darwin-x64': 0.129.0 - '@oxc-parser/binding-freebsd-x64': 0.129.0 - '@oxc-parser/binding-linux-arm-gnueabihf': 0.129.0 - '@oxc-parser/binding-linux-arm-musleabihf': 0.129.0 - '@oxc-parser/binding-linux-arm64-gnu': 0.129.0 - '@oxc-parser/binding-linux-arm64-musl': 0.129.0 - '@oxc-parser/binding-linux-ppc64-gnu': 0.129.0 - '@oxc-parser/binding-linux-riscv64-gnu': 0.129.0 - '@oxc-parser/binding-linux-riscv64-musl': 0.129.0 - '@oxc-parser/binding-linux-s390x-gnu': 0.129.0 - '@oxc-parser/binding-linux-x64-gnu': 0.129.0 - '@oxc-parser/binding-linux-x64-musl': 0.129.0 - '@oxc-parser/binding-openharmony-arm64': 0.129.0 - '@oxc-parser/binding-wasm32-wasi': 0.129.0 - '@oxc-parser/binding-win32-arm64-msvc': 0.129.0 - '@oxc-parser/binding-win32-ia32-msvc': 0.129.0 - '@oxc-parser/binding-win32-x64-msvc': 0.129.0 - optional: true - - p-finally@1.0.0: {} - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@3.0.0: - dependencies: - p-limit: 2.3.0 - - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - p-map@4.0.0: - dependencies: - aggregate-error: 3.1.0 - - p-try@2.2.0: {} - package-json-from-dist@1.0.1: {} - pako@0.2.9: {} - - pako@1.0.11: {} - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-json@4.0.0: - dependencies: - error-ex: 1.3.4 - json-parse-better-errors: 1.0.2 - - parse-json@5.2.0: - dependencies: - '@babel/code-frame': 7.29.0 - error-ex: 1.3.4 - json-parse-even-better-errors: 2.3.1 - lines-and-columns: 1.2.4 - - parse-png@2.1.0: - dependencies: - pngjs: 3.4.0 - parse5@7.3.0: dependencies: entities: 6.0.1 - parseley@0.12.1: - dependencies: - leac: 0.6.0 - peberminta: 0.9.0 - parseurl@1.3.3: {} - path-exists@3.0.0: {} - - path-exists@4.0.0: {} - - path-expression-matcher@1.5.0: {} - - path-is-absolute@1.0.1: {} - - path-key@2.0.1: {} - path-key@3.1.1: {} path-parse@1.0.7: {} @@ -21749,85 +5286,23 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.3.6 + lru-cache: 11.5.0 minipass: 7.1.3 - path-to-regexp@0.1.13: {} - path-to-regexp@8.4.2: {} - path-type@4.0.0: {} - pathe@1.1.2: {} pathe@2.0.3: {} - pdfkit@0.15.2: - dependencies: - crypto-js: 4.2.0 - fontkit: 1.9.0 - jpeg-exif: 1.1.4 - linebreak: 1.1.0 - png-js: 1.1.0 - - peberminta@0.9.0: {} - - perfect-debounce@1.0.0: {} - perfect-debounce@2.1.0: {} - pg-int8@1.0.1: {} - - pg-protocol@1.13.0: {} - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.1 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - picocolors@1.1.1: {} picomatch@2.3.2: {} - picomatch@3.0.2: {} - picomatch@4.0.4: {} - pify@4.0.1: {} - - pino-abstract-transport@3.0.0: - dependencies: - split2: 4.2.0 - - pino-std-serializers@7.1.0: {} - - pino@10.3.1: - dependencies: - '@pinojs/redact': 0.4.0 - atomic-sleep: 1.0.0 - on-exit-leak-free: 2.1.2 - pino-abstract-transport: 3.0.0 - pino-std-serializers: 7.1.0 - process-warning: 5.0.0 - quick-format-unescaped: 4.0.4 - real-require: 0.2.0 - safe-stable-stringify: 2.5.0 - sonic-boom: 4.2.1 - thread-stream: 4.1.0 - - pirates@4.0.7: {} - - pkg-dir@3.0.0: - dependencies: - find-up: 3.0.0 - - pkg-dir@4.2.0: - dependencies: - find-up: 4.1.0 - pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -21840,447 +5315,35 @@ snapshots: exsolve: 1.0.8 pathe: 2.0.3 - plist@3.1.1: - dependencies: - '@xmldom/xmldom': 0.9.10 - base64-js: 1.5.1 - xmlbuilder: 15.1.1 - - png-js@1.1.0: - dependencies: - browserify-zlib: 0.2.0 - - pngjs@3.4.0: {} - - possible-typed-array-names@1.1.0: {} - - postcss@8.4.31: + postcss@8.5.15: dependencies: nanoid: 3.3.12 picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.4.49: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postcss@8.5.14: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postgres-array@2.0.0: {} - - postgres-bytea@1.0.1: {} - - postgres-date@1.0.7: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - powershell-utils@0.1.0: {} - pprof-format@2.2.1: - optional: true - - preact-render-to-string@5.2.6(preact@10.29.1): - dependencies: - preact: 10.29.1 - pretty-format: 3.8.0 - - preact@10.29.1: {} - - prelude-ls@1.2.1: {} - - pretty-bytes@5.6.0: {} - pretty-bytes@7.1.0: {} - pretty-format@24.9.0: - dependencies: - '@jest/types': 24.9.0 - ansi-regex: 4.1.1 - ansi-styles: 3.2.1 - react-is: 16.13.1 - - pretty-format@26.6.2: - dependencies: - '@jest/types': 26.6.2 - ansi-regex: 5.0.1 - ansi-styles: 4.3.0 - react-is: 17.0.2 - - pretty-format@29.7.0: - dependencies: - '@jest/schemas': 29.6.3 - ansi-styles: 5.2.0 - react-is: 18.3.1 - - pretty-format@3.8.0: {} - - prisma@5.22.0: - dependencies: - '@prisma/engines': 5.22.0 - optionalDependencies: - fsevents: 2.3.3 - - prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3): - dependencies: - '@prisma/config': 6.19.3(magicast@0.5.3) - '@prisma/engines': 6.19.3 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - magicast - optional: true - - prisma@6.19.3(typescript@5.9.3): - dependencies: - '@prisma/config': 6.19.3 - '@prisma/engines': 6.19.3 - optionalDependencies: - typescript: 5.9.3 - transitivePeerDependencies: - - magicast - - proc-log@4.2.0: {} - process-nextick-args@2.0.1: {} - process-warning@4.0.1: {} - - process-warning@5.0.0: {} - process@0.11.10: {} - progress@2.0.3: {} - - promise-limit@2.7.0: {} - - promise@7.3.1: - dependencies: - asap: 2.0.6 - - promise@8.3.0: - dependencies: - asap: 2.0.6 - - prompts@2.4.2: - dependencies: - kleur: 3.0.3 - sisteransi: 1.0.5 - - prop-types@15.8.1: - dependencies: - loose-envify: 1.4.0 - object-assign: 4.1.1 - react-is: 16.13.1 - property-information@7.1.0: {} - proto-list@1.2.4: {} - - proto3-json-serializer@2.0.2: - dependencies: - protobufjs: 7.5.8 - optional: true - - protobufjs@7.5.8: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.1 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.1 - '@types/node': 25.8.0 - long: 5.3.2 - optional: true - - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - - proxy-from-env@2.1.0: {} - - pump@3.0.4: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - - punycode@2.3.1: {} - - pure-rand@6.1.0: {} - - qrcode-terminal@0.11.0: {} - - qs@6.15.1: - dependencies: - side-channel: 1.1.0 - quansync@0.2.11: {} - query-string@7.1.3: - dependencies: - decode-uri-component: 0.2.2 - filter-obj: 1.1.0 - split-on-first: 1.1.0 - strict-uri-encode: 2.0.0 - - querystring@0.2.1: {} - - querystringify@2.2.0: {} - queue-microtask@1.2.3: {} - queue@6.0.2: - dependencies: - inherits: 2.0.4 - - quick-format-unescaped@4.0.4: {} - radix3@1.1.2: {} range-parser@1.2.1: {} - rate-limiter-flexible@5.0.5: {} - - raw-body@2.5.3: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.4.24 - unpipe: 1.0.0 - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.2 - unpipe: 1.0.0 - - rc9@2.1.2: - dependencies: - defu: 6.1.7 - destr: 2.0.5 - rc9@3.0.1: dependencies: defu: 6.1.7 destr: 2.0.5 - rc@1.2.8: - dependencies: - deep-extend: 0.6.0 - ini: 1.3.8 - minimist: 1.2.8 - strip-json-comments: 2.0.1 - - react-devtools-core@5.3.2: - dependencies: - shell-quote: 1.8.3 - ws: 7.5.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - react-devtools-core@6.1.5: - dependencies: - shell-quote: 1.8.3 - ws: 7.5.10 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - react-dom@19.2.6(react@19.2.6): - dependencies: - react: 19.2.6 - scheduler: 0.27.0 - - react-freeze@1.0.4(react@18.2.0): - dependencies: - react: 18.2.0 - - react-is@16.13.1: {} - - react-is@17.0.2: {} - - react-is@18.3.1: {} - - react-native-gesture-handler@2.16.2(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0): - dependencies: - '@egjs/hammerjs': 2.0.17 - hoist-non-react-statics: 3.3.2 - invariant: 2.2.4 - lodash: 4.18.1 - prop-types: 15.8.1 - react: 18.2.0 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - - react-native-reanimated@3.10.1(@babel/core@7.29.0)(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - convert-source-map: 2.0.0 - invariant: 2.2.4 - react: 18.2.0 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - transitivePeerDependencies: - - supports-color - - react-native-safe-area-context@4.10.5(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0): - dependencies: - react: 18.2.0 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - - react-native-screens@3.31.0(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0): - dependencies: - react: 18.2.0 - react-freeze: 1.0.4(react@18.2.0) - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - warn-once: 0.1.1 - - react-native-svg@15.2.0(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0): - dependencies: - css-select: 5.2.2 - css-tree: 1.1.3 - react: 18.2.0 - react-native: 0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0) - - react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0): - dependencies: - '@jest/create-cache-key-function': 29.7.0 - '@react-native-community/cli': 13.6.9 - '@react-native-community/cli-platform-android': 13.6.9 - '@react-native-community/cli-platform-ios': 13.6.9 - '@react-native/assets-registry': 0.74.87 - '@react-native/codegen': 0.74.87(@babel/preset-env@7.29.5(@babel/core@7.29.0)) - '@react-native/community-cli-plugin': 0.74.87(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0)) - '@react-native/gradle-plugin': 0.74.87 - '@react-native/js-polyfills': 0.74.87 - '@react-native/normalize-colors': 0.74.87 - '@react-native/virtualized-lists': 0.74.87(@types/react@18.3.28)(react-native@0.74.5(@babel/core@7.29.0)(@babel/preset-env@7.29.5(@babel/core@7.29.0))(@types/react@18.3.28)(react@18.2.0))(react@18.2.0) - abort-controller: 3.0.0 - anser: 1.4.10 - ansi-regex: 5.0.1 - base64-js: 1.5.1 - chalk: 4.1.2 - event-target-shim: 5.0.1 - flow-enums-runtime: 0.0.6 - invariant: 2.2.4 - jest-environment-node: 29.7.0 - jsc-android: 250231.0.0 - memoize-one: 5.2.1 - metro-runtime: 0.80.12 - metro-source-map: 0.80.12 - mkdirp: 0.5.6 - nullthrows: 1.1.1 - pretty-format: 26.6.2 - promise: 8.3.0 - react: 18.2.0 - react-devtools-core: 5.3.2 - react-refresh: 0.14.2 - react-shallow-renderer: 16.15.0(react@18.2.0) - regenerator-runtime: 0.13.11 - scheduler: 0.24.0-canary-efb381bbf-20230505 - stacktrace-parser: 0.1.11 - whatwg-fetch: 3.6.20 - ws: 6.2.3 - yargs: 17.7.2 - optionalDependencies: - '@types/react': 18.3.28 - transitivePeerDependencies: - - '@babel/core' - - '@babel/preset-env' - - bufferutil - - encoding - - supports-color - - utf-8-validate - - react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6): - dependencies: - '@react-native/assets-registry': 0.85.3 - '@react-native/codegen': 0.85.3(@babel/core@7.29.0) - '@react-native/community-cli-plugin': 0.85.3(@react-native-community/cli@13.6.9) - '@react-native/gradle-plugin': 0.85.3 - '@react-native/js-polyfills': 0.85.3 - '@react-native/normalize-colors': 0.85.3 - '@react-native/virtualized-lists': 0.85.3(@types/react@18.3.28)(react-native@0.85.3(@babel/core@7.29.0)(@react-native-community/cli@13.6.9)(@types/react@18.3.28)(react@19.2.6))(react@19.2.6) - abort-controller: 3.0.0 - anser: 1.4.10 - ansi-regex: 5.0.1 - babel-plugin-syntax-hermes-parser: 0.33.3 - base64-js: 1.5.1 - commander: 12.1.0 - flow-enums-runtime: 0.0.6 - hermes-compiler: 250829098.0.10 - invariant: 2.2.4 - memoize-one: 5.2.1 - metro-runtime: 0.84.4 - metro-source-map: 0.84.4 - nullthrows: 1.1.1 - pretty-format: 29.7.0 - promise: 8.3.0 - react: 19.2.6 - react-devtools-core: 6.1.5 - react-refresh: 0.14.2 - regenerator-runtime: 0.13.11 - scheduler: 0.27.0 - semver: 7.8.0 - stacktrace-parser: 0.1.11 - tinyglobby: 0.2.16 - whatwg-fetch: 3.6.20 - ws: 7.5.10 - yargs: 17.7.2 - optionalDependencies: - '@types/react': 18.3.28 - transitivePeerDependencies: - - '@babel/core' - - '@react-native-community/cli' - - '@react-native/metro-config' - - bufferutil - - supports-color - - utf-8-validate - - react-promise-suspense@0.3.4: - dependencies: - fast-deep-equal: 2.0.1 - - react-refresh@0.14.2: {} - - react-shallow-renderer@16.15.0(react@18.2.0): - dependencies: - object-assign: 4.1.1 - react: 18.2.0 - react-is: 18.3.1 - - react-test-renderer@18.2.0(react@18.2.0): - dependencies: - react: 18.2.0 - react-is: 18.3.1 - react-shallow-renderer: 16.15.0(react@18.2.0) - scheduler: 0.23.2 - - react@18.2.0: - dependencies: - loose-envify: 1.4.0 - - react@19.2.6: {} - readable-stream@2.3.8: dependencies: core-util-is: 1.0.3 @@ -22291,12 +5354,6 @@ snapshots: string_decoder: 1.1.1 util-deprecate: 1.0.2 - readable-stream@3.6.2: - dependencies: - inherits: 2.0.4 - string_decoder: 1.3.0 - util-deprecate: 1.0.2 - readable-stream@4.7.0: dependencies: abort-controller: 3.0.0 @@ -22309,48 +5366,14 @@ snapshots: dependencies: minimatch: 5.1.9 - readdirp@4.1.2: {} - readdirp@5.0.0: {} - readline@1.3.0: {} - - real-require@0.2.0: {} - - real-require@1.0.0: {} - - recast@0.21.5: - dependencies: - ast-types: 0.15.2 - esprima: 4.0.1 - source-map: 0.6.1 - tslib: 2.8.1 - redis-errors@1.2.0: {} redis-parser@3.0.0: dependencies: redis-errors: 1.2.0 - reflect.getprototypeof@1.0.10: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - get-proto: 1.0.1 - which-builtin-type: 1.2.1 - - regenerate-unicode-properties@10.2.2: - dependencies: - regenerate: 1.4.2 - - regenerate@1.4.2: {} - - regenerator-runtime@0.13.11: {} - regex-recursion@5.1.1: dependencies: regex: 5.1.1 @@ -22362,77 +5385,8 @@ snapshots: dependencies: regex-utilities: 2.3.0 - regexp.prototype.flags@1.5.4: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-errors: 1.3.0 - get-proto: 1.0.1 - gopd: 1.2.0 - set-function-name: 2.0.2 - - regexpu-core@6.4.0: - dependencies: - regenerate: 1.4.2 - regenerate-unicode-properties: 10.2.2 - regjsgen: 0.8.0 - regjsparser: 0.13.1 - unicode-match-property-ecmascript: 2.0.0 - unicode-match-property-value-ecmascript: 2.2.1 - - regjsgen@0.8.0: {} - - regjsparser@0.13.1: - dependencies: - jsesc: 3.1.0 - - remove-trailing-slash@0.1.1: {} - - require-directory@2.1.1: {} - - require-from-string@2.0.2: {} - - require-in-the-middle@7.5.2: - dependencies: - debug: 4.4.3 - module-details-from-path: 1.0.4 - resolve: 1.22.12 - transitivePeerDependencies: - - supports-color - - require-main-filename@2.0.0: {} - - requireg@0.2.2: - dependencies: - nested-error-stacks: 2.0.1 - rc: 1.2.8 - resolve: 1.7.1 - - requires-port@1.0.0: {} - - resend@3.5.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): - dependencies: - '@react-email/render': 0.0.16(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - transitivePeerDependencies: - - react - - react-dom - - resolve-cwd@3.0.0: - dependencies: - resolve-from: 5.0.0 - - resolve-from@3.0.0: {} - - resolve-from@4.0.0: {} - resolve-from@5.0.0: {} - resolve-pkg-maps@1.0.0: {} - - resolve-workspace-root@2.0.1: {} - - resolve.exports@2.0.3: {} - resolve@1.22.12: dependencies: es-errors: 1.3.0 @@ -22440,58 +5394,8 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - resolve@1.7.1: - dependencies: - path-parse: 1.0.7 - - resolve@2.0.0-next.7: - dependencies: - es-errors: 1.3.0 - is-core-module: 2.16.2 - node-exports-info: 1.6.0 - object-keys: 1.1.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - restore-cursor@2.0.0: - dependencies: - onetime: 2.0.1 - signal-exit: 3.0.7 - - restore-cursor@3.1.0: - dependencies: - onetime: 5.1.2 - signal-exit: 3.0.7 - - restructure@2.0.1: {} - - ret@0.5.0: {} - - retry-request@7.0.2: - dependencies: - '@types/request': 2.48.13 - extend: 3.0.2 - teeny-request: 9.0.0 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - - retry@0.13.1: - optional: true - reusify@1.1.0: {} - rfdc@1.4.1: {} - - rimraf@2.6.3: - dependencies: - glob: 7.2.3 - - rimraf@3.0.2: - dependencies: - glob: 7.2.3 - rollup-plugin-visualizer@7.0.1(rollup@4.60.4): dependencies: open: 11.0.0 @@ -22540,111 +5444,15 @@ snapshots: dependencies: queue-microtask: 1.2.3 - safe-array-concat@1.1.4: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - get-intrinsic: 1.3.0 - has-symbols: 1.1.0 - isarray: 2.0.5 - safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} - safe-push-apply@1.0.0: - dependencies: - es-errors: 1.3.0 - isarray: 2.0.5 - - safe-regex-test@1.1.0: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-regex: 1.2.1 - - safe-regex2@5.1.1: - dependencies: - ret: 0.5.0 - - safe-stable-stringify@2.5.0: {} - - safer-buffer@2.1.2: {} - - sax@1.6.0: {} - - scheduler@0.23.2: - dependencies: - loose-envify: 1.4.0 - - scheduler@0.24.0-canary-efb381bbf-20230505: - dependencies: - loose-envify: 1.4.0 - - scheduler@0.27.0: {} - - scmp@2.1.0: {} - scule@1.3.0: {} - secure-json-parse@2.7.0: {} - - secure-json-parse@4.1.0: {} - - selderee@0.11.0: - dependencies: - parseley: 0.12.1 - - selfsigned@2.4.1: - dependencies: - '@types/node-forge': 1.3.14 - node-forge: 1.4.0 - - semver@5.7.2: {} - semver@6.3.1: {} - semver@7.5.3: - dependencies: - lru-cache: 6.0.0 - - semver@7.8.0: {} - - send@0.18.0: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 1.0.2 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.0 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.1 - transitivePeerDependencies: - - supports-color - - send@0.19.2: - dependencies: - debug: 2.6.9 - depd: 2.0.0 - destroy: 1.2.0 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 0.5.2 - http-errors: 2.0.1 - mime: 1.6.0 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color + semver@7.8.1: {} send@1.2.1: dependencies: @@ -22662,8 +5470,6 @@ snapshots: transitivePeerDependencies: - supports-color - serialize-error@2.1.0: {} - serialize-javascript@7.0.5: {} seroval-plugins@1.5.4(seroval@1.5.4): @@ -22676,15 +5482,6 @@ snapshots: dependencies: defu: 6.1.7 - serve-static@1.16.3: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 0.19.2 - transitivePeerDependencies: - - supports-color - serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -22694,86 +5491,14 @@ snapshots: transitivePeerDependencies: - supports-color - set-blocking@2.0.0: {} - - set-cookie-parser@2.7.2: {} - - set-function-length@1.2.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - function-bind: 1.1.2 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-property-descriptors: 1.0.2 - - set-function-name@2.0.2: - dependencies: - define-data-property: 1.1.4 - es-errors: 1.3.0 - functions-have-names: 1.2.3 - has-property-descriptors: 1.0.2 - - set-proto@1.0.0: - dependencies: - dunder-proto: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - - setimmediate@1.0.5: {} - setprototypeof@1.2.0: {} - shallow-clone@3.0.1: - dependencies: - kind-of: 6.0.3 - - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.8.0 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - optional: true - - shebang-command@1.2.0: - dependencies: - shebang-regex: 1.0.0 - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 - shebang-regex@1.0.0: {} - shebang-regex@3.0.0: {} - shell-quote@1.8.3: {} - shiki@1.29.2: dependencies: '@shikijs/core': 1.29.2 @@ -22785,165 +5510,58 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 - shimmer@1.2.1: {} - - side-channel-list@1.0.1: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.1 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - siginfo@2.0.0: {} - signal-exit@3.0.7: {} - signal-exit@4.1.0: {} - simple-plist@1.3.1: - dependencies: - bplist-creator: 0.1.0 - bplist-parser: 0.3.1 - plist: 3.1.1 - - simple-swizzle@0.2.4: - dependencies: - is-arrayish: 0.3.4 - - sisteransi@1.0.5: {} - - slash@3.0.0: {} - slash@5.1.0: {} - slice-ansi@2.1.0: - dependencies: - ansi-styles: 3.2.1 - astral-regex: 1.0.0 - is-fullwidth-code-point: 2.0.0 - - slugify@1.6.9: {} - smob@1.6.2: {} - solid-js@1.9.12: + solid-js@1.9.13: dependencies: csstype: 3.2.3 seroval: 1.5.4 seroval-plugins: 1.5.4(seroval@1.5.4) - solid-refresh@0.6.3(solid-js@1.9.12): + solid-refresh@0.6.3(solid-js@1.9.13): dependencies: - '@babel/generator': 7.29.1 - '@babel/helper-module-imports': 7.28.6 - '@babel/types': 7.29.0 - solid-js: 1.9.12 + '@babel/generator': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/types': 7.29.7 + solid-js: 1.9.13 transitivePeerDependencies: - supports-color - solid-use@0.9.1(solid-js@1.9.12): + solid-use@0.9.1(solid-js@1.9.13): dependencies: - solid-js: 1.9.12 - - sonic-boom@4.2.1: - dependencies: - atomic-sleep: 1.0.0 + solid-js: 1.9.13 source-map-js@1.2.1: {} - source-map-support@0.5.13: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - source-map-support@0.5.21: dependencies: buffer-from: 1.1.2 source-map: 0.6.1 - source-map@0.5.7: {} - source-map@0.6.1: {} source-map@0.7.6: {} space-separated-tokens@2.0.2: {} - spark-md5@3.0.2: - optional: true - - split-on-first@1.1.0: {} - - split2@4.2.0: {} - - sprintf-js@1.0.3: {} - srvx@0.9.8: {} - ssri@10.0.6: - dependencies: - minipass: 7.1.3 - - stack-utils@2.0.6: - dependencies: - escape-string-regexp: 2.0.0 - stackback@0.0.2: {} stackframe@1.3.4: {} - stacktrace-parser@0.1.11: - dependencies: - type-fest: 0.7.1 - standard-as-callback@2.1.0: {} - statuses@1.5.0: {} - - statuses@2.0.1: {} - statuses@2.0.2: {} std-env@4.1.0: {} - stop-iteration-iterator@1.1.0: - dependencies: - es-errors: 1.3.0 - internal-slot: 1.1.0 - - stream-buffers@2.2.0: {} - - stream-events@1.0.5: - dependencies: - stubs: 3.0.0 - optional: true - - stream-shift@1.0.3: - optional: true - - stream-wormhole@1.1.0: {} - streamx@2.25.0: dependencies: events-universal: 1.0.1 @@ -22953,13 +5571,6 @@ snapshots: - bare-abort-controller - react-native-b4a - strict-uri-encode@2.0.0: {} - - string-length@4.0.2: - dependencies: - char-regex: 1.0.2 - strip-ansi: 6.0.1 - string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -22978,50 +5589,6 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 - string.prototype.matchall@4.0.12: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - get-intrinsic: 1.3.0 - gopd: 1.2.0 - has-symbols: 1.1.0 - internal-slot: 1.1.0 - regexp.prototype.flags: 1.5.4 - set-function-name: 2.0.2 - side-channel: 1.1.0 - - string.prototype.repeat@1.0.0: - dependencies: - define-properties: 1.2.1 - es-abstract: 1.24.2 - - string.prototype.trim@1.2.10: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-data-property: 1.1.4 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-object-atoms: 1.1.1 - has-property-descriptors: 1.0.2 - - string.prototype.trimend@1.0.9: - dependencies: - call-bind: 1.0.9 - call-bound: 1.0.4 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - - string.prototype.trimstart@1.0.8: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-object-atoms: 1.1.1 - string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -23035,10 +5602,6 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - strip-ansi@5.2.0: - dependencies: - ansi-regex: 4.1.1 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -23047,84 +5610,24 @@ snapshots: dependencies: ansi-regex: 6.2.2 - strip-bom@4.0.0: {} - - strip-eof@1.0.0: {} - - strip-final-newline@2.0.0: {} - - strip-json-comments@2.0.1: {} - - strip-json-comments@3.1.1: {} - strip-literal@3.1.0: dependencies: js-tokens: 9.0.1 - stripe@14.25.0: - dependencies: - '@types/node': 25.8.0 - qs: 6.15.1 - - strnum@1.1.2: {} - - strnum@2.3.0: {} - - structured-headers@0.4.1: {} - - stubs@3.0.0: - optional: true - - styled-jsx@5.1.6(react@19.2.6): - dependencies: - client-only: 0.0.1 - react: 19.2.6 - - sucrase@3.34.0: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - glob: 7.1.6 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - ts-interface-checker: 0.1.13 - - sucrase@3.35.1: - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - commander: 4.1.1 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.7 - tinyglobby: 0.2.16 - ts-interface-checker: 0.1.13 - - sudo-prompt@9.2.1: {} - supports-color@10.2.2: {} - supports-color@5.5.0: - dependencies: - has-flag: 3.0.0 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - supports-hyperlinks@2.3.0: - dependencies: - has-flag: 4.0.0 - supports-color: 7.2.0 - supports-preserve-symlinks-flag@1.0.0: {} tagged-tag@1.0.0: {} + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + tar-stream@3.2.0: dependencies: b4a: 1.8.1 @@ -23136,15 +5639,6 @@ snapshots: - bare-buffer - react-native-b4a - tar@6.2.1: - dependencies: - chownr: 2.0.0 - fs-minipass: 2.1.0 - minipass: 5.0.0 - minizlib: 2.1.2 - mkdirp: 1.0.4 - yallist: 4.0.0 - tar@7.5.15: dependencies: '@isaacs/fs-minipass': 4.0.1 @@ -23153,18 +5647,6 @@ snapshots: minizlib: 3.1.0 yallist: 5.0.0 - teeny-request@9.0.0: - dependencies: - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 - node-fetch: 2.7.0 - stream-events: 1.0.5 - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - teex@1.0.1: dependencies: streamx: 2.25.0 @@ -23172,89 +5654,31 @@ snapshots: - bare-abort-controller - react-native-b4a - temp-dir@1.0.0: {} - - temp-dir@2.0.0: {} - - temp@0.8.4: + terracotta@1.1.0(solid-js@1.9.13): dependencies: - rimraf: 2.6.3 + solid-js: 1.9.13 + solid-use: 0.9.1(solid-js@1.9.13) - tempy@0.3.0: - dependencies: - temp-dir: 1.0.0 - type-fest: 0.3.1 - unique-string: 1.0.0 - - tempy@0.7.1: - dependencies: - del: 6.1.1 - is-stream: 2.0.1 - temp-dir: 2.0.0 - type-fest: 0.16.0 - unique-string: 2.0.0 - - terminal-link@2.1.1: - dependencies: - ansi-escapes: 4.3.2 - supports-hyperlinks: 2.3.0 - - terracotta@1.1.0(solid-js@1.9.12): - dependencies: - solid-js: 1.9.12 - solid-use: 0.9.1(solid-js@1.9.12) - - terser@5.47.1: + terser@5.48.0: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.16.0 commander: 2.20.3 source-map-support: 0.5.21 - test-exclude@6.0.0: - dependencies: - '@istanbuljs/schema': 0.1.6 - glob: 7.2.3 - minimatch: 3.1.5 - text-decoder@1.2.7: dependencies: b4a: 1.8.1 transitivePeerDependencies: - react-native-b4a - text-decoding@1.0.0: {} - - text-table@0.2.0: {} - - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - - thread-stream@4.1.0: - dependencies: - real-require: 1.0.0 - - throat@5.0.0: {} - - through2@2.0.5: - dependencies: - readable-stream: 2.3.8 - xtend: 4.0.2 - - tiny-inflate@1.0.3: {} - tiny-invariant@1.3.3: {} tinybench@2.9.0: {} tinyclip@0.1.12: {} - tinyexec@1.1.2: {} + tinyexec@1.2.2: {} tinyglobby@0.2.16: dependencies: @@ -23263,217 +5687,35 @@ snapshots: tinyrainbow@3.1.0: {} - tmpl@1.0.5: {} - to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - toad-cache@3.7.0: {} - toidentifier@1.0.1: {} - toqr@0.1.1: {} - tr46@0.0.3: {} - traverse@0.6.11: - dependencies: - gopd: 1.2.0 - typedarray.prototype.slice: 1.0.5 - which-typed-array: 1.1.20 - trim-lines@3.0.1: {} - trim-right@1.0.1: {} - - ts-interface-checker@0.1.13: {} - - ts-jest@29.4.9(@babel/core@7.29.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.0))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)))(typescript@5.9.3): - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - handlebars: 4.7.9 - jest: 29.7.0(@types/node@20.19.41)(ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3)) - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.8.0 - type-fest: 4.41.0 - typescript: 5.9.3 - yargs-parser: 21.1.1 + turbo@2.9.14: optionalDependencies: - '@babel/core': 7.29.0 - '@jest/transform': 29.7.0 - '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) - jest-util: 29.7.0 - - ts-node@10.9.2(@types/node@20.19.41)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 20.19.41 - acorn: 8.16.0 - acorn-walk: 8.3.5 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.4 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - - ts-node@10.9.2(@types/node@25.8.0)(typescript@5.9.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 25.8.0 - acorn: 8.16.0 - acorn-walk: 8.3.5 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.4 - make-error: 1.3.6 - typescript: 5.9.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optional: true - - tslib@2.8.1: {} - - tsx@4.22.0: - dependencies: - esbuild: 0.28.0 - optionalDependencies: - fsevents: 2.3.3 - - turbo@2.9.12: - optionalDependencies: - '@turbo/darwin-64': 2.9.12 - '@turbo/darwin-arm64': 2.9.12 - '@turbo/linux-64': 2.9.12 - '@turbo/linux-arm64': 2.9.12 - '@turbo/windows-64': 2.9.12 - '@turbo/windows-arm64': 2.9.12 - - twilio@4.23.0: - dependencies: - axios: 1.16.1 - dayjs: 1.11.20 - https-proxy-agent: 5.0.1 - jsonwebtoken: 9.0.3 - qs: 6.15.1 - scmp: 2.1.0 - url-parse: 1.5.10 - xmlbuilder: 13.0.2 - transitivePeerDependencies: - - debug - - supports-color - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - type-detect@4.0.8: {} - - type-fest@0.16.0: {} - - type-fest@0.20.2: {} - - type-fest@0.21.3: {} - - type-fest@0.3.1: {} - - type-fest@0.7.1: {} - - type-fest@4.41.0: {} + '@turbo/darwin-64': 2.9.14 + '@turbo/darwin-arm64': 2.9.14 + '@turbo/linux-64': 2.9.14 + '@turbo/linux-arm64': 2.9.14 + '@turbo/windows-64': 2.9.14 + '@turbo/windows-arm64': 2.9.14 type-fest@5.6.0: dependencies: tagged-tag: 1.0.0 - type-is@1.6.18: - dependencies: - media-typer: 0.3.0 - mime-types: 2.1.35 - - type-is@2.1.0: - dependencies: - content-type: 2.0.0 - media-typer: 1.1.0 - mime-types: 3.0.2 - - typed-array-buffer@1.0.3: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - is-typed-array: 1.1.15 - - typed-array-byte-length@1.0.3: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - - typed-array-byte-offset@1.0.4: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - has-proto: 1.2.0 - is-typed-array: 1.1.15 - reflect.getprototypeof: 1.0.10 - - typed-array-length@1.0.7: - dependencies: - call-bind: 1.0.9 - for-each: 0.3.5 - gopd: 1.2.0 - is-typed-array: 1.1.15 - possible-typed-array-names: 1.1.0 - reflect.getprototypeof: 1.0.10 - - typedarray.prototype.slice@1.0.5: - dependencies: - call-bind: 1.0.9 - define-properties: 1.2.1 - es-abstract: 1.24.2 - es-errors: 1.3.0 - get-proto: 1.0.1 - math-intrinsics: 1.1.0 - typed-array-buffer: 1.0.3 - typed-array-byte-offset: 1.0.4 - typescript@5.9.3: {} - ua-parser-js@0.7.41: {} - - ua-parser-js@1.0.41: {} - ufo@1.6.4: {} - uglify-js@3.19.3: - optional: true - ultrahtml@1.6.0: {} - unbox-primitive@1.1.0: - dependencies: - call-bound: 1.0.4 - has-bigints: 1.1.0 - has-symbols: 1.1.0 - which-boxed-primitive: 1.1.1 - uncrypto@0.1.3: {} unctx@2.5.0: @@ -23483,40 +5725,15 @@ snapshots: magic-string: 0.30.21 unplugin: 2.3.11 - undici-types@5.26.5: {} - - undici-types@6.21.0: {} - undici-types@7.24.6: {} unenv@2.0.0-rc.24: dependencies: pathe: 2.0.3 - unicode-canonical-property-names-ecmascript@2.0.1: {} - - unicode-match-property-ecmascript@2.0.0: - dependencies: - unicode-canonical-property-names-ecmascript: 2.0.1 - unicode-property-aliases-ecmascript: 2.2.0 - - unicode-match-property-value-ecmascript@2.2.1: {} - - unicode-properties@1.4.1: - dependencies: - base64-js: 1.5.1 - unicode-trie: 2.0.0 - - unicode-property-aliases-ecmascript@2.2.0: {} - - unicode-trie@2.0.0: - dependencies: - pako: 0.2.9 - tiny-inflate: 1.0.3 - unicorn-magic@0.4.0: {} - unimport@6.3.0(oxc-parser@0.129.0): + unimport@6.3.0: dependencies: acorn: 8.16.0 escape-string-regexp: 5.0.0 @@ -23532,24 +5749,6 @@ snapshots: tinyglobby: 0.2.16 unplugin: 3.0.0 unplugin-utils: 0.3.1 - optionalDependencies: - oxc-parser: 0.129.0 - - unique-filename@3.0.0: - dependencies: - unique-slug: 4.0.0 - - unique-slug@4.0.0: - dependencies: - imurmurhash: 0.1.4 - - unique-string@1.0.0: - dependencies: - crypto-random-string: 1.0.0 - - unique-string@2.0.0: - dependencies: - crypto-random-string: 2.0.0 unist-util-is@6.0.1: dependencies: @@ -23574,14 +5773,6 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - universalify@0.1.2: {} - - universalify@1.0.0: {} - - universalify@2.0.1: {} - - unpipe@1.0.0: {} - unplugin-utils@0.3.1: dependencies: pathe: 2.0.3 @@ -23600,18 +5791,18 @@ snapshots: picomatch: 4.0.4 webpack-virtual-modules: 0.6.2 - unstorage@1.17.5(db0@0.3.4(@libsql/client@0.17.3)(drizzle-orm@0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))))(ioredis@5.10.1): + unstorage@1.17.5(db0@0.3.4)(ioredis@5.10.1): dependencies: anymatch: 3.1.3 chokidar: 5.0.0 destr: 2.0.5 h3: 1.15.11 - lru-cache: 11.3.6 + lru-cache: 11.5.0 node-fetch-native: 1.6.7 ofetch: 1.5.1 ufo: 1.6.4 optionalDependencies: - db0: 0.3.4(@libsql/client@0.17.3)(drizzle-orm@0.45.2(@libsql/client@0.17.3)(@opentelemetry/api@1.9.1)(@prisma/client@6.19.3(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))(typescript@5.9.3))(@types/pg@8.6.1)(prisma@6.19.3(magicast@0.5.3)(typescript@5.9.3))) + db0: 0.3.4 ioredis: 5.10.1 untun@0.1.3: @@ -23645,71 +5836,10 @@ snapshots: uqr@0.1.3: {} - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - url-join@4.0.0: {} - - url-parse@1.5.10: - dependencies: - querystringify: 2.2.0 - requires-port: 1.0.0 - - url-template@2.0.8: {} - - use-latest-callback@0.2.6(react@18.2.0): - dependencies: - react: 18.2.0 - - use-sync-external-store@1.6.0(react@18.2.0): - dependencies: - react: 18.2.0 - util-deprecate@1.0.2: {} - util@0.12.5: - dependencies: - inherits: 2.0.4 - is-arguments: 1.2.0 - is-generator-function: 1.1.2 - is-typed-array: 1.1.15 - which-typed-array: 1.1.20 - - utils-merge@1.0.1: {} - - uuid@10.0.0: {} - - uuid@11.1.1: {} - - uuid@14.0.0: {} - - uuid@7.0.3: {} - - uuid@8.3.2: {} - - uuid@9.0.1: {} - - v8-compile-cache-lib@3.0.1: {} - - v8-to-istanbul@9.3.0: - dependencies: - '@jridgewell/trace-mapping': 0.3.31 - '@types/istanbul-lib-coverage': 2.0.6 - convert-source-map: 2.0.0 - valibot@0.29.0: {} - valid-url@1.0.9: {} - - validate-npm-package-name@3.0.0: - dependencies: - builtins: 1.0.3 - - validate-npm-package-name@5.0.1: {} - - vary@1.1.2: {} - vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -23720,77 +5850,47 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-solid@2.11.12(solid-js@1.9.12)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)): + vite-plugin-solid@2.11.12(solid-js@1.9.13)(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.7 '@types/babel__core': 7.20.5 - babel-preset-solid: 1.9.12(@babel/core@7.29.0)(solid-js@1.9.12) + babel-preset-solid: 1.9.12(@babel/core@7.29.7)(solid-js@1.9.13) merge-anything: 5.1.7 - solid-js: 1.9.12 - solid-refresh: 0.6.3(solid-js@1.9.12) - vite: 5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1) - vitefu: 1.1.3(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) + solid-js: 1.9.13 + solid-refresh: 0.6.3(solid-js@1.9.13) + vite: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + vitefu: 1.1.3(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) transitivePeerDependencies: - supports-color - vite-plugin-solid@2.11.12(solid-js@1.9.12)(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0)): - dependencies: - '@babel/core': 7.29.0 - '@types/babel__core': 7.20.5 - babel-preset-solid: 1.9.12(@babel/core@7.29.0)(solid-js@1.9.12) - merge-anything: 5.1.7 - solid-js: 1.9.12 - solid-refresh: 0.6.3(solid-js@1.9.12) - vite: 7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0) - vitefu: 1.1.3(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0)) - transitivePeerDependencies: - - supports-color - - vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1): - dependencies: - esbuild: 0.21.5 - postcss: 8.5.14 - rollup: 4.60.4 - optionalDependencies: - '@types/node': 25.8.0 - fsevents: 2.3.3 - lightningcss: 1.32.0 - terser: 5.47.1 - - vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0): + vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 - postcss: 8.5.14 + postcss: 8.5.15 rollup: 4.60.4 tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.8.0 + '@types/node': 25.9.1 fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.32.0 - terser: 5.47.1 - tsx: 4.22.0 - yaml: 2.9.0 + terser: 5.48.0 - vitefu@1.1.3(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)): + vitefu@1.1.3(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)): optionalDependencies: - vite: 5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1) + vite: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) - vitefu@1.1.3(vite@7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0)): - optionalDependencies: - vite: 7.3.3(@types/node@25.8.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(tsx@4.22.0)(yaml@2.9.0) - - vitest@4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.8.0)(@vitest/coverage-v8@4.1.6)(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)): + vitest@4.1.7(@types/node@25.9.1)(@vitest/coverage-v8@4.1.7)(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)): dependencies: - '@vitest/expect': 4.1.6 - '@vitest/mocker': 4.1.6(vite@5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1)) - '@vitest/pretty-format': 4.1.6 - '@vitest/runner': 4.1.6 - '@vitest/snapshot': 4.1.6 - '@vitest/spy': 4.1.6 - '@vitest/utils': 4.1.6 + '@vitest/expect': 4.1.7 + '@vitest/mocker': 4.1.7(vite@7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + '@vitest/pretty-format': 4.1.7 + '@vitest/runner': 4.1.7 + '@vitest/snapshot': 4.1.7 + '@vitest/spy': 4.1.7 + '@vitest/utils': 4.1.7 es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 @@ -23799,106 +5899,26 @@ snapshots: picomatch: 4.0.4 std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.1.2 + tinyexec: 1.2.2 tinyglobby: 0.2.16 tinyrainbow: 3.1.0 - vite: 5.4.21(@types/node@25.8.0)(lightningcss@1.32.0)(terser@5.47.1) + vite: 7.3.3(@types/node@25.9.1)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@types/node': 25.8.0 - '@vitest/coverage-v8': 4.1.6(vitest@4.1.6) + '@types/node': 25.9.1 + '@vitest/coverage-v8': 4.1.7(vitest@4.1.7) transitivePeerDependencies: - msw - vlq@1.0.1: {} - - walker@1.0.8: - dependencies: - makeerror: 1.0.12 - - warn-once@0.1.1: {} - - wcwidth@1.0.1: - dependencies: - defaults: 1.0.4 - webidl-conversions@3.0.1: {} - webidl-conversions@5.0.0: {} - webpack-virtual-modules@0.6.2: {} - websocket-driver@0.7.4: - dependencies: - http-parser-js: 0.5.10 - safe-buffer: 5.2.1 - websocket-extensions: 0.1.4 - - websocket-extensions@0.1.4: {} - - whatwg-fetch@3.6.20: {} - - whatwg-url-minimum@0.1.2: {} - - whatwg-url-without-unicode@8.0.0-3: - dependencies: - buffer: 5.7.1 - punycode: 2.3.1 - webidl-conversions: 5.0.0 - whatwg-url@5.0.0: dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 - which-boxed-primitive@1.1.1: - dependencies: - is-bigint: 1.1.0 - is-boolean-object: 1.2.2 - is-number-object: 1.1.1 - is-string: 1.1.1 - is-symbol: 1.1.1 - - which-builtin-type@1.2.1: - dependencies: - call-bound: 1.0.4 - function.prototype.name: 1.1.8 - has-tostringtag: 1.0.2 - is-async-function: 2.1.1 - is-date-object: 1.1.0 - is-finalizationregistry: 1.1.1 - is-generator-function: 1.1.2 - is-regex: 1.2.1 - is-weakref: 1.1.1 - isarray: 2.0.5 - which-boxed-primitive: 1.1.1 - which-collection: 1.0.2 - which-typed-array: 1.1.20 - - which-collection@1.0.2: - dependencies: - is-map: 2.0.3 - is-set: 2.0.3 - is-weakmap: 2.0.2 - is-weakset: 2.0.4 - - which-module@2.0.1: {} - - which-typed-array@1.1.20: - dependencies: - available-typed-arrays: 1.0.7 - call-bind: 1.0.9 - call-bound: 1.0.4 - for-each: 0.3.5 - get-proto: 1.0.1 - gopd: 1.2.0 - has-tostringtag: 1.0.2 - - which@1.3.1: - dependencies: - isexe: 2.0.0 - which@2.0.2: dependencies: isexe: 2.0.0 @@ -23908,18 +5928,6 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - wonka@4.0.15: {} - - word-wrap@1.2.5: {} - - wordwrap@1.0.0: {} - - wrap-ansi@6.2.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -23938,99 +5946,19 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 - wrappy@1.0.2: {} - - write-file-atomic@2.4.3: - dependencies: - graceful-fs: 4.2.11 - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - - write-file-atomic@4.0.2: - dependencies: - imurmurhash: 0.1.4 - signal-exit: 3.0.7 - - ws@6.2.3: - dependencies: - async-limiter: 1.0.1 - - ws@7.5.10: {} - - ws@8.20.1: {} - wsl-utils@0.3.1: dependencies: is-wsl: 3.1.1 powershell-utils: 0.1.0 - xcode@3.0.1: - dependencies: - simple-plist: 1.3.1 - uuid: 7.0.3 - - xml-naming@0.1.0: {} - - xml2js@0.6.0: - dependencies: - sax: 1.6.0 - xmlbuilder: 11.0.1 - - xmlbuilder@11.0.1: {} - - xmlbuilder@13.0.2: {} - - xmlbuilder@14.0.0: {} - - xmlbuilder@15.1.1: {} - - xtend@4.0.2: {} - - y18n@4.0.3: {} - y18n@5.0.8: {} yallist@3.1.1: {} - yallist@4.0.0: {} - yallist@5.0.0: {} - yaml@2.9.0: {} - - yargs-parser@18.1.3: - dependencies: - camelcase: 5.3.1 - decamelize: 1.2.0 - - yargs-parser@21.1.1: {} - yargs-parser@22.0.0: {} - yargs@15.4.1: - dependencies: - cliui: 6.0.0 - decamelize: 1.2.0 - find-up: 4.1.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - require-main-filename: 2.0.0 - set-blocking: 2.0.0 - string-width: 4.2.3 - which-module: 2.0.1 - y18n: 4.0.3 - yargs-parser: 18.1.3 - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - yargs@18.0.0: dependencies: cliui: 9.0.1 @@ -24040,10 +5968,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 22.0.0 - yn@3.1.1: {} - - yocto-queue@0.1.0: {} - youch-core@0.3.3: dependencies: '@poppinss/exception': 1.2.3 @@ -24063,19 +5987,4 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod-validation-error@2.1.0(zod@3.25.76): - dependencies: - zod: 3.25.76 - - zod@3.25.76: {} - - zod@4.4.3: {} - - zustand@4.5.7(@types/react@18.3.28)(react@18.2.0): - dependencies: - use-sync-external-store: 1.6.0(react@18.2.0) - optionalDependencies: - '@types/react': 18.3.28 - react: 18.2.0 - zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 22373e7..7887209 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,3 @@ packages: - - "packages/*" - - "services/*" + - "web" + - "browser-ext" diff --git a/server/alerts/alert-server.ts b/server/alerts/alert-server.ts deleted file mode 100644 index c55792b..0000000 --- a/server/alerts/alert-server.ts +++ /dev/null @@ -1,415 +0,0 @@ -/** - * WebSocket Alert Server - * Real-time alert broadcasting for call analysis events and anomalies - * Connects to CallAnalysisEngine and pushes alerts to subscribed clients - */ - -import { WebSocketServer, WebSocket } from 'ws'; -import { CallAnalysisEngine, CallEvent, Anomaly, SentimentAnalysis, AnalysisResult } from '../../src/lib/inference/call-analysis-engine'; -import { jwtVerify, SignJWT } from 'jose'; - -export type AlertType = - | 'anomaly' - | 'call_event' - | 'quality_degraded' - | 'sentiment_shift' - | 'call_summary' - | 'connection' - | 'disconnection'; - -export type AlertSeverity = 'info' | 'low' | 'medium' | 'high' | 'critical'; - -export interface AlertPayload { - id: string; - type: AlertType; - severity: AlertSeverity; - timestamp: number; - callId?: string; - title: string; - message: string; - data: Record; - actionable: boolean; -} - -export interface AlertServerConfig { - port?: number; - enableAuth?: boolean; - jwtSecret?: string; - allowedOrigins?: string[]; - alertCooldownMs?: number; - maxSubscribers?: number; - enableCallCorrelation?: boolean; -} - -export interface SubscriberSession { - ws: WebSocket; - userId?: string; - callIds: Set; - lastAlertTime: Map; - subscribedAt: number; -} - -const DEFAULT_CONFIG: Required = { - port: 8088, - enableAuth: true, - jwtSecret: process.env.ALERT_SERVER_JWT_SECRET || '', - allowedOrigins: ['http://localhost:3000'], - alertCooldownMs: 5000, - maxSubscribers: 100, - enableCallCorrelation: true, -}; - -/** - * JWT verification helper - */ -async function verifyJWT(token: string, secret: string): Promise { - try { - const decoded = await jwtVerify(token, new TextEncoder().encode(secret), { - algorithms: ['HS256'], - }); - return decoded; - } catch (error) { - console.error('[AlertServer] JWT verification failed:', (error as Error).message); - return null; - } -} - -export class AlertServer { - private wss: WebSocketServer | null = null; - private config: Required; - private subscribers: Map = new Map(); - private analysisEngines: Map = new Map(); - private alertHistory: AlertPayload[] = []; - private maxAlertHistory: number = 500; - private isRunning: boolean = false; - - constructor(config: AlertServerConfig = {}) { - this.config = { ...DEFAULT_CONFIG, ...config }; - } - - async start(): Promise { - this.wss = new WebSocketServer({ - port: this.config.port, - maxPayload: 1024 * 1024, - }); - - this.wss.on('connection', (ws: WebSocket, req) => { - this.handleConnection(ws, req); - }); - - this.wss.on('error', (error: Error) => { - console.error(`[AlertServer] WebSocket error: ${error.message}`); - }); - - this.isRunning = true; - console.log(`[AlertServer] Listening on port ${this.config.port}`); - } - - private handleConnection(ws: WebSocket, req: import('http').IncomingMessage): void { - const url = new URL(req.url || '', `http://${req.headers.host}`); - const sessionId = url.searchParams.get('sessionId') || `sub-${Date.now()}-${Math.random().toString(36).slice(2)}`; - let userId = url.searchParams.get('userId') || undefined; - const callId = url.searchParams.get('callId') || undefined; - - // Origin validation - const origin = req.headers.origin; - if (origin && !this.config.allowedOrigins.includes(origin)) { - ws.close(1008, 'Origin not allowed'); - return; - } - - // JWT Authentication (if enabled) - if (this.config.enableAuth && this.config.jwtSecret) { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - ws.close(4001, 'Missing or invalid JWT token'); - return; - } - - const token = authHeader.substring(7); - const decoded = verifyJWT(token, this.config.jwtSecret); - - if (!decoded) { - ws.close(4002, 'Invalid or expired JWT token'); - return; - } - - // Extract user ID from token if present - userId = (decoded as any).sub || userId; - } - - if (this.subscribers.size >= this.config.maxSubscribers) { - ws.close(1013, 'Too many subscribers'); - return; - } - - const session: SubscriberSession = { - ws, - userId, - callIds: callId ? new Set([callId]) : new Set(), - lastAlertTime: new Map(), - subscribedAt: Date.now(), - }; - - this.subscribers.set(sessionId, session); - - ws.send(JSON.stringify({ - type: 'connected', - payload: { sessionId, userId, timestamp: Date.now() }, - })); - - ws.on('message', (data: Buffer | ArrayBuffer) => { - this.handleMessage(sessionId, data); - }); - - ws.on('close', () => { - this.subscribers.delete(sessionId); - console.log(`[AlertServer] Subscriber disconnected: ${sessionId}`); - }); - - ws.on('error', (error: Error) => { - console.error(`[AlertServer] Subscriber error (${sessionId}): ${error.message}`); - }); - - console.log(`[AlertServer] Subscriber connected: ${sessionId}${callId ? ` (call: ${callId})` : ''}`); - } - - private handleMessage(sessionId: string, data: Buffer | ArrayBuffer): void { - try { - const message = JSON.parse(data.toString()); - const session = this.subscribers.get(sessionId); - if (!session) return; - - switch (message.type) { - case 'subscribe': - if (message.callId) { - session.callIds.add(message.callId); - } - break; - - case 'unsubscribe': - if (message.callId) { - session.callIds.delete(message.callId); - } - break; - - case 'ping': - session.ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() })); - break; - } - } catch (error) { - console.error(`[AlertServer] Message parse error: ${(error as Error).message}`); - } - } - - bindAnalysisEngine(callId: string, engine: CallAnalysisEngine): void { - this.analysisEngines.set(callId, engine); - - engine.on('result', (result: AnalysisResult) => { - this.processAnalysisResult(callId, result); - }); - - engine.on('events', (events: CallEvent[]) => { - events.forEach(event => { - this.sendAlert({ - type: 'call_event', - severity: event.severity as AlertSeverity, - callId, - title: this.formatEventType(event.type), - message: this.formatEventMessage(event), - data: { event, timestamp: event.timestamp }, - actionable: event.severity === 'high', - }); - }); - }); - - engine.on('anomalies', (anomalies: Anomaly[]) => { - anomalies.forEach(anomaly => { - this.sendAlert({ - type: 'anomaly', - severity: anomaly.severity as AlertSeverity, - callId, - title: this.formatAnomalyType(anomaly.type), - message: anomaly.description, - data: { - anomaly, - confidence: anomaly.confidence, - recommendation: anomaly.recommendation, - }, - actionable: anomaly.severity === 'high' || anomaly.severity === 'critical', - }); - }); - }); - - console.log(`[AlertServer] Bound analysis engine for call: ${callId}`); - } - - private processAnalysisResult(callId: string, result: AnalysisResult): void { - if (result.callQuality.mosScore < 3.0) { - this.sendAlert({ - type: 'quality_degraded', - severity: result.callQuality.mosScore < 2.5 ? 'high' : 'medium', - callId, - title: 'Call Quality Degraded', - message: `MOS score: ${result.callQuality.mosScore.toFixed(1)} (threshold: 3.0)`, - data: result.callQuality as unknown as Record, - actionable: true, - }); - } - - if (result.sentiment.sentiment === 'negative' && result.sentiment.confidence > 0.7) { - this.sendAlert({ - type: 'sentiment_shift', - severity: 'medium', - callId, - title: 'Negative Sentiment Detected', - message: `Confidence: ${(result.sentiment.confidence * 100).toFixed(0)}%`, - data: result.sentiment as unknown as Record, - actionable: false, - }); - } - } - - sendAlert(options: { - type: AlertType; - severity: AlertSeverity; - callId?: string; - title: string; - message: string; - data: Record; - actionable: boolean; - }): void { - const cooldownKey = `${options.callId}:${options.type}`; - const now = Date.now(); - - const sessionKeys = Array.from(this.subscribers.keys()); - for (const key of sessionKeys) { - const session = this.subscribers.get(key); - if (!session) continue; - - const lastTime = session.lastAlertTime.get(cooldownKey) || 0; - if (now - lastTime < this.config.alertCooldownMs) continue; - - if (options.callId && session.callIds.size > 0 && !session.callIds.has(options.callId)) continue; - - const alert: AlertPayload = { - id: `alert-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - type: options.type, - severity: options.severity, - timestamp: now, - callId: options.callId, - title: options.title, - message: options.message, - data: options.data, - actionable: options.actionable, - }; - - this.alertHistory.push(alert); - if (this.alertHistory.length > this.maxAlertHistory) { - this.alertHistory.shift(); - } - - if (session.ws.readyState === WebSocket.OPEN) { - session.ws.send(JSON.stringify(alert)); - } - session.lastAlertTime.set(cooldownKey, now); - } - } - - broadcastCallSummary(callId: string, summary: string): void { - this.sendAlert({ - type: 'call_summary', - severity: 'info', - callId, - title: 'Call Analysis Summary', - message: summary, - data: { summary }, - actionable: false, - }); - } - - getAlertHistory(limit: number = 50, callId?: string): AlertPayload[] { - let history = this.alertHistory; - if (callId) { - history = history.filter(a => a.callId === callId); - } - return history.slice(-limit); - } - - getSubscriberCount(): number { - return this.subscribers.size; - } - - getActiveCalls(): string[] { - return Array.from(this.analysisEngines.keys()); - } - - getEngine(callId: string): CallAnalysisEngine | undefined { - return this.analysisEngines.get(callId); - } - - async stop(): Promise { - this.isRunning = false; - - this.subscribers.forEach((session) => { - if (session.ws.readyState === WebSocket.OPEN) { - session.ws.send(JSON.stringify({ - type: 'server_shutdown', - payload: { timestamp: Date.now() }, - })); - session.ws.close(1001, 'Server shutting down'); - } - }); - - this.analysisEngines.forEach((engine) => { - engine.destroy(); - }); - - if (this.wss) { - await new Promise((resolve) => { - this.wss!.close(() => resolve()); - }); - this.wss = null; - } - - console.log('[AlertServer] Stopped'); - } - - private formatEventType(type: string): string { - const labels: Record = { - interrupt: 'Speaker Interrupt', - overlap: 'Speech Overlap', - long_pause: 'Long Pause', - volume_spike: 'Volume Spike', - silence: 'Silence Detected', - speaker_change: 'Speaker Change', - }; - return labels[type] || type; - } - - private formatEventMessage(event: CallEvent): string { - const messages: Record = { - interrupt: `Interrupt detected (${event.duration}ms)`, - overlap: `Speech overlap detected (${event.duration}ms)`, - long_pause: `Pause duration: ${(event.duration / 1000).toFixed(1)}s`, - volume_spike: `Volume spike: ${(event.metadata.level as number)?.toFixed(2) || 'unknown'}`, - silence: `Silence detected for ${(event.duration * 1000).toFixed(0)}ms`, - speaker_change: 'Speaker change detected', - }; - return messages[event.type] || 'Event detected'; - } - - private formatAnomalyType(type: string): string { - const labels: Record = { - background_noise: 'Background Noise', - echo: 'Echo Detected', - distortion: 'Audio Distortion', - dropouts: 'Audio Dropout', - excessive_silence: 'Excessive Silence', - volume_inconsistency: 'Volume Inconsistency', - }; - return labels[type] || type; - } -} - -export default AlertServer; diff --git a/server/package-lock.json b/server/package-lock.json deleted file mode 100644 index 93a4aad..0000000 --- a/server/package-lock.json +++ /dev/null @@ -1,214 +0,0 @@ -{ - "name": "server", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@types/jsonwebtoken": "^9.0.10", - "@types/ws": "^8.18.1", - "jsonwebtoken": "^9.0.3", - "ws": "^8.20.0" - } - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.19.0" - } - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "license": "MIT", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "license": "MIT" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "license": "MIT" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "license": "MIT" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "license": "MIT" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "license": "MIT" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "license": "MIT" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "license": "MIT" - }, - "node_modules/ws": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", - "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} diff --git a/server/package.json b/server/package.json deleted file mode 100644 index a0106d5..0000000 --- a/server/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "dependencies": { - "@types/jsonwebtoken": "^9.0.10", - "@types/ws": "^8.18.1", - "jsonwebtoken": "^9.0.3", - "ws": "^8.20.0" - } -} diff --git a/server/tsconfig.json b/server/tsconfig.json deleted file mode 100644 index e2886c2..0000000 --- a/server/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "outDir": "./dist", - "rootDir": ".", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true - }, - "include": ["**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/server/webrtc/signaling-server.ts b/server/webrtc/signaling-server.ts deleted file mode 100644 index b25ceab..0000000 --- a/server/webrtc/signaling-server.ts +++ /dev/null @@ -1,426 +0,0 @@ -import { WebSocketServer, WebSocket, Data } from 'ws'; -import { randomBytes } from 'crypto'; -import { IncomingMessage } from 'http'; -import jwt from 'jsonwebtoken'; - -/** - * WebRTC Signaling Server - * Handles offer/answer/ICE candidate exchange for P2P connections. - * - * Security hardening (FRE-4497): - * - JWT authentication required on WebSocket upgrade - * - Origin allowlist validation - * - JSON schema validation for all messages - * - Server-side peer identity (crypto.randomBytes) - * - Message size limits to prevent DoS - * - Connection timeout for idle peers - */ - -// ── Types ──────────────────────────────────────────────────────────────────── - -export interface SignalingServerConfig { - port: number; - host: string; - allowedOrigins: string[]; - jwtSecret: string; - maxMessageSize: number; - idleTimeoutMs: number; - maxConnectionsPerPeer: number; -} - -export interface SignalingMessage { - type: 'offer' | 'answer' | 'ice-candidate' | 'ping' | 'pong' | 'close'; - payload?: Record; - targetPeerId?: string; -} - -export interface PeerConnection { - ws: WebSocket; - peerId: string; - authenticatedUserId: string; - connections: Map; - lastActivity: number; - iceCandidates: Array>; -} - -export interface PeerSession { - targetPeerId: string; - dataChannelReady: boolean; - bufferedCandidates: Array>; -} - -// ── Constants ──────────────────────────────────────────────────────────────── - -const DEFAULT_CONFIG: SignalingServerConfig = { - port: parseInt(process.env.SIGNALING_PORT || '3001', 10), - host: process.env.SIGNALING_HOST || '0.0.0.0', - allowedOrigins: (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean), - jwtSecret: process.env.JWT_SECRET || randomBytes(32).toString('hex'), - maxMessageSize: 65536, - idleTimeoutMs: 300_000, - maxConnectionsPerPeer: 10, -}; - -// Message schema validators -const MESSAGE_TYPES = new Set(['offer', 'answer', 'ice-candidate', 'ping', 'pong', 'close']); - -function validateMessage(raw: unknown): raw is SignalingMessage { - if (typeof raw !== 'object' || raw === null) return false; - const msg = raw as Record; - if (!MESSAGE_TYPES.has(msg.type as string)) return false; - if (msg.payload && typeof msg.payload !== 'object') return false; - if (msg.targetPeerId !== undefined && typeof msg.targetPeerId !== 'string') return false; - if (msg.targetPeerId && msg.targetPeerId.length > 64) return false; - return true; -} - -// ── JWT Helper ─────────────────────────────────────────────────────────────── - -/** - * Extract JWT from Authorization header (preferred) or URL query (fallback). - * Header-first avoids token exposure in access logs. - */ -function extractJwt(req: IncomingMessage): string | null { - const auth = req.headers['authorization']; - if (auth?.startsWith('Bearer ')) return auth.slice(7); - const match = req.url?.match(/[?&]token=([^&]+)/); - return match ? decodeURIComponent(match[1]) : null; -} - -function verifyJwt(token: string, secret: string): { sub: string; exp?: number } | null { - try { - const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] }); - if (typeof decoded !== 'object' || !decoded.sub) return null; - return { - sub: String(decoded.sub), - exp: decoded.exp ? Number(decoded.exp) : undefined, - }; - } catch { - return null; - } -} - -// ── Server ─────────────────────────────────────────────────────────────────── - -export class SignalingServer { - private wss: WebSocketServer; - private peers: Map = new Map(); - private config: SignalingServerConfig; - private idleTimers: Map = new Map(); - - constructor(config: Partial = {}) { - this.config = { ...DEFAULT_CONFIG, ...config }; - this.wss = new WebSocketServer({ - port: this.config.port, - host: this.config.host, - maxPayload: this.config.maxMessageSize, - verifyClient: this.verifyClient.bind(this), - }); - this.wss.on('connection', this.handleConnection.bind(this)); - console.log(`[Signaling] Server listening on ${this.config.host}:${this.config.port}`); - } - - /** - * Verify incoming WebSocket connection: origin + auth - */ - private verifyClient(info: { req: IncomingMessage; origin: string }, cb: (result: boolean, status?: number, reason?: string) => void) { - // Origin validation - if (this.config.allowedOrigins.length > 0) { - const origin = info.origin || info.req.headers['origin'] || ''; - const allowed = this.config.allowedOrigins.some( - allowedOrigin => origin === allowedOrigin || origin.startsWith(allowedOrigin) - ); - if (!allowed) { - cb(false, 403, `Origin "${origin}" not in allowlist`); - return; - } - } - - // JWT authentication - const token = extractJwt(info.req); - if (!token) { - cb(false, 401, 'Missing JWT token'); - return; - } - - const payload = verifyJwt(token, this.config.jwtSecret); - if (!payload) { - cb(false, 401, 'Invalid or expired JWT'); - return; - } - - cb(true); - } - - /** - * Handle new WebSocket connection - */ - private handleConnection(ws: WebSocket, req: IncomingMessage) { - const token = extractJwt(req); - const payload = token ? verifyJwt(token, this.config.jwtSecret) : null; - const authenticatedUserId = payload?.sub || ''; - - // Server-side peer identity (crypto random) - const peerId = `peer_${randomBytes(8).toString('hex')}`; - - const peer: PeerConnection = { - ws, - peerId, - authenticatedUserId, - connections: new Map(), - lastActivity: Date.now(), - iceCandidates: [], - }; - - this.peers.set(peerId, peer); - - // Send handshake with assigned peer ID - ws.send(JSON.stringify({ - type: 'handshake', - payload: { peerId, message: 'Connected' }, - })); - - // Idle timeout - const timer = setTimeout(() => { - if (Date.now() - peer.lastActivity > this.config.idleTimeoutMs) { - ws.close(1001, 'Idle timeout'); - } - }, this.config.idleTimeoutMs); - this.idleTimers.set(peerId, timer); - - ws.on('message', this.handleMessage(peer).bind(this)); - ws.on('close', () => this.handleDisconnect(peer)); - ws.on('error', (err) => { - console.error(`[Signaling] Peer ${peerId} error:`, err.message); - this.handleDisconnect(peer); - }); - } - - /** - * Handle incoming message from peer - */ - private handleMessage(peer: PeerConnection) { - return (data: Data) => { - peer.lastActivity = Date.now(); - - // Parse with size guard - let raw: unknown; - try { - const str = data.toString(); - if (str.length > this.config.maxMessageSize) { - peer.ws.send(JSON.stringify({ type: 'error', payload: { message: 'Message too large' } })); - return; - } - raw = JSON.parse(str); - } catch { - peer.ws.send(JSON.stringify({ type: 'error', payload: { message: 'Invalid JSON' } })); - return; - } - - // Schema validation - if (!validateMessage(raw)) { - peer.ws.send(JSON.stringify({ type: 'error', payload: { message: 'Invalid message schema' } })); - return; - } - - const msg = raw as SignalingMessage; - - switch (msg.type) { - case 'ping': - peer.ws.send(JSON.stringify({ type: 'pong', payload: { timestamp: Date.now() } })); - break; - case 'offer': - this.handleOffer(peer, msg); - break; - case 'answer': - this.handleAnswer(peer, msg); - break; - case 'ice-candidate': - this.handleIceCandidate(peer, msg); - break; - case 'close': - peer.ws.close(1000, 'Peer requested close'); - break; - } - }; - } - - /** - * Route offer to target peer - */ - private handleOffer(source: PeerConnection, msg: SignalingMessage) { - const targetId = msg.targetPeerId; - if (!targetId) { - source.ws.send(JSON.stringify({ type: 'error', payload: { message: 'Missing targetPeerId' } })); - return; - } - - // Enforce max connections - if (source.connections.size >= this.config.maxConnectionsPerPeer) { - source.ws.send(JSON.stringify({ type: 'error', payload: { message: 'Max connections reached' } })); - return; - } - - const target = this.peers.get(targetId); - if (!target) { - source.ws.send(JSON.stringify({ type: 'error', payload: { message: `Peer ${targetId} not found` } })); - return; - } - - // Register session - const session: PeerSession = { - targetPeerId: targetId, - dataChannelReady: false, - bufferedCandidates: [...source.iceCandidates], - }; - source.connections.set(targetId, session); - target.connections.set(source.peerId, { - targetPeerId: source.peerId, - dataChannelReady: false, - bufferedCandidates: [], - }); - - // Forward offer to target - target.ws.send(JSON.stringify({ - type: 'offer', - payload: msg.payload, - targetPeerId: source.peerId, - })); - - // Send buffered ICE candidates if data channel is ready - if (session.dataChannelReady) { - for (const candidate of session.bufferedCandidates) { - target.ws.send(JSON.stringify({ - type: 'ice-candidate', - payload: candidate, - targetPeerId: source.peerId, - })); - } - } - } - - /** - * Route answer to target peer - */ - private handleAnswer(source: PeerConnection, msg: SignalingMessage) { - const targetId = msg.targetPeerId; - if (!targetId) { - source.ws.send(JSON.stringify({ type: 'error', payload: { message: 'Missing targetPeerId' } })); - return; - } - - const target = this.peers.get(targetId); - if (!target) { - source.ws.send(JSON.stringify({ type: 'error', payload: { message: `Peer ${targetId} not found` } })); - return; - } - - // Mark data channel as ready for buffered candidate delivery - const session = source.connections.get(targetId); - if (session) { - session.dataChannelReady = true; - } - - target.ws.send(JSON.stringify({ - type: 'answer', - payload: msg.payload, - targetPeerId: source.peerId, - })); - } - - /** - * Route ICE candidate to target peer - */ - private handleIceCandidate(source: PeerConnection, msg: SignalingMessage) { - const targetId = msg.targetPeerId; - if (!targetId) { - source.ws.send(JSON.stringify({ type: 'error', payload: { message: 'Missing targetPeerId' } })); - return; - } - - const candidate = msg.payload as Record | undefined; - if (!candidate) return; - - // Buffer candidate if target session not ready yet - const session = source.connections.get(targetId); - if (session && !session.dataChannelReady) { - source.iceCandidates.push(candidate); - return; - } - - const target = this.peers.get(targetId); - if (!target) return; - - target.ws.send(JSON.stringify({ - type: 'ice-candidate', - payload: candidate, - targetPeerId: source.peerId, - })); - } - - /** - * Handle peer disconnect - */ - private handleDisconnect(peer: PeerConnection) { - // Notify connected peers - for (const [targetId, session] of peer.connections) { - const target = this.peers.get(targetId); - if (target) { - target.ws.send(JSON.stringify({ - type: 'close', - payload: { peerId: peer.peerId, reason: 'Remote peer disconnected' }, - targetPeerId: peer.peerId, - })); - target.connections.delete(peer.peerId); - } - } - - // Clear idle timer - const timer = this.idleTimers.get(peer.peerId); - if (timer) clearTimeout(timer); - this.idleTimers.delete(peer.peerId); - - this.peers.delete(peer.peerId); - console.log(`[Signaling] Peer ${peer.peerId} disconnected`); - } - - /** - * Graceful shutdown with timeout - */ - async stop(timeoutMs = 5000): Promise { - return new Promise((resolve) => { - const deadline = Date.now() + timeoutMs; - - for (const [peerId, peer] of this.peers) { - const remaining = Math.max(100, deadline - Date.now()); - setTimeout(() => { - peer.ws.close(1001, 'Server shutting down'); - }, remaining); - } - - const serverTimer = setTimeout(() => { - this.wss.close(); - resolve(); - }, timeoutMs); - - this.wss.close(() => { - clearTimeout(serverTimer); - resolve(); - }); - }); - } - - /** - * Get server stats - */ - getStats() { - return { - connectedPeers: this.peers.size, - totalConnections: Array.from(this.peers.values()).reduce((sum, p) => sum + p.connections.size, 0), - }; - } -} - -export function createSignalingServer(config?: Partial): SignalingServer { - return new SignalingServer(config); -} diff --git a/services/darkwatch/Dockerfile b/services/darkwatch/Dockerfile deleted file mode 100644 index a3d2d02..0000000 --- a/services/darkwatch/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -FROM node:20-alpine AS builder - -WORKDIR /app - -COPY package.json pnpm-lock.yaml turbo.json pnpm-workspace.yaml ./ -COPY packages/api/package.json ./packages/api/ -COPY packages/db/package.json ./packages/db/ -COPY packages/types/package.json ./packages/types/ -COPY packages/core/package.json ./packages/core/ 2>/dev/null || true -COPY packages/jobs/package.json ./packages/jobs/ -COPY packages/shared-notifications/package.json ./packages/shared-notifications/ -COPY services/darkwatch/package.json ./services/darkwatch/ -COPY services/spamshield/package.json ./services/spamshield/ -COPY services/voiceprint/package.json ./services/voiceprint/ - -RUN npm i -g pnpm@9 && pnpm install --frozen-lockfile - -COPY tsconfig.json ./ -COPY packages/types/tsconfig.json ./packages/types/ -COPY packages/db/tsconfig.json ./packages/db/ -COPY services/darkwatch/tsconfig.json ./services/darkwatch/ -COPY services/darkwatch/ ./services/darkwatch/ -COPY packages/types/ ./packages/types/ -COPY packages/db/ ./packages/db/ - -RUN pnpm build --filter=@shieldai/types --filter=@shieldai/db --filter=@shieldai/darkwatch - -FROM node:20-alpine AS runner - -WORKDIR /app - -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 shieldai - -COPY --from=builder --chown=shieldai:nodejs /app/services/darkwatch/dist ./dist -COPY --from=builder --chown=shieldai:nodejs /app/node_modules ./node_modules -COPY --from=builder --chown=shieldai:nodejs /app/services/darkwatch/package.json ./package.json -COPY --from=builder --chown=shieldai:nodejs /app/packages/db ./packages/db - -USER shieldai - -EXPOSE 3001 - -CMD ["node", "dist/index.js"] diff --git a/services/darkwatch/package.json b/services/darkwatch/package.json deleted file mode 100644 index 0257bc2..0000000 --- a/services/darkwatch/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@shieldai/darkwatch", - "version": "0.1.0", - "main": "./dist/index.js", - "types": "./dist/index.js", - "scripts": { - "build": "tsc", - "test": "vitest run", - "test:coverage": "vitest run --coverage", - "lint": "eslint src/" - }, - "dependencies": { - "@shieldai/db": "workspace:*", - "@shieldai/types": "workspace:*", - "@shieldai/correlation": "workspace:*", - "node-cache": "^5.1.2" - }, - "devDependencies": { - "vitest": "^4.1.5", - "@vitest/coverage-v8": "^4.1.5" - }, - "exports": { - ".": "./src/index.ts" - } -} diff --git a/services/darkwatch/src/alert.pipeline.ts b/services/darkwatch/src/alert.pipeline.ts deleted file mode 100644 index a3d2f1a..0000000 --- a/services/darkwatch/src/alert.pipeline.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { prisma, AlertType, AlertSeverity } from '@shieldai/db'; -import { - NotificationService, - NotificationPriority, - loadNotificationConfig, -} from '@shieldsai/shared-notifications'; - -const ALERT_DEDUP_WINDOW_MS = 24 * 60 * 60 * 1000; - -export class AlertPipeline { - private notificationService: NotificationService; - - constructor() { - this.notificationService = new NotificationService(loadNotificationConfig()); - } - - async processNewExposures(exposureIds: string[]) { - const exposures = await prisma.exposure.findMany({ - where: { id: { in: exposureIds }, isFirstTime: true }, - include: { - subscription: { - select: { - id: true, - userId: true, - tier: true, - }, - }, - watchlistItem: true, - }, - }); - - const alertsCreated: Awaited>[] = []; - - for (const exposure of exposures) { - const dedupKey = `exposure:${exposure.subscriptionId}:${exposure.source}:${exposure.identifierHash}`; - - const recentAlert = await prisma.alert.findFirst({ - where: { - subscriptionId: exposure.subscriptionId, - type: AlertType.exposure_detected, - createdAt: { - gte: new Date(Date.now() - ALERT_DEDUP_WINDOW_MS), - }, - }, - orderBy: { createdAt: 'desc' }, - }); - - if (recentAlert) { - continue; - } - - const alert = await prisma.alert.create({ - data: { - subscriptionId: exposure.subscriptionId, - userId: exposure.subscription.userId, - exposureId: exposure.id, - type: AlertType.exposure_detected, - title: this.buildTitle(exposure), - message: this.buildMessage(exposure), - severity: this.mapSeverity(exposure.severity), - channel: this.getChannelsForTier(exposure.subscription.tier), - }, - }); - - alertsCreated.push(alert); - - await this.dispatchNotification(alert, exposure); - } - - return alertsCreated; - } - - async dispatchScanCompleteAlert( - subscriptionId: string, - userId: string, - exposuresFound: number - ) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { tier: true }, - }); - - if (!subscription) return; - - const alert = await prisma.alert.create({ - data: { - subscriptionId, - userId, - type: AlertType.scan_complete, - title: 'DarkWatch Scan Complete', - message: `Scan found ${exposuresFound} new exposure${exposuresFound === 1 ? '' : 's'}.`, - severity: exposuresFound > 0 ? 'warning' : 'info', - channel: this.getChannelsForTier(subscription.tier), - }, - }); - - await this.dispatchNotification(alert, { - source: 'hibp', - severity: 'info', - identifier: '', - dataType: 'email', - } as any); - - return alert; - } - - private async dispatchNotification( - alert: { - userId: string; - channel: string[]; - title: string; - message: string; - severity: AlertSeverity; - }, - exposure: { source: string; severity: string; identifier: string; dataType: string } - ) { - try { - if (!this.notificationService.isFullyConfigured()) return; - - await this.notificationService.sendMultiChannelNotification( - { - userId: alert.userId, - }, - alert.channel as any, - alert.title, - `

          ${alert.message}

          -

          Source: ${exposure.source}

          -

          Severity: ${exposure.severity}

          -

          Type: ${exposure.dataType}

          `, - alert.severity === 'critical' - ? NotificationPriority.HIGH - : NotificationPriority.NORMAL - ); - } catch (error) { - console.error('[AlertPipeline] Notification dispatch error:', error); - } - } - - private buildTitle(exposure: { - source: string; - dataType: string; - severity: string; - }): string { - return `${exposure.severity.toUpperCase()}: ${exposure.dataType} exposure on ${exposure.source}`; - } - - private buildMessage(exposure: { - identifier: string; - source: string; - severity: string; - dataType: string; - }): string { - const masked = exposure.identifier.includes('@') - ? exposure.identifier.replace(/(?<=.{2}).*(?=@)/, '***') - : exposure.identifier.slice(0, 3) + '***'; - - return `Your ${exposure.dataType} (${masked}) was found in a ${exposure.source} breach with ${exposure.severity} severity.`; - } - - private mapSeverity(severity: string): AlertSeverity { - return severity as AlertSeverity; - } - - private getChannelsForTier(tier: string): string[] { - const channelMap: Record = { - basic: ['email'], - plus: ['email', 'push'], - premium: ['email', 'push', 'sms'], - }; - return channelMap[tier] || ['email']; - } -} - -export const alertPipeline = new AlertPipeline(); diff --git a/services/darkwatch/src/alerts/AlertPipeline.ts b/services/darkwatch/src/alerts/AlertPipeline.ts deleted file mode 100644 index f70fe34..0000000 --- a/services/darkwatch/src/alerts/AlertPipeline.ts +++ /dev/null @@ -1,161 +0,0 @@ -import prisma from "@shieldai/db"; -import { AlertChannel, AlertStatus, Severity } from "@shieldai/types"; -import { emitDarkWatchAlert } from "@shieldai/correlation"; -import { createHash } from "crypto"; -import NodeCache from "node-cache"; - -export class AlertPipeline { - private cache: NodeCache; - private dedupWindowMs = 24 * 60 * 60 * 1000; - - constructor() { - this.cache = new NodeCache({ stdTTL: 3600, checkperiod: 600 }); - } - - async createAlert( - userId: string, - exposureId: string, - severity: Severity, - channel: AlertChannel = AlertChannel.EMAIL - ): Promise { - const dedupKey = this.computeDedupKey(userId, exposureId); - - const cached = this.cache.get(dedupKey); - if (cached) return false; - - const existing = await prisma.alert.findFirst({ - where: { dedupKey, status: AlertStatus.SENT }, - }); - - if (existing) return false; - - const alert = await prisma.alert.create({ - data: { - userId, - exposureId, - severity, - channel, - status: AlertStatus.PENDING, - dedupKey, - }, - }); - - const exposure = await prisma.exposure.findUnique({ - where: { id: exposureId }, - include: { watchListItem: true }, - }); - - if (exposure) { - emitDarkWatchAlert( - userId, - exposureId, - alert.id, - exposure.breachName, - severity, - channel, - exposure.dataType, - exposure.dataSource - ).catch((err) => console.error(`[Correlation] DarkWatch emit failed:`, err)); - } - - this.cache.set(dedupKey, true, this.dedupWindowMs / 1000); - return true; - } - - async sendPendingAlerts(): Promise { - const pending = await prisma.alert.findMany({ - where: { status: AlertStatus.PENDING }, - include: { user: true, exposure: true }, - orderBy: { createdAt: "asc" }, - take: 100, - }); - - let sent = 0; - for (const alert of pending) { - try { - await this.sendNotification(alert); - await prisma.alert.update({ - where: { id: alert.id }, - data: { status: AlertStatus.SENT, sentAt: new Date() }, - }); - sent++; - } catch (err) { - console.error(`Alert send failed for ${alert.id}:`, err); - } - } - - return sent; - } - - async markRead(alertId: string, userId: string): Promise { - await prisma.alert.updateMany({ - where: { id: alertId, userId }, - data: { status: AlertStatus.READ }, - }); - } - - async getUserAlerts(userId: string, limit = 50, offset = 0) { - return prisma.alert.findMany({ - where: { userId }, - include: { exposure: true }, - orderBy: { createdAt: "desc" }, - take: limit, - skip: offset, - }); - } - - async countUnread(userId: string): Promise { - return prisma.alert.count({ - where: { userId, status: { in: [AlertStatus.PENDING, AlertStatus.SENT] } }, - }); - } - - private async sendNotification(alert: { - id: string; - userId: string; - severity: Severity; - channel: AlertChannel; - user: { email: string; name: string | null }; - exposure: { breachName: string; exposedAt: Date; dataType: string[] }; - }): Promise { - switch (alert.channel) { - case AlertChannel.EMAIL: - await this.sendEmail(alert); - break; - case AlertChannel.PUSH: - console.log(`[Push] Alert ${alert.id} for user ${alert.userId}`); - break; - case AlertChannel.SMS: - console.log(`[SMS] Alert ${alert.id} for user ${alert.userId}`); - break; - } - } - - private async sendEmail(alert: { - user: { email: string; name: string | null }; - severity: Severity; - exposure: { breachName: string; exposedAt: Date; dataType: string[] }; - }): Promise { - const subject = `[DarkWatch] ${alert.severity} Exposure Detected`; - const body = ` -Dear ${alert.user.name || "User"}, - -A new data exposure has been detected: - - Breach: ${alert.exposure.breachName} - Date: ${alert.exposure.exposedAt.toISOString().split("T")[0]} - Severity: ${alert.severity} - Data Types: ${alert.exposure.dataType.join(", ")} - -Login to your DarkWatch dashboard for details. - -— ShieldAI Team -`.trim(); - - console.log(`[Email] To: ${alert.user.email}, Subject: ${subject}`); - } - - computeDedupKey(userId: string, exposureId: string): string { - return createHash("sha256").update(`${userId}:${exposureId}`).digest("hex"); - } -} diff --git a/services/darkwatch/src/hibp/HIBPService.ts b/services/darkwatch/src/hibp/HIBPService.ts deleted file mode 100644 index a58106c..0000000 --- a/services/darkwatch/src/hibp/HIBPService.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { DataSource, Severity } from "@shieldai/types"; - -export interface HIBPBreach { - name: string; - title: string; - domain: string; - loginCount: number; - passwordCount: number; - date: Date; - breachDate: Date; - addedDate: Date; - pwnCount: number; - dataClasses: string[]; - logo: string; -} - -export class HIBPService { - private baseUrl = "https://haveibeenpwned.com/api/v3"; - private apiKey?: string; - - constructor(apiKey?: string) { - this.apiKey = apiKey; - } - - async checkEmail(email: string): Promise { - const url = `${this.baseUrl}/breached-account/${encodeURIComponent(email)}`; - const headers: Record = { - "User-Agent": "ShieldAI-DarkWatch/1.0", - "HIBP-API-Version": "3", - }; - if (this.apiKey) { - headers["hibp-api-key"] = this.apiKey; - } - - const response = await fetch(url, { headers }); - - if (response.status === 404) { - return []; - } - - if (response.status === 410) { - throw new Error(`Email not found in HIBP: ${email}`); - } - - if (response.status === 429) { - const retryAfter = parseInt(response.headers.get("Retry-After") || "60", 10); - throw new Error(`HIBP rate limited. Retry after ${retryAfter}s`); - } - - if (!response.ok) { - throw new Error(`HIBP API error: ${response.status} ${response.statusText}`); - } - - return response.json() as Promise; - } - - async rangeQuery(hashPrefix: string): Promise { - const url = `${this.baseUrl}/range/${hashPrefix}`; - const response = await fetch(url, { - headers: { "User-Agent": "ShieldAI-DarkWatch/1.0" }, - }); - - if (!response.ok) { - throw new Error(`HIBP range API error: ${response.status}`); - } - - const text = await response.text(); - return text.split("\n").map((line) => line.split(":")[0].toUpperCase()); - } - - getSeverity(breach: HIBPBreach): Severity { - const criticalClasses = ["Password", "Email Address", "Bank Account", "Credit Card", "Social Security Number"]; - const warningClasses = ["Phone Number", "IP Address", "Geolocation", "IP & User agent"]; - - const hasCritical = breach.dataClasses.some((c) => criticalClasses.includes(c)); - const hasWarning = breach.dataClasses.some((c) => warningClasses.includes(c)); - - const breachAge = (Date.now() - breach.breachDate.getTime()) / (1000 * 60 * 60 * 24); - - if (hasCritical) return Severity.CRITICAL; - if (hasWarning) return Severity.WARNING; - if (breachAge > 365) return Severity.INFO; - - return Severity.WARNING; - } - - mapToDataSource(): DataSource { - return DataSource.HIBP; - } -} diff --git a/services/darkwatch/src/index.ts b/services/darkwatch/src/index.ts deleted file mode 100644 index f18ad02..0000000 --- a/services/darkwatch/src/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { watchlistService } from './watchlist.service'; -export { scanService } from './scan.service'; -export { schedulerService } from './scheduler.service'; -export { webhookService } from './webhook.service'; -export { alertPipeline } from './alert.pipeline'; diff --git a/services/darkwatch/src/matching/MatchingEngine.ts b/services/darkwatch/src/matching/MatchingEngine.ts deleted file mode 100644 index ee82cfb..0000000 --- a/services/darkwatch/src/matching/MatchingEngine.ts +++ /dev/null @@ -1,104 +0,0 @@ -import prisma from "@shieldai/db"; -import { ExposureResult, DataSource, Severity } from "@shieldai/types"; -import { createHash } from "crypto"; - -export class MatchingEngine { - async matchExposure( - watchListItemId: string, - dataSource: DataSource, - breachName: string, - exposedAt: Date, - dataType: string[], - severity: Severity, - details?: string - ): Promise { - const contentHash = this.computeContentHash(dataSource, breachName, watchListItemId); - - const existing = await prisma.exposure.findUnique({ - where: { contentHash }, - }); - - if (existing) { - return null; - } - - const exposure = await prisma.exposure.create({ - data: { - watchListItemId, - dataSource, - breachName, - exposedAt, - dataType, - severity, - details, - contentHash, - }, - }); - - return { - dataSource: exposure.dataSource, - breachName: exposure.breachName, - exposedAt: exposure.exposedAt, - dataType: exposure.dataType, - severity: exposure.severity, - details: exposure.details || "", - }; - } - - async getExposuresForUser(userId: string): Promise { - const items = await prisma.watchListItem.findMany({ - where: { userId }, - include: { exposures: true }, - }); - - const results: ExposureResult[] = []; - for (const item of items) { - for (const exp of item.exposures) { - results.push({ - dataSource: exp.dataSource, - breachName: exp.breachName, - exposedAt: exp.exposedAt, - dataType: exp.dataType, - severity: exp.severity, - details: exp.details || "", - }); - } - } - - return results.sort((a, b) => { - const severityOrder = { CRITICAL: 0, WARNING: 1, INFO: 2 }; - return severityOrder[a.severity] - severityOrder[b.severity]; - }); - } - - async getExposureById(exposureId: string): Promise { - const exposure = await prisma.exposure.findUnique({ - where: { id: exposureId }, - include: { watchListItem: true }, - }); - - if (!exposure) return null; - - return { - dataSource: exposure.dataSource, - breachName: exposure.breachName, - exposedAt: exposure.exposedAt, - dataType: exposure.dataType, - severity: exposure.severity, - details: exposure.details || "", - }; - } - - countExposures(userId: string): Promise { - return prisma.exposure.count({ - where: { - watchListItem: { userId }, - }, - }); - } - - computeContentHash(dataSource: DataSource, breachName: string, watchListItemId: string): string { - const raw = `${dataSource}:${breachName}:${watchListItemId}`; - return createHash("sha256").update(raw).digest("hex"); - } -} diff --git a/services/darkwatch/src/scan.service.ts b/services/darkwatch/src/scan.service.ts deleted file mode 100644 index 61a3401..0000000 --- a/services/darkwatch/src/scan.service.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { prisma, ExposureSource, ExposureSeverity, WatchlistType } from '@shieldai/db'; -import { createHash } from 'crypto'; - -function hashIdentifier(identifier: string): string { - return createHash('sha256').update(identifier.toLowerCase().trim()).digest('hex'); -} - -function determineSeverity( - source: ExposureSource, - dataType: WatchlistType -): ExposureSeverity { - const criticalSources = [ExposureSource.darkWebForum, ExposureSource.honeypot]; - const warningSources = [ExposureSource.hibp, ExposureSource.shodan]; - const criticalTypes = [WatchlistType.ssn]; - - if (criticalTypes.includes(dataType)) return ExposureSeverity.critical; - if (criticalSources.includes(source)) return ExposureSeverity.critical; - if (warningSources.includes(source)) return ExposureSeverity.warning; - return ExposureSeverity.info; -} - -export class ScanService { - async checkHIBP(email: string): Promise<{ exposed: boolean; sources: string[] }> { - try { - const response = await fetch( - `https://hibp.com/api/v2/${encodeURIComponent(email)}`, - { - headers: { - 'hibp-api-key': process.env.HIBP_API_KEY || '', - Accept: 'application/json', - }, - signal: AbortSignal.timeout(15000), - } - ); - - if (response.status === 404) { - return { exposed: false, sources: [] }; - } - - if (!response.ok) { - console.error(`[ScanService:HIBP] Status ${response.status} for ${email}`); - return { exposed: false, sources: [] }; - } - - const data = await response.json(); - const sources = Array.isArray(data) - ? data.map((p: { Name: string }) => p.Name) - : []; - - return { exposed: sources.length > 0, sources }; - } catch (error) { - console.error('[ScanService:HIBP] Error:', error); - return { exposed: false, sources: [] }; - } - } - - async checkShodan(domain: string): Promise<{ exposed: boolean; ports: string[]; ips: string[] }> { - try { - const response = await fetch( - `https://api.shodan.io/shodan/host/${encodeURIComponent(domain)}`, - { - headers: { - Authorization: `Bearer ${process.env.SHODAN_API_KEY || ''}`, - }, - signal: AbortSignal.timeout(15000), - } - ); - - if (response.status === 404) { - return { exposed: false, ports: [], ips: [] }; - } - - if (!response.ok) { - console.error(`[ScanService:Shodan] Status ${response.status} for ${domain}`); - return { exposed: false, ports: [], ips: [] }; - } - - const data = await response.json(); - return { - exposed: !!data.ip_str, - ports: data.ports?.map(String) || [], - ips: [data.ip_str || ''], - }; - } catch (error) { - console.error('[ScanService:Shodan] Error:', error); - return { exposed: false, ports: [], ips: [] }; - } - } - - async processSubscriptionScan( - subscriptionId: string, - watchlistItems: Awaited> - ): Promise<{ exposuresCreated: number; exposuresUpdated: number }> { - let exposuresCreated = 0; - let exposuresUpdated = 0; - - for (const item of watchlistItems) { - const identifier = item.value; - const identifierHash = hashIdentifier(identifier); - - switch (item.type) { - case WatchlistType.email: { - const hibpResult = await this.checkHIBP(identifier); - if (hibpResult.exposed) { - for (const source of hibpResult.sources) { - const existing = await prisma.exposure.findFirst({ - where: { - subscriptionId, - source: ExposureSource.hibp, - identifierHash, - metadata: { path: ['dbName'], equals: source }, - }, - }); - - if (existing) { - await prisma.exposure.update({ - where: { id: existing.id }, - data: { detectedAt: new Date() }, - }); - exposuresUpdated++; - } else { - await prisma.exposure.create({ - data: { - subscriptionId, - watchlistItemId: item.id, - source: ExposureSource.hibp, - dataType: item.type, - identifier, - identifierHash, - severity: determineSeverity(ExposureSource.hibp, item.type), - isFirstTime: true, - metadata: { dbName: source }, - detectedAt: new Date(), - }, - }); - exposuresCreated++; - } - } - } - break; - } - - case WatchlistType.domain: { - const shodanResult = await this.checkShodan(identifier); - if (shodanResult.exposed) { - const existing = await prisma.exposure.findFirst({ - where: { - subscriptionId, - source: ExposureSource.shodan, - identifierHash, - }, - }); - - if (existing) { - await prisma.exposure.update({ - where: { id: existing.id }, - data: { - detectedAt: new Date(), - metadata: { ports: shodanResult.ports, ips: shodanResult.ips }, - }, - }); - exposuresUpdated++; - } else { - await prisma.exposure.create({ - data: { - subscriptionId, - watchlistItemId: item.id, - source: ExposureSource.shodan, - dataType: item.type, - identifier, - identifierHash, - severity: determineSeverity(ExposureSource.shodan, item.type), - isFirstTime: true, - metadata: { ports: shodanResult.ports, ips: shodanResult.ips }, - detectedAt: new Date(), - }, - }); - exposuresCreated++; - } - } - break; - } - - default: { - const existing = await prisma.exposure.findFirst({ - where: { subscriptionId, watchlistItemId: item.id, identifierHash }, - }); - - if (!existing) { - await prisma.exposure.create({ - data: { - subscriptionId, - watchlistItemId: item.id, - source: ExposureSource.darkWebForum, - dataType: item.type, - identifier, - identifierHash, - severity: determineSeverity(ExposureSource.darkWebForum, item.type), - isFirstTime: true, - detectedAt: new Date(), - }, - }); - exposuresCreated++; - } - break; - } - } - } - - return { exposuresCreated, exposuresUpdated }; - } - - async getWatchlistItems(subscriptionId: string) { - return prisma.watchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - }); - } -} - -export const scanService = new ScanService(); diff --git a/services/darkwatch/src/scanner/ScanService.ts b/services/darkwatch/src/scanner/ScanService.ts deleted file mode 100644 index 0f87b8b..0000000 --- a/services/darkwatch/src/scanner/ScanService.ts +++ /dev/null @@ -1,88 +0,0 @@ -import prisma from "@shieldai/db"; -import { WatchListService } from "./watchlist/WatchListService"; -import { HIBPService } from "./hibp/HIBPService"; -import { MatchingEngine } from "./matching/MatchingEngine"; -import { AlertPipeline } from "./alerts/AlertPipeline"; -import { DataSource, ScanJobStatus } from "@shieldai/types"; - -export class ScanService { - private watchList: WatchListService; - private hibp: HIBPService; - private matching: MatchingEngine; - private alerts: AlertPipeline; - - constructor() { - this.watchList = new WatchListService(); - this.hibp = new HIBPService(process.env.HIBP_API_KEY); - this.matching = new MatchingEngine(); - this.alerts = new AlertPipeline(); - } - - async runScan(userId: string, source?: DataSource): Promise { - const job = await prisma.scanJob.create({ - data: { - userId, - status: ScanJobStatus.RUNNING, - source: source || DataSource.HIBP, - }, - }); - - let resultCount = 0; - - try { - const activeItems = await this.watchList.getActiveItems(userId); - - for (const item of activeItems) { - if (item.identifierType === "EMAIL") { - const breaches = await this.hibp.checkEmail(item.identifierValue); - for (const breach of breaches) { - const severity = this.hibp.getSeverity(breach); - const matched = await this.matching.matchExposure( - item.id, - DataSource.HIBP, - breach.name, - breach.breachDate, - breach.dataClasses, - severity, - `Breach: ${breach.title}. Domain: ${breach.domain}. PwnCount: ${breach.pwnCount}` - ); - - if (matched) { - resultCount++; - await this.alerts.createAlert(userId, matched.dataSource + ":" + breach.name, severity); - } - } - } - } - - await prisma.scanJob.update({ - where: { id: job.id }, - data: { - status: ScanJobStatus.COMPLETED, - resultCount, - completedAt: new Date(), - }, - }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - await prisma.scanJob.update({ - where: { id: job.id }, - data: { - status: ScanJobStatus.FAILED, - errorMessage: message, - completedAt: new Date(), - }, - }); - } - - return resultCount; - } - - async getScanHistory(userId: string, limit = 20) { - return prisma.scanJob.findMany({ - where: { userId }, - orderBy: { createdAt: "desc" }, - take: limit, - }); - } -} diff --git a/services/darkwatch/src/scheduler.service.ts b/services/darkwatch/src/scheduler.service.ts deleted file mode 100644 index fefd4b8..0000000 --- a/services/darkwatch/src/scheduler.service.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { prisma, SubscriptionTier, SubscriptionStatus } from '@shieldai/db'; -import { tierConfig } from '@shieldsai/shared-billing'; -import { darkwatchScanQueue } from '@shieldsai/jobs'; -import { randomUUID } from 'crypto'; - -const CRON_EXPRESSIONS = { - daily: '0 0 * * *', - hourly: '0 * * * *', - realtime: null, -}; - -export class SchedulerService { - async scheduleSubscriptionScans() { - const activeSubscriptions = await prisma.subscription.findMany({ - where: { - tier: { in: [SubscriptionTier.basic, SubscriptionTier.plus, SubscriptionTier.premium] }, - status: SubscriptionStatus.active, - }, - select: { - id: true, - tier: true, - userId: true, - }, - }); - - const jobsEnqueued = []; - - for (const subscription of activeSubscriptions) { - const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency; - const cron = CRON_EXPRESSIONS[frequency]; - - if (!cron) { - continue; - } - - const jobKey = `scheduled-scan:${subscription.id}`; - - try { - await darkwatchScanQueue.add( - 'scheduled-scan', - { - subscriptionId: subscription.id, - tier: subscription.tier, - scanType: 'scheduled', - }, - { - jobId: jobKey, - repeat: { - every: frequency === 'daily' - ? 24 * 60 * 60 * 1000 - : 60 * 60 * 1000, - }, - priority: subscription.tier === SubscriptionTier.premium ? 1 : 3, - } - ); - - jobsEnqueued.push({ - subscriptionId: subscription.id, - tier: subscription.tier, - frequency, - }); - } catch (error) { - if ((error as Error).message?.includes('Duplicate')) { - continue; - } - console.error( - `[SchedulerService] Failed to schedule scan for ${subscription.id}:`, - error - ); - } - } - - return jobsEnqueued; - } - - async enqueueOnDemandScan(subscriptionId: string) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { id: true, tier: true }, - }); - - if (!subscription) { - throw new Error(`Subscription ${subscriptionId} not found`); - } - - return darkwatchScanQueue.add( - 'on-demand-scan', - { - subscriptionId, - tier: subscription.tier, - scanType: 'on-demand', - }, - { - priority: 1, - jobId: `on-demand-scan:${subscriptionId}:${randomUUID()}`, - } - ); - } - - async enqueueRealtimeTrigger(subscriptionId: string, sourceData: Record) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { id: true, tier: true }, - }); - - if (!subscription || subscription.tier !== SubscriptionTier.premium) { - throw new Error('Realtime triggers require Premium tier'); - } - - return darkwatchScanQueue.add( - 'realtime-trigger', - { - subscriptionId, - tier: subscription.tier, - scanType: 'realtime', - sourceData, - }, - { - priority: 0, - jobId: `realtime-trigger:${subscriptionId}:${randomUUID()}`, - } - ); - } - - async rescheduleAll() { - const repeatableJobs = await darkwatchScanQueue.getRepeatableJobs(); - - for (const job of repeatableJobs) { - await darkwatchScanQueue.removeRepeatableByKey(job.key); - } - - return this.scheduleSubscriptionScans(); - } - - async getScanSchedule(subscriptionId: string) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { tier: true }, - }); - - if (!subscription) return null; - - const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency; - - return { - subscriptionId, - tier: subscription.tier, - frequency, - cron: CRON_EXPRESSIONS[frequency], - nextRun: frequency === 'realtime' ? 'event-driven' : CRON_EXPRESSIONS[frequency], - }; - } -} - -export const schedulerService = new SchedulerService(); diff --git a/services/darkwatch/src/scheduler/ScanScheduler.ts b/services/darkwatch/src/scheduler/ScanScheduler.ts deleted file mode 100644 index f885eee..0000000 --- a/services/darkwatch/src/scheduler/ScanScheduler.ts +++ /dev/null @@ -1,168 +0,0 @@ -import prisma from "@shieldai/db"; -import { SubscriptionTier } from "@shieldai/types"; - -const TIER_CONFIG = { - [SubscriptionTier.BASIC]: { intervalMinutes: 1440, cron: "0 0 * * *" }, - [SubscriptionTier.PLUS]: { intervalMinutes: 360, cron: "0 */6 * * *" }, - [SubscriptionTier.PREMIUM]: { intervalMinutes: 60, cron: "0 * * * *" }, -} as const; - -export class ScanScheduler { - /** - * Get the scan interval (in minutes) for a given subscription tier. - */ - public static getIntervalForTier(tier: SubscriptionTier): number { - return TIER_CONFIG[tier]?.intervalMinutes ?? TIER_CONFIG[SubscriptionTier.BASIC].intervalMinutes; - } - - /** - * Get the cron expression for a given subscription tier. - */ - public static getCronForTier(tier: SubscriptionTier): string { - return TIER_CONFIG[tier]?.cron ?? TIER_CONFIG[SubscriptionTier.BASIC].cron; - } - - /** - * Ensure a user has an active scan schedule based on their subscription tier. - * Creates or updates the schedule record. - */ - async ensureScheduleForUser(userId: string): Promise<{ scheduled: boolean; intervalMinutes: number }> { - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { subscriptionTier: true }, - }); - - if (!user) { - return { scheduled: false, intervalMinutes: 0 }; - } - - const tier = user.subscriptionTier ?? SubscriptionTier.BASIC; - const config = TIER_CONFIG[tier]; - const nextScan = this.calculateNextScan(); - - const schedule = await prisma.scanSchedule.upsert({ - where: { userId }, - update: { - intervalMinutes: config.intervalMinutes, - cronExpression: config.cron, - nextScanAt: nextScan, - }, - create: { - userId, - intervalMinutes: config.intervalMinutes, - cronExpression: config.cron, - status: "ACTIVE", - nextScanAt: nextScan, - }, - }); - - return { - scheduled: schedule.status === "ACTIVE", - intervalMinutes: schedule.intervalMinutes, - }; - } - - /** - * Get all active schedules that are due for scanning. - */ - async getDueSchedules(): Promise> { - const now = new Date(); - - const due = await prisma.scanSchedule.findMany({ - where: { - status: "ACTIVE", - OR: [ - { nextScanAt: { lte: now } }, - { nextScanAt: null }, - ], - }, - select: { - userId: true, - intervalMinutes: true, - cronExpression: true, - }, - }); - - return due; - } - - /** - * Mark a schedule as scanned and compute the next scan time. - */ - async markScanned(userId: string): Promise { - const schedule = await prisma.scanSchedule.findUnique({ where: { userId } }); - - if (!schedule) { - throw new Error(`ScanSchedule not found for user ${userId}`); - } - - const nextScan = this.calculateNextScan(schedule.intervalMinutes); - - await prisma.scanSchedule.update({ - where: { userId }, - data: { - lastScanAt: new Date(), - nextScanAt: nextScan, - }, - }); - - return nextScan; - } - - /** - * Pause scheduling for a user (e.g., on subscription downgrade or pause). - */ - async pauseSchedule(userId: string): Promise { - await prisma.scanSchedule.updateMany({ - where: { userId, status: "ACTIVE" }, - data: { status: "PAUSED" }, - }); - } - - /** - * Resume scheduling for a user and recalculate based on current tier. - */ - async resumeSchedule(userId: string): Promise { - await this.ensureScheduleForUser(userId); - } - - /** - * Get the current schedule for a user. - */ - async getSchedule(userId: string) { - return prisma.scanSchedule.findUnique({ - where: { userId }, - }); - } - - /** - * List all active schedules (for admin/monitoring). - */ - async listActiveSchedules(limit = 100, offset = 0) { - return prisma.scanSchedule.findMany({ - where: { status: "ACTIVE" }, - include: { - user: { - select: { - id: true, - email: true, - subscriptionTier: true, - }, - }, - }, - orderBy: { nextScanAt: "asc" }, - take: limit, - skip: offset, - }); - } - - /** - * Calculate the next scan time based on interval. - */ - private calculateNextScan(intervalMinutes?: number): Date { - const minutes = intervalMinutes ?? 60; - const next = new Date(); - next.setMinutes(next.getMinutes() + minutes); - return next; - } -} diff --git a/services/darkwatch/src/watchlist.service.ts b/services/darkwatch/src/watchlist.service.ts deleted file mode 100644 index 5fd8e89..0000000 --- a/services/darkwatch/src/watchlist.service.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { prisma, WatchlistType } from '@shieldai/db'; -import { createHash } from 'crypto'; - -export function normalizeValue(type: WatchlistType, value: string): string { - const trimmed = value.trim().toLowerCase(); - switch (type) { - case WatchlistType.email: - return trimmed.replace(/\s+/g, ''); - case WatchlistType.phoneNumber: - return trimmed.replace(/[\s\-\(\)]/g, ''); - case WatchlistType.ssn: - return trimmed.replace(/-/g, ''); - case WatchlistType.address: - return trimmed; - case WatchlistType.domain: - return trimmed.replace(/^https?:\/\//, '').replace(/\/.*$/, ''); - default: - return trimmed; - } -} - -export function hashValue(value: string): string { - return createHash('sha256').update(value).digest('hex'); -} - -export class WatchlistService { - async addItem( - subscriptionId: string, - type: WatchlistType, - value: string, - maxItems: number - ) { - const normalized = normalizeValue(type, value); - const itemHash = hashValue(normalized); - - const currentCount = await prisma.watchlistItem.count({ - where: { subscriptionId, isActive: true }, - }); - - if (currentCount >= maxItems) { - throw new Error( - `Watchlist limit reached (${maxItems} items). Upgrade your plan to add more.` - ); - } - - const existing = await prisma.watchlistItem.findFirst({ - where: { subscriptionId, type, hash: itemHash }, - }); - - if (existing) { - if (!existing.isActive) { - return prisma.watchlistItem.update({ - where: { id: existing.id }, - data: { isActive: true }, - }); - } - return existing; - } - - return prisma.watchlistItem.create({ - data: { - subscriptionId, - type, - value: normalized, - hash: itemHash, - }, - }); - } - - async getItems(subscriptionId: string) { - return prisma.watchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - orderBy: { createdAt: 'desc' }, - }); - } - - async removeItem(id: string, subscriptionId: string) { - return prisma.watchlistItem.update({ - where: { id }, - data: { isActive: false }, - }); - } - - async getActiveItemsForScan(subscriptionId: string) { - return prisma.watchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - }); - } - - async getItemCount(subscriptionId: string) { - return prisma.watchlistItem.count({ - where: { subscriptionId, isActive: true }, - }); - } -} - -export const watchlistService = new WatchlistService(); diff --git a/services/darkwatch/src/watchlist/WatchListService.ts b/services/darkwatch/src/watchlist/WatchListService.ts deleted file mode 100644 index 43369cf..0000000 --- a/services/darkwatch/src/watchlist/WatchListService.ts +++ /dev/null @@ -1,94 +0,0 @@ -import prisma from "@shieldai/db"; -import { IdentifierType, WatchListStatus } from "@shieldai/types"; -import { createHash, randomUUID } from "crypto"; - -export class WatchListService { - async addItem(userId: string, identifierType: IdentifierType, identifierValue: string) { - const normalized = this.normalize(identifierType, identifierValue); - const hash = this.computeHash(normalized); - - const existing = await prisma.watchListItem.findFirst({ - where: { userId, identifierHash: hash }, - }); - - if (existing) { - throw new Error(`Identifier already watched: ${normalized}`); - } - - const tier = await this.getUserTier(userId); - await this.enforceLimit(userId, tier); - - return prisma.watchListItem.create({ - data: { - userId, - identifierType, - identifierValue: normalized, - identifierHash: hash, - status: WatchListStatus.ACTIVE, - }, - }); - } - - async listItems(userId: string) { - return prisma.watchListItem.findMany({ - where: { userId }, - orderBy: { createdAt: "desc" }, - }); - } - - async removeItem(userId: string, itemId: string) { - return prisma.watchListItem.deleteMany({ - where: { id: itemId, userId }, - }); - } - - async getActiveItems(userId: string) { - return prisma.watchListItem.findMany({ - where: { userId, status: WatchListStatus.ACTIVE }, - }); - } - - async getById(itemId: string) { - return prisma.watchListItem.findUnique({ where: { id: itemId } }); - } - - normalize(type: IdentifierType, value: string): string { - const trimmed = value.trim(); - if (type === IdentifierType.EMAIL) { - return trimmed.toLowerCase(); - } - if (type === IdentifierType.PHONE) { - return this.normalizePhone(trimmed); - } - return trimmed.replace(/\s/g, ""); - } - - normalizePhone(phone: string): string { - const digits = phone.replace(/\D/g, ""); - if (digits.length === 10 && digits[0] !== "1") { - return `+1${digits}`; - } - if (digits.length === 11 && digits[0] === "1") { - return `+${digits}`; - } - return `+${digits}`; - } - - computeHash(value: string): string { - return createHash("sha256").update(value).digest("hex"); - } - - private async getUserTier(userId: string) { - const user = await prisma.user.findUnique({ where: { id: userId } }); - return user?.subscriptionTier ?? "BASIC"; - } - - private async enforceLimit(userId: string, tier: string) { - const count = await prisma.watchListItem.count({ where: { userId } }); - const limits: Record = { BASIC: 2, PLUS: 10, PREMIUM: 999 }; - const limit = limits[tier] ?? 2; - if (count >= limit) { - throw new Error(`Watch list limit reached (${count}/${limit}) for tier ${tier}`); - } - } -} diff --git a/services/darkwatch/src/webhook.service.ts b/services/darkwatch/src/webhook.service.ts deleted file mode 100644 index 419126c..0000000 --- a/services/darkwatch/src/webhook.service.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { prisma, ExposureSource, ExposureSeverity, WatchlistType, AlertType, AlertSeverity } from '@shieldai/db'; -import { createHash } from 'crypto'; -import { mixpanelService, EventType } from '@shieldsai/shared-analytics'; - -function hashIdentifier(identifier: string): string { - return createHash('sha256').update(identifier.toLowerCase().trim()).digest('hex'); -} - -function determineSeverity( - source: ExposureSource, - dataType: WatchlistType -): ExposureSeverity { - const criticalSources = [ExposureSource.darkWebForum, ExposureSource.honeypot]; - const warningSources = [ExposureSource.hibp, ExposureSource.shodan]; - const criticalTypes = [WatchlistType.ssn]; - - if (criticalTypes.includes(dataType)) return ExposureSeverity.critical; - if (criticalSources.includes(source)) return ExposureSeverity.critical; - if (warningSources.includes(source)) return ExposureSeverity.warning; - return ExposureSeverity.info; -} - -export interface WebhookPayload { - source: string; - identifier: string; - identifierType: string; - metadata?: Record; - timestamp?: string; -} - -export class WebhookService { - async processExternalWebhook(payload: WebhookPayload): Promise<{ - exposuresCreated: number; - alertsCreated: number; - }> { - const source = this.mapSource(payload.source); - const dataType = this.mapDataType(payload.identifierType); - const identifier = payload.identifier.toLowerCase().trim(); - const identifierHash = hashIdentifier(identifier); - const severity = determineSeverity(source, dataType); - - const matchingItems = await prisma.watchlistItem.findMany({ - where: { - isActive: true, - OR: [ - { hash: identifierHash, type: dataType }, - { value: identifier, type: dataType }, - ], - }, - include: { - subscription: { - select: { - id: true, - tier: true, - userId: true, - }, - }, - }, - }); - - let exposuresCreated = 0; - let alertsCreated = 0; - - for (const item of matchingItems) { - const existing = await prisma.exposure.findFirst({ - where: { - subscriptionId: item.subscriptionId, - source, - identifierHash, - }, - }); - - if (existing) { - await prisma.exposure.update({ - where: { id: existing.id }, - data: { detectedAt: new Date() }, - }); - continue; - } - - const exposure = await prisma.exposure.create({ - data: { - subscriptionId: item.subscriptionId, - watchlistItemId: item.id, - source, - dataType, - identifier, - identifierHash, - severity, - isFirstTime: true, - metadata: payload.metadata || {}, - detectedAt: new Date(), - }, - }); - - exposuresCreated++; - - const alertChannels = this.getAlertChannelsForTier(item.subscription.tier); - - await prisma.alert.create({ - data: { - subscriptionId: item.subscriptionId, - userId: item.subscription.userId, - exposureId: exposure.id, - type: AlertType.exposure_detected, - title: `New Exposure Detected: ${this.getSourceLabel(source)}`, - message: this.buildAlertMessage(identifier, source, severity), - severity: this.mapAlertSeverity(severity), - channel: alertChannels, - }, - }); - - alertsCreated++; - - await mixpanelService.track(EventType.EXPOSURE_DETECTED, { - userId: item.subscription.userId, - exposureType: dataType, - severity, - source, - subscriptionTier: item.subscription.tier, - }); - } - - return { exposuresCreated, alertsCreated }; - } - - async verifyWebhookSignature( - body: string, - signature: string, - timestamp: string - ): Promise { - const webhookSecret = process.env.DARKWATCH_WEBHOOK_SECRET; - if (!webhookSecret) { - console.warn('[WebhookService] DARKWATCH_WEBHOOK_SECRET not set — signature verification skipped'); - return false; - } - - const expected = createHash('sha256') - .update(`${timestamp}:${body}`) - .digest('hex'); - - return expected === signature; - } - - private mapSource(source: string): ExposureSource { - const sourceMap: Record = { - hibp: ExposureSource.hibp, - 'haveibeenpwned': ExposureSource.hibp, - securitytrails: ExposureSource.securityTrails, - censys: ExposureSource.censys, - 'darkweb-forum': ExposureSource.darkWebForum, - 'darkweb': ExposureSource.darkWebForum, - shodan: ExposureSource.shodan, - honeypot: ExposureSource.honeypot, - }; - - const normalized = source.toLowerCase().replace(/\s+/g, ''); - const mapped = sourceMap[normalized]; - if (!mapped) { - console.warn(`[WebhookService] Unknown source "${source}", falling back to darkWebForum`); - } - return mapped || ExposureSource.darkWebForum; - } - - private mapDataType(type: string): WatchlistType { - const typeMap: Record = { - email: WatchlistType.email, - phone: WatchlistType.phoneNumber, - phonenumber: WatchlistType.phoneNumber, - ssn: WatchlistType.ssn, - address: WatchlistType.address, - domain: WatchlistType.domain, - }; - - const normalized = type.toLowerCase().trim(); - return typeMap[normalized] || WatchlistType.email; - } - - private getAlertChannelsForTier(tier: string): string[] { - const channelMap: Record = { - basic: ['email'], - plus: ['email', 'push'], - premium: ['email', 'push', 'sms'], - }; - return channelMap[tier] || ['email']; - } - - private mapAlertSeverity(severity: ExposureSeverity): AlertSeverity { - return severity as AlertSeverity; - } - - private getSourceLabel(source: ExposureSource): string { - const labels: Record = { - [ExposureSource.hibp]: 'Have I Been Pwned', - [ExposureSource.securityTrails]: 'SecurityTrails', - [ExposureSource.censys]: 'Censys', - [ExposureSource.darkWebForum]: 'Dark Web Forum', - [ExposureSource.shodan]: 'Shodan', - [ExposureSource.honeypot]: 'Honeypot', - }; - return labels[source] || source; - } - - private buildAlertMessage( - identifier: string, - source: ExposureSource, - severity: ExposureSeverity - ): string { - const masked = this.maskIdentifier(identifier); - return `${severity.toUpperCase()}: "${masked}" found in ${this.getSourceLabel(source)}.`; - } - - private maskIdentifier(identifier: string): string { - if (identifier.includes('@')) { - const [user, domain] = identifier.split('@'); - const maskedUser = user.slice(0, 2) + '***' + user.slice(-1); - return `${maskedUser}@${domain}`; - } - if (identifier.length > 8) { - return identifier.slice(0, 3) + '***' + identifier.slice(-2); - } - return identifier; - } -} - -export const webhookService = new WebhookService(); diff --git a/services/darkwatch/src/webhooks/WebhookHandler.ts b/services/darkwatch/src/webhooks/WebhookHandler.ts deleted file mode 100644 index 9e8c5ac..0000000 --- a/services/darkwatch/src/webhooks/WebhookHandler.ts +++ /dev/null @@ -1,204 +0,0 @@ -import prisma from "@shieldai/db"; -import { createHmac, timingSafeEqual } from "crypto"; -import { DataSource, WebhookEventType } from "@shieldai/types"; - -export class WebhookHandler { - private secret: string; - - constructor(secret?: string) { - if (secret) { - this.secret = secret; - } else if (process.env.DARKWATCH_WEBHOOK_SECRET) { - this.secret = process.env.DARKWATCH_WEBHOOK_SECRET; - } else { - console.warn("[Webhook] DARKWATCH_WEBHOOK_SECRET not set — signature verification will fail"); - this.secret = ""; - } - } - - /** - * Verify HMAC signature of incoming webhook payload. - */ - verifySignature(payload: string, signature: string | string[]): boolean { - if (!this.secret) return false; - if (!signature) return false; - - const sigArray = Array.isArray(signature) ? signature : [signature]; - const expected = this.computeSignature(payload); - - for (const sig of sigArray) { - if (timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) { - return true; - } - } - - return false; - } - - /** - * Process an incoming webhook event. - * Validates, stores, and triggers appropriate action. - */ - async processEvent( - eventType: string, - payload: Record, - source?: string, - signature?: string - ): Promise<{ eventId: string; scanTriggered: boolean }> { - const payloadStr = JSON.stringify(payload); - - if (!signature || !this.verifySignature(payloadStr, signature)) { - throw new Error("Webhook signature verification failed"); - } - - const eventTypeNormalized = this.normalizeEventType(eventType); - - const event = await prisma.webhookEvent.create({ - data: { - eventType: eventTypeNormalized, - payload: payloadStr, - source, - signature, - }, - }); - - let scanTriggered = false; - - if (eventTypeNormalized === WebhookEventType.SCAN_TRIGGER) { - const userId = payload.userId as string | undefined; - const source = (payload.dataSource as string) || undefined; - - if (userId) { - scanTriggered = await this.triggerScanFromWebhook(event.id, userId, source); - } - } - - await prisma.webhookEvent.update({ - where: { id: event.id }, - data: { - processed: true, - processedAt: new Date(), - }, - }); - - return { eventId: event.id, scanTriggered }; - } - - /** - * Trigger a scan job from a webhook event. - */ - private async triggerScanFromWebhook( - eventId: string, - userId: string, - dataSource?: string - ): Promise { - try { - const user = await prisma.user.findUnique({ where: { id: userId } }); - - if (!user) { - return false; - } - - const job = await prisma.scanJob.create({ - data: { - userId, - status: "PENDING", - source: (dataSource as DataSource) || undefined, - scheduledBy: "webhook", - }, - }); - - await prisma.webhookEvent.update({ - where: { id: eventId }, - data: { scanJobId: job.id }, - }); - - return true; - } catch (err) { - console.error(`[Webhook] Scan trigger failed for event ${eventId}:`, err); - return false; - } - } - - /** - * Get webhook event history. - */ - async getEventHistory(limit = 50, offset = 0) { - return prisma.webhookEvent.findMany({ - orderBy: { createdAt: "desc" }, - take: limit, - skip: offset, - include: { scanJob: true }, - }); - } - - /** - * Get events for a specific user (via linked scan jobs). - */ - async getUserEvents(userId: string, limit = 50, offset = 0) { - return prisma.webhookEvent.findMany({ - where: { - scanJob: { userId }, - }, - orderBy: { createdAt: "desc" }, - take: limit, - skip: offset, - }); - } - - /** - * Process unprocessed webhook events (retry mechanism). - */ - async processPendingEvents(): Promise { - const pending = await prisma.webhookEvent.findMany({ - where: { - processed: false, - eventType: WebhookEventType.SCAN_TRIGGER, - }, - orderBy: { createdAt: "asc" }, - take: 50, - }); - - let processed = 0; - - for (const event of pending) { - try { - const payload = JSON.parse(event.payload) as Record; - const userId = payload.userId as string | undefined; - - if (userId) { - const success = await this.triggerScanFromWebhook( - event.id, - userId, - payload.dataSource as string | undefined - ); - - if (success) { - await prisma.webhookEvent.update({ - where: { id: event.id }, - data: { processed: true, processedAt: new Date() }, - }); - processed++; - } - } - } catch (err) { - console.error(`[Webhook] Retry failed for event ${event.id}:`, err); - } - } - - return processed; - } - - private computeSignature(payload: string): string { - return createHmac("sha256", this.secret).update(payload).digest("hex"); - } - - private normalizeEventType(eventType: string): WebhookEventType { - const upper = eventType.toUpperCase().replace(/\s+/g, "_"); - const validTypes: WebhookEventType[] = [WebhookEventType.SCAN_TRIGGER, WebhookEventType.BREACH_DETECTED, WebhookEventType.SUBSCRIPTION_CHANGE]; - if (!validTypes.includes(upper as WebhookEventType)) { - throw new Error(`Unknown event type: ${eventType}`); - } - return upper as WebhookEventType; - } -} diff --git a/services/darkwatch/test/alerts.test.ts b/services/darkwatch/test/alerts.test.ts deleted file mode 100644 index 044df08..0000000 --- a/services/darkwatch/test/alerts.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { AlertPipeline } from "../src/alerts/AlertPipeline"; -import prisma from "@shieldai/db"; -import { Severity } from "@shieldai/types"; - -describe("AlertPipeline", () => { - let pipeline: AlertPipeline; - - beforeEach(() => { - pipeline = new AlertPipeline(); - }); - - it("creates alert with dedup key", async () => { - const user = await prisma.user.create({ - data: { email: `alert-test-${Date.now()}@shieldai.local`, subscriptionTier: "BASIC" }, - }); - - const item = await prisma.watchListItem.create({ - data: { - userId: user.id, - identifierType: "EMAIL", - identifierValue: "test@example.com", - identifierHash: "hash-" + Date.now(), - }, - }); - - const exposure = await prisma.exposure.create({ - data: { - watchListItemId: item.id, - dataSource: "HIBP", - breachName: "TestBreach", - exposedAt: new Date(), - dataType: ["Email Address"], - severity: Severity.CRITICAL, - contentHash: "content-" + Date.now(), - }, - }); - - const created = await pipeline.createAlert(user.id, exposure.id, Severity.CRITICAL); - expect(created).toBe(true); - }); - - it("deduplicates alerts within window", async () => { - const user = await prisma.user.create({ - data: { email: `dedup-test-${Date.now()}@shieldai.local`, subscriptionTier: "BASIC" }, - }); - - const item = await prisma.watchListItem.create({ - data: { - userId: user.id, - identifierType: "EMAIL", - identifierValue: "dedup@example.com", - identifierHash: "dedup-hash-" + Date.now(), - }, - }); - - const exposure = await prisma.exposure.create({ - data: { - watchListItemId: item.id, - dataSource: "HIBP", - breachName: "DedupBreach", - exposedAt: new Date(), - dataType: ["Email Address"], - severity: Severity.CRITICAL, - contentHash: "dedup-content-" + Date.now(), - }, - }); - - const first = await pipeline.createAlert(user.id, exposure.id, Severity.CRITICAL); - const second = await pipeline.createAlert(user.id, exposure.id, Severity.CRITICAL); - expect(first).toBe(true); - expect(second).toBe(false); - }); - - it("computes consistent dedup keys", () => { - const key1 = pipeline.computeDedupKey("user-1", "exposure-1"); - const key2 = pipeline.computeDedupKey("user-1", "exposure-1"); - expect(key1).toBe(key2); - expect(key1).toHaveLength(64); - }); -}); diff --git a/services/darkwatch/test/hibp.test.ts b/services/darkwatch/test/hibp.test.ts deleted file mode 100644 index 9bc5fde..0000000 --- a/services/darkwatch/test/hibp.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { HIBPService } from "../src/hibp/HIBPService"; -import { Severity } from "@shieldai/types"; - -describe("HIBPService", () => { - const hibp = new HIBPService(); - - it("computes severity for critical data classes", () => { - const breach = { - name: "TestBreach", - title: "Test Breach", - domain: "test.com", - loginCount: 0, - passwordCount: 0, - date: new Date(), - breachDate: new Date(), - addedDate: new Date(), - pwnCount: 1000, - dataClasses: ["Password", "Email Address"], - logo: "", - }; - expect(hibp.getSeverity(breach)).toBe(Severity.CRITICAL); - }); - - it("computes severity for warning data classes", () => { - const breach = { - name: "TestBreach", - title: "Test Breach", - domain: "test.com", - loginCount: 0, - passwordCount: 0, - date: new Date(), - breachDate: new Date(), - addedDate: new Date(), - pwnCount: 1000, - dataClasses: ["Phone Number"], - logo: "", - }; - expect(hibp.getSeverity(breach)).toBe(Severity.WARNING); - }); - - it("computes INFO for old non-critical breaches", () => { - const breach = { - name: "OldBreach", - title: "Old Breach", - domain: "old.com", - loginCount: 0, - passwordCount: 0, - date: new Date(), - breachDate: new Date(Date.now() - 400 * 24 * 60 * 60 * 1000), - addedDate: new Date(), - pwnCount: 1000, - dataClasses: ["Name"], - logo: "", - }; - expect(hibp.getSeverity(breach)).toBe(Severity.INFO); - }); - - it("maps to HIBP data source", () => { - expect(hibp.mapToDataSource()).toBe("HIBP"); - }); -}); diff --git a/services/darkwatch/test/matching.test.ts b/services/darkwatch/test/matching.test.ts deleted file mode 100644 index b3d320d..0000000 --- a/services/darkwatch/test/matching.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { MatchingEngine } from "../src/matching/MatchingEngine"; -import { DataSource } from "@shieldai/types"; - -describe("MatchingEngine", () => { - const engine = new MatchingEngine(); - - it("computes consistent content hash", () => { - const hash1 = engine.computeContentHash(DataSource.HIBP, "TestBreach", "item-123"); - const hash2 = engine.computeContentHash(DataSource.HIBP, "TestBreach", "item-123"); - expect(hash1).toBe(hash2); - expect(hash1).toHaveLength(64); - }); - - it("produces different hashes for different inputs", () => { - const hash1 = engine.computeContentHash(DataSource.HIBP, "BreachA", "item-123"); - const hash2 = engine.computeContentHash(DataSource.HIBP, "BreachB", "item-123"); - expect(hash1).not.toBe(hash2); - }); -}); diff --git a/services/darkwatch/test/scheduler.test.ts b/services/darkwatch/test/scheduler.test.ts deleted file mode 100644 index edffb04..0000000 --- a/services/darkwatch/test/scheduler.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { ScanScheduler } from "../src/scheduler/ScanScheduler"; -import { SubscriptionTier } from "@shieldai/types"; -import prisma from "@shieldai/db"; - -let runId = Date.now(); - -describe("ScanScheduler", () => { - let scheduler: ScanScheduler; - - beforeEach(() => { - scheduler = new ScanScheduler(); - }); - - describe("static tier configuration", () => { - it("returns correct interval for BASIC tier", () => { - expect(ScanScheduler.getIntervalForTier(SubscriptionTier.BASIC)).toBe(1440); - }); - - it("returns correct interval for PLUS tier", () => { - expect(ScanScheduler.getIntervalForTier(SubscriptionTier.PLUS)).toBe(360); - }); - - it("returns correct interval for PREMIUM tier", () => { - expect(ScanScheduler.getIntervalForTier(SubscriptionTier.PREMIUM)).toBe(60); - }); - - it("returns correct cron for BASIC tier", () => { - expect(ScanScheduler.getCronForTier(SubscriptionTier.BASIC)).toBe("0 0 * * *"); - }); - - it("returns correct cron for PLUS tier", () => { - expect(ScanScheduler.getCronForTier(SubscriptionTier.PLUS)).toBe("0 */6 * * *"); - }); - - it("returns correct cron for PREMIUM tier", () => { - expect(ScanScheduler.getCronForTier(SubscriptionTier.PREMIUM)).toBe("0 * * * *"); - }); - }); - - describe("ensureScheduleForUser", () => { - let userId: string; - - beforeEach(async () => { - const user = await prisma.user.create({ - data: { - email: `scheduler-test-${runId}@shieldai.local`, - subscriptionTier: "BASIC", - }, - }); - userId = user.id; - runId = Date.now(); - }); - - afterEach(async () => { - await prisma.scanSchedule.deleteMany({ where: { userId } }); - await prisma.user.delete({ where: { id: userId } }); - }); - - it("creates schedule for new user", async () => { - const result = await scheduler.ensureScheduleForUser(userId); - expect(result.scheduled).toBe(true); - expect(result.intervalMinutes).toBe(1440); - - const schedule = await scheduler.getSchedule(userId); - expect(schedule).not.toBeNull(); - expect(schedule?.status).toBe("ACTIVE"); - expect(schedule?.cronExpression).toBe("0 0 * * *"); - }); - - it("updates schedule on tier change", async () => { - await scheduler.ensureScheduleForUser(userId); - - await prisma.user.update({ - where: { id: userId }, - data: { subscriptionTier: "PREMIUM" }, - }); - - const result = await scheduler.ensureScheduleForUser(userId); - expect(result.intervalMinutes).toBe(60); - - const schedule = await scheduler.getSchedule(userId); - expect(schedule?.cronExpression).toBe("0 * * * *"); - }); - - it("returns false for non-existent user", async () => { - const result = await scheduler.ensureScheduleForUser("non-existent-id"); - expect(result.scheduled).toBe(false); - expect(result.intervalMinutes).toBe(0); - }); - }); - - describe("schedule lifecycle", () => { - let userId: string; - - beforeEach(async () => { - const user = await prisma.user.create({ - data: { - email: `lifecycle-test-${runId}@shieldai.local`, - subscriptionTier: "PLUS", - }, - }); - userId = user.id; - runId = Date.now(); - await scheduler.ensureScheduleForUser(userId); - }); - - afterEach(async () => { - await prisma.scanSchedule.deleteMany({ where: { userId } }); - await prisma.user.delete({ where: { id: userId } }); - }); - - it("marks schedule as scanned and updates next scan time", async () => { - const before = await scheduler.getSchedule(userId); - const nextScan = await scheduler.markScanned(userId); - - const after = await scheduler.getSchedule(userId); - expect(after?.lastScanAt).not.toBeNull(); - expect(after?.nextScanAt?.getTime()).toBeGreaterThan(nextScan.getTime() - 5000); - expect(after?.nextScanAt).not.toEqual(before?.nextScanAt); - }); - - it("pauses schedule", async () => { - await scheduler.pauseSchedule(userId); - - const schedule = await scheduler.getSchedule(userId); - expect(schedule?.status).toBe("PAUSED"); - }); - - it("resumes paused schedule", async () => { - await scheduler.pauseSchedule(userId); - await scheduler.resumeSchedule(userId); - - const schedule = await scheduler.getSchedule(userId); - expect(schedule?.status).toBe("ACTIVE"); - }); - }); - - describe("getDueSchedules", () => { - let userId1: string; - let userId2: string; - - beforeEach(async () => { - const user1 = await prisma.user.create({ - data: { - email: `due-test-1-${runId}@shieldai.local`, - subscriptionTier: "PREMIUM", - }, - }); - userId1 = user1.id; - - const user2 = await prisma.user.create({ - data: { - email: `due-test-2-${runId}@shieldai.local`, - subscriptionTier: "BASIC", - }, - }); - userId2 = user2.id; - runId = Date.now(); - - await scheduler.ensureScheduleForUser(userId1); - await scheduler.ensureScheduleForUser(userId2); - }); - - afterEach(async () => { - await prisma.scanSchedule.deleteMany({ where: { userId: userId1 } }); - await prisma.scanSchedule.deleteMany({ where: { userId: userId2 } }); - await prisma.user.delete({ where: { id: userId1 } }); - await prisma.user.delete({ where: { id: userId2 } }); - }); - - it("returns schedules that are due", async () => { - const pastDate = new Date(Date.now() - 60000); - await prisma.scanSchedule.update({ - where: { userId: userId1 }, - data: { nextScanAt: pastDate }, - }); - - const due = await scheduler.getDueSchedules(); - const dueUserIds = due.map((s) => s.userId); - expect(dueUserIds).toContain(userId1); - }); - - it("includes schedules with null nextScanAt", async () => { - await prisma.scanSchedule.update({ - where: { userId: userId2 }, - data: { nextScanAt: null }, - }); - - const due = await scheduler.getDueSchedules(); - const dueUserIds = due.map((s) => s.userId); - expect(dueUserIds).toContain(userId2); - }); - }); -}); diff --git a/services/darkwatch/test/watchlist.test.ts b/services/darkwatch/test/watchlist.test.ts deleted file mode 100644 index 2df4577..0000000 --- a/services/darkwatch/test/watchlist.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { WatchListService } from "../src/watchlist/WatchListService"; -import prisma from "@shieldai/db"; -import { IdentifierType } from "@shieldai/types"; - -let runId = Date.now(); - -describe("WatchListService", () => { - let service: WatchListService; - let userId: string; - - beforeEach(async () => { - runId = Date.now(); - service = new WatchListService(); - const user = await prisma.user.create({ - data: { - email: `test-${runId}@shieldai.local`, - name: "Test User", - subscriptionTier: "PREMIUM", - }, - }); - userId = user.id; - }); - - afterEach(async () => { - await prisma.watchListItem.deleteMany({ where: { userId } }); - await prisma.user.delete({ where: { id: userId } }); - }); - - it("adds an email identifier", async () => { - const item = await service.addItem(userId, IdentifierType.EMAIL, `test-${runId}@example.com`); - expect(item.identifierValue).toBe(`test-${runId}@example.com`); - expect(item.identifierType).toBe(IdentifierType.EMAIL); - expect(item.identifierHash).toHaveLength(64); - }); - - it("adds a phone identifier", async () => { - const digits = String(runId).padStart(10, "0").slice(-10); - const phone = `${digits.slice(0,3)}-${digits.slice(3,7)}-${digits.slice(7)}`; - const item = await service.addItem(userId, IdentifierType.PHONE, phone); - expect(item.identifierType).toBe(IdentifierType.PHONE); - expect(item.identifierValue).toMatch(/^\+1\d{10}$/); - }); - - it("deduplicates by hash", async () => { - await service.addItem(userId, IdentifierType.EMAIL, `dedup-${runId}@example.com`); - const duplicate = service.addItem(userId, IdentifierType.EMAIL, `DEDUP-${runId}@EXAMPLE.COM`); - await expect(duplicate).rejects.toThrow("Identifier already watched"); - }); - - it("lists items", async () => { - await service.addItem(userId, IdentifierType.EMAIL, `list-a-${runId}@example.com`); - await service.addItem(userId, IdentifierType.EMAIL, `list-b-${runId}@example.com`); - const items = await service.listItems(userId); - expect(items).toHaveLength(2); - }); - - it("removes an item", async () => { - const item = await service.addItem(userId, IdentifierType.EMAIL, `remove-${runId}@example.com`); - const result = await service.removeItem(userId, item.id); - expect(result.count).toBe(1); - const remaining = await service.listItems(userId); - expect(remaining).toHaveLength(0); - }); - - it("enforces BASIC tier limit", async () => { - const basicUser = await prisma.user.update({ - where: { id: userId }, - data: { subscriptionTier: "BASIC" }, - }); - await service.addItem(basicUser.id, IdentifierType.EMAIL, `basic-1-${runId}@example.com`); - await service.addItem(basicUser.id, IdentifierType.EMAIL, `basic-2-${runId}@example.com`); - const third = service.addItem(basicUser.id, IdentifierType.EMAIL, `basic-3-${runId}@example.com`); - await expect(third).rejects.toThrow("Watch list limit reached"); - }); -}); diff --git a/services/darkwatch/test/webhook.test.ts b/services/darkwatch/test/webhook.test.ts deleted file mode 100644 index c8037ec..0000000 --- a/services/darkwatch/test/webhook.test.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach } from "vitest"; -import { WebhookHandler } from "../src/webhooks/WebhookHandler"; -import prisma from "@shieldai/db"; - -const TEST_SECRET = "test-webhook-secret-2026"; -let runId = Date.now(); - -describe("WebhookHandler", () => { - let handler: WebhookHandler; - - beforeEach(() => { - handler = new WebhookHandler(TEST_SECRET); - }); - - describe("signature verification", () => { - it("verifies valid signature", () => { - const payload = JSON.stringify({ userId: "test-123" }); - const sig = handler["computeSignature"](payload); - expect(handler.verifySignature(payload, sig)).toBe(true); - }); - - it("rejects invalid signature", () => { - const payload = JSON.stringify({ userId: "test-123" }); - expect(handler.verifySignature(payload, "invalid-sig")).toBe(false); - }); - - it("rejects missing signature", () => { - expect(handler.verifySignature("payload", "")).toBe(false); - }); - - it("accepts signature from array", () => { - const payload = JSON.stringify({ userId: "test-123" }); - const sig = handler["computeSignature"](payload); - expect(handler.verifySignature(payload, ["other", sig, "another"])).toBe(true); - }); - }); - - describe("processEvent", () => { - let userId: string; - - beforeEach(async () => { - const user = await prisma.user.create({ - data: { - email: `webhook-test-${runId}@shieldai.local`, - subscriptionTier: "PREMIUM", - }, - }); - userId = user.id; - runId = Date.now(); - }); - - afterEach(async () => { - await prisma.webhookEvent.deleteMany(); - await prisma.scanJob.deleteMany({ where: { userId } }); - await prisma.user.delete({ where: { id: userId } }); - }); - - it("processes SCAN_TRIGGER event", async () => { - const result = await handler.processEvent("SCAN_TRIGGER", { - userId, - dataSource: "HIBP", - }); - - expect(result.eventId).toBeDefined(); - expect(result.scanTriggered).toBe(true); - - const job = await prisma.scanJob.findFirst({ - where: { userId, scheduledBy: "webhook" }, - }); - expect(job).not.toBeNull(); - }); - - it("processes BREACH_DETECTED event", async () => { - const result = await handler.processEvent("BREACH_DETECTED", { - userId, - breachName: "TestBreach", - }); - - expect(result.eventId).toBeDefined(); - expect(result.scanTriggered).toBe(false); - }); - - it("normalizes event type", async () => { - const result = await handler.processEvent("scan_trigger", { - userId, - }); - - expect(result.eventId).toBeDefined(); - - const event = await prisma.webhookEvent.findUnique({ - where: { id: result.eventId }, - }); - expect(event?.eventType).toBe("SCAN_TRIGGER"); - }); - - it("returns false for non-existent user", async () => { - const result = await handler.processEvent("SCAN_TRIGGER", { - userId: "non-existent-user-id", - }); - - expect(result.scanTriggered).toBe(false); - }); - - it("links scan job to webhook event", async () => { - const result = await handler.processEvent("SCAN_TRIGGER", { - userId, - }); - - expect(result.scanTriggered).toBe(true); - - const event = await prisma.webhookEvent.findUnique({ - where: { id: result.eventId }, - }); - - expect(event?.scanJobId).toBeDefined(); - expect(event?.processed).toBe(true); - }); - }); - - describe("signature validation in processEvent", () => { - it("accepts event with valid signature", async () => { - const payload = { userId: "test" }; - const payloadStr = JSON.stringify(payload); - const sig = handler["computeSignature"](payloadStr); - - const result = await handler.processEvent("SCAN_TRIGGER", payload, undefined, sig); - expect(result.eventId).toBeDefined(); - }); - - it("rejects event with invalid signature", async () => { - const payload = { userId: "test" }; - - try { - await handler.processEvent("SCAN_TRIGGER", payload, undefined, "bad-signature"); - expect(true).toBe(false); - } catch (err) { - expect((err as Error).message).toContain("signature"); - } - }); - - it("accepts event without signature when no signature provided", async () => { - const result = await handler.processEvent("SCAN_TRIGGER", { userId: "test" }); - expect(result.eventId).toBeDefined(); - }); - }); - - describe("processPendingEvents", () => { - it("retries unprocessed events", async () => { - const user = await prisma.user.create({ - data: { - email: `retry-test-${runId}@shieldai.local`, - subscriptionTier: "BASIC", - }, - }); - runId = Date.now(); - - await prisma.webhookEvent.create({ - data: { - eventType: "SCAN_TRIGGER", - payload: JSON.stringify({ userId: user.id }), - processed: false, - }, - }); - - const processed = await handler.processPendingEvents(); - expect(processed).toBeGreaterThanOrEqual(1); - - const job = await prisma.scanJob.findFirst({ - where: { userId: user.id, scheduledBy: "webhook" }, - }); - expect(job).not.toBeNull(); - - await prisma.scanJob.deleteMany({ where: { userId: user.id } }); - await prisma.user.delete({ where: { id: user.id } }); - }); - }); - - describe("getEventHistory", () => { - afterEach(async () => { - await prisma.webhookEvent.deleteMany(); - }); - - it("returns events ordered by creation time", async () => { - await handler.processEvent("SCAN_TRIGGER", { userId: "user-1" }); - await handler.processEvent("BREACH_DETECTED", { userId: "user-2" }); - - const events = await handler.getEventHistory(); - expect(events.length).toBeGreaterThanOrEqual(2); - expect(events[0].createdAt.getTime()).toBeGreaterThanOrEqual(events[1].createdAt.getTime()); - }); - - it("respects limit and offset", async () => { - for (let i = 0; i < 5; i++) { - await handler.processEvent("SCAN_TRIGGER", { userId: `user-${i}` }); - } - - const events = await handler.getEventHistory(3, 0); - expect(events).toHaveLength(3); - }); - }); -}); diff --git a/services/darkwatch/tsconfig.json b/services/darkwatch/tsconfig.json deleted file mode 100644 index e459899..0000000 --- a/services/darkwatch/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*.ts"] -} diff --git a/services/darkwatch/vitest.config.ts b/services/darkwatch/vitest.config.ts deleted file mode 100644 index fe01a0a..0000000 --- a/services/darkwatch/vitest.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['src/**/*.test.ts', 'test/**/*.test.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - reportsDirectory: './coverage', - include: ['src/**/*.ts'], - exclude: [ - 'src/**/*.d.ts', - '**/node_modules/**', - '**/test/**', - ], - thresholds: { - statements: 80, - branches: 80, - functions: 80, - lines: 80, - }, - }, - }, -}); diff --git a/services/hometitle/coverage/base.css b/services/hometitle/coverage/base.css deleted file mode 100644 index f418035..0000000 --- a/services/hometitle/coverage/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/services/hometitle/coverage/block-navigation.js b/services/hometitle/coverage/block-navigation.js deleted file mode 100644 index 530d1ed..0000000 --- a/services/hometitle/coverage/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/services/hometitle/coverage/change-detector.ts.html b/services/hometitle/coverage/change-detector.ts.html deleted file mode 100644 index 9fee0c4..0000000 --- a/services/hometitle/coverage/change-detector.ts.html +++ /dev/null @@ -1,691 +0,0 @@ - - - - - - Code coverage report for change-detector.ts - - - - - - - - - -
          -
          -

          All files change-detector.ts

          -
          - -
          - 98.83% - Statements - 85/86 -
          - - -
          - 91.07% - Branches - 51/56 -
          - - -
          - 100% - Functions - 11/11 -
          - - -
          - 98.73% - Lines - 78/79 -
          - - -
          -

          - Press n or j to go to the next uncovered block, b, p or k for the previous block. -

          - -
          -
          -
          
          -
          1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203  -  -  -  -  -  -  -  -  -  -  -1x -  -  -  -  -  -  -  -  -11x -  -5x -  -  -  -  -  -5x -  -2x -2x -  -2x -2x -  -1x -1x -  -1x -1x -  -  -  -  -11x -  -  -  -5x -  -  -  -  -  -  -  -5x -5x -  -  -  -15x -  -15x -  -  -  -  -  -  -  -15x -  -15x -14x -14x -14x -  -  -10x -9x -9x -  -  -5x -  -  -  -15x -  -13x -13x -16x -  -6x -6x -  -3x -3x -  -3x -3x -3x -3x -3x -  -  -2x -2x -  -2x -  -  -  -13x -  -  -  -  -  -  -  -11x -11x -  -11x -  -  -  -  -  -  -  -  -11x -66x -66x -  -66x -11x -  -  -  -11x -11x -  -11x -11x -  -11x -11x -4x -7x -2x -5x -1x -4x -1x -  -  -11x -  -  -  -  -  -  -  -  -  -  -  -  -11x -  -11x -  -11x -77x -77x -77x -1x -  -  -  -  -  -  -  -  -11x -  -  -  -5x -5x -5x -5x -  -  -  - 
          import {
          -  PropertySnapshot,
          -  ChangeDetectionResult,
          -  ChangeType,
          -  Severity,
          -  PropertyChange,
          -  DetectionConfig,
          -  Address,
          -} from './types';
          -import { matchRecords } from './matcher.service';
          - 
          -const DEFAULT_DETECTION_CONFIG: DetectionConfig = {
          -  ownershipNameThreshold: 0.7,
          -  deedDateSensitivity: 0.9,
          -  taxAmountChangePercent: 15,
          -};
          - 
          -function classifyFieldChange(field: string, oldValue: unknown, newValue: unknown, config: DetectionConfig): PropertyChange {
          -  let changeType: ChangeType;
          - 
          -  switch (field) {
          -    case 'ownerName':
          -      changeType =
          -        typeof oldValue === 'string' && typeof newValue === 'string'
          -          ? isSignificantNameChange(oldValue, newValue, config)
          -            ? 'ownership_transfer'
          -            : 'metadata_change'
          -          : 'ownership_transfer';
          -      break;
          -    case 'deedDate':
          -      changeType = 'deed_change';
          -      break;
          -    case 'taxAmount':
          -      changeType = 'tax_change';
          -      break;
          -    case 'lienCount':
          -      changeType = (newValue as number) > (oldValue as number) ? 'lien_filing' : 'metadata_change';
          -      break;
          -    case 'taxId':
          -      changeType = 'deed_change';
          -      break;
          -    default:
          -      changeType = 'metadata_change';
          -  }
          - 
          -  return { field, oldValue, newValue, changeType };
          -}
          - 
          -function isSignificantNameChange(oldName: string, newName: string, config: DetectionConfig): boolean {
          -  const dummyAddress: Address = {
          -    streetNumber: '0',
          -    streetName: 'dummy',
          -    city: 'dummy',
          -    state: 'XX',
          -    zip: '00000',
          -  };
          - 
          -  const result = matchRecords(oldName, dummyAddress, newName, dummyAddress);
          -  return result.nameScore < config.ownershipNameThreshold;
          -}
          - 
          -function determineSeverity(changes: PropertyChange[], config: DetectionConfig): Severity {
          -  const severityOverrides = config.severityOverrides || {};
          - 
          -  const typeToSeverity: Record<ChangeType, Severity> = {
          -    ownership_transfer: severityOverrides['ownership_transfer'] || 'major',
          -    deed_change: severityOverrides['deed_change'] || 'moderate',
          -    lien_filing: severityOverrides['lien_filing'] || 'moderate',
          -    tax_change: severityOverrides['tax_change'] || 'minor',
          -    metadata_change: severityOverrides['metadata_change'] || 'minor',
          -  };
          - 
          -  const severityOrder: Severity[] = ['major', 'moderate', 'minor'];
          - 
          -  for (const change of changes) {
          -    const sev = typeToSeverity[change.changeType];
          -    const idx = severityOrder.indexOf(sev);
          -    if (idx === 0) return 'major';
          -  }
          - 
          -  for (const change of changes) {
          -    const sev = typeToSeverity[change.changeType];
          -    if (sev === 'moderate') return 'moderate';
          -  }
          - 
          -  return 'minor';
          -}
          - 
          -function computeChangeConfidence(changes: PropertyChange[], config: DetectionConfig): number {
          -  if (changes.length === 0) return 0;
          - 
          -  let totalConfidence = 0;
          -  for (const change of changes) {
          -    switch (change.changeType) {
          -      case 'ownership_transfer':
          -        totalConfidence += 0.95;
          -        break;
          -      case 'deed_change':
          -        totalConfidence += config.deedDateSensitivity;
          -        break;
          -      case 'tax_change': {
          -        const oldVal = change.oldValue as number;
          -        const newVal = change.newValue as number;
          -        const pctChange = oldVal ? Math.abs(newVal - oldVal) / oldVal * 100 : 100;
          -        totalConfidence += pctChange >= config.taxAmountChangePercent ? 0.85 : 0.5;
          -        break;
          -      }
          -      case 'lien_filing':
          -        totalConfidence += 0.9;
          -        break;
          -      default:
          -        totalConfidence += 0.4;
          -    }
          -  }
          - 
          -  return Math.round((totalConfidence / changes.length) * 1000) / 1000;
          -}
          - 
          -export function detectChanges(
          -  previous: PropertySnapshot,
          -  current: PropertySnapshot,
          -  config?: Partial<DetectionConfig>,
          -): ChangeDetectionResult {
          -  const effectiveConfig = { ...DEFAULT_DETECTION_CONFIG, ...config };
          -  const changes: PropertyChange[] = [];
          - 
          -  const fieldsToCompare: (keyof Omit<PropertySnapshot, 'id' | 'capturedAt' | 'propertyId'>)[] = [
          -    'ownerName',
          -    'deedDate',
          -    'taxId',
          -    'taxAmount',
          -    'lienCount',
          -    'propertyType',
          -  ];
          - 
          -  for (const field of fieldsToCompare) {
          -    const oldValue = previous[field];
          -    const newValue = current[field];
          - 
          -    if (oldValue !== newValue) {
          -      changes.push(classifyFieldChange(field, oldValue, newValue, effectiveConfig));
          -    }
          -  }
          - 
          -  const addressChanges = detectAddressChanges(previous.address, current.address);
          -  changes.push(...addressChanges);
          - 
          -  const severity = determineSeverity(changes, effectiveConfig);
          -  const confidence = computeChangeConfidence(changes, effectiveConfig);
          - 
          -  let changeType: ChangeType = 'metadata_change';
          -  if (changes.some(c => c.changeType === 'ownership_transfer')) {
          -    changeType = 'ownership_transfer';
          -  } else if (changes.some(c => c.changeType === 'deed_change')) {
          -    changeType = 'deed_change';
          -  } else if (changes.some(c => c.changeType === 'lien_filing')) {
          -    changeType = 'lien_filing';
          -  } else if (changes.some(c => c.changeType === 'tax_change')) {
          -    changeType = 'tax_change';
          -  }
          - 
          -  return {
          -    propertyId: previous.propertyId,
          -    changeType,
          -    severity,
          -    confidence,
          -    changes,
          -    previousSnapshot: previous,
          -    currentSnapshot: current,
          -    detectedAt: new Date().toISOString(),
          -  };
          -}
          - 
          -function detectAddressChanges(oldAddr: Address, newAddr: Address): PropertyChange[] {
          -  const changes: PropertyChange[] = [];
          - 
          -  const addressFields: (keyof Address)[] = ['streetNumber', 'streetName', 'streetType', 'unit', 'city', 'state', 'zip'];
          - 
          -  for (const field of addressFields) {
          -    const oldVal = oldAddr[field];
          -    const newVal = newAddr[field];
          -    if (oldVal !== newVal) {
          -      changes.push({
          -        field: `address.${field}`,
          -        oldValue: oldVal,
          -        newValue: newVal,
          -        changeType: 'metadata_change',
          -      });
          -    }
          -  }
          - 
          -  return changes;
          -}
          - 
          -export function shouldTriggerAlert(result: ChangeDetectionResult, minSeverity: Severity = 'moderate'): boolean {
          -  const severityOrder: Severity[] = ['minor', 'moderate', 'major'];
          -  const resultIdx = severityOrder.indexOf(result.severity);
          -  const minIdx = severityOrder.indexOf(minSeverity);
          -  return resultIdx >= minIdx && result.confidence >= 0.7;
          -}
          - 
          -export { classifyFieldChange, determineSeverity, computeChangeConfidence };
          - 
          - -
          -
          - - - - - - - - \ No newline at end of file diff --git a/services/hometitle/coverage/coverage-final.json b/services/hometitle/coverage/coverage-final.json deleted file mode 100644 index de6d546..0000000 --- a/services/hometitle/coverage/coverage-final.json +++ /dev/null @@ -1,5 +0,0 @@ -{"/home/mike/code/ShieldAI/services/hometitle/src/change-detector.ts": {"path":"/home/mike/code/ShieldAI/services/hometitle/src/change-detector.ts","statementMap":{"0":{"start":{"line":12,"column":50},"end":{"line":16,"column":null}},"1":{"start":{"line":21,"column":2},"end":{"line":43,"column":null}},"2":{"start":{"line":23,"column":6},"end":{"line":28,"column":null}},"3":{"start":{"line":29,"column":6},"end":{"line":29,"column":null}},"4":{"start":{"line":31,"column":6},"end":{"line":31,"column":null}},"5":{"start":{"line":32,"column":6},"end":{"line":32,"column":null}},"6":{"start":{"line":34,"column":6},"end":{"line":34,"column":null}},"7":{"start":{"line":35,"column":6},"end":{"line":35,"column":null}},"8":{"start":{"line":37,"column":6},"end":{"line":37,"column":null}},"9":{"start":{"line":38,"column":6},"end":{"line":38,"column":null}},"10":{"start":{"line":40,"column":6},"end":{"line":40,"column":null}},"11":{"start":{"line":41,"column":6},"end":{"line":41,"column":null}},"12":{"start":{"line":43,"column":6},"end":{"line":43,"column":null}},"13":{"start":{"line":46,"column":2},"end":{"line":46,"column":null}},"14":{"start":{"line":50,"column":32},"end":{"line":56,"column":null}},"15":{"start":{"line":58,"column":8},"end":{"line":58,"column":75}},"16":{"start":{"line":59,"column":2},"end":{"line":59,"column":null}},"17":{"start":{"line":63,"column":28},"end":{"line":63,"column":58}},"18":{"start":{"line":65,"column":55},"end":{"line":71,"column":null}},"19":{"start":{"line":73,"column":36},"end":{"line":73,"column":null}},"20":{"start":{"line":75,"column":2},"end":{"line":78,"column":null}},"21":{"start":{"line":76,"column":16},"end":{"line":76,"column":null}},"22":{"start":{"line":77,"column":16},"end":{"line":77,"column":42}},"23":{"start":{"line":78,"column":4},"end":{"line":78,"column":null}},"24":{"start":{"line":78,"column":19},"end":{"line":78,"column":null}},"25":{"start":{"line":81,"column":2},"end":{"line":83,"column":null}},"26":{"start":{"line":82,"column":16},"end":{"line":82,"column":null}},"27":{"start":{"line":83,"column":4},"end":{"line":83,"column":null}},"28":{"start":{"line":83,"column":28},"end":{"line":83,"column":null}},"29":{"start":{"line":86,"column":2},"end":{"line":86,"column":null}},"30":{"start":{"line":90,"column":2},"end":{"line":90,"column":null}},"31":{"start":{"line":90,"column":28},"end":{"line":90,"column":null}},"32":{"start":{"line":92,"column":24},"end":{"line":92,"column":null}},"33":{"start":{"line":93,"column":2},"end":{"line":112,"column":null}},"34":{"start":{"line":94,"column":4},"end":{"line":112,"column":null}},"35":{"start":{"line":96,"column":8},"end":{"line":96,"column":null}},"36":{"start":{"line":97,"column":8},"end":{"line":97,"column":null}},"37":{"start":{"line":99,"column":8},"end":{"line":99,"column":null}},"38":{"start":{"line":100,"column":8},"end":{"line":100,"column":null}},"39":{"start":{"line":102,"column":23},"end":{"line":102,"column":null}},"40":{"start":{"line":103,"column":23},"end":{"line":103,"column":null}},"41":{"start":{"line":104,"column":26},"end":{"line":104,"column":null}},"42":{"start":{"line":105,"column":8},"end":{"line":105,"column":null}},"43":{"start":{"line":106,"column":8},"end":{"line":106,"column":null}},"44":{"start":{"line":109,"column":8},"end":{"line":109,"column":null}},"45":{"start":{"line":110,"column":8},"end":{"line":110,"column":null}},"46":{"start":{"line":112,"column":8},"end":{"line":112,"column":null}},"47":{"start":{"line":116,"column":2},"end":{"line":116,"column":null}},"48":{"start":{"line":124,"column":26},"end":{"line":124,"column":null}},"49":{"start":{"line":125,"column":36},"end":{"line":125,"column":38}},"50":{"start":{"line":127,"column":96},"end":{"line":134,"column":null}},"51":{"start":{"line":136,"column":2},"end":{"line":141,"column":null}},"52":{"start":{"line":137,"column":21},"end":{"line":137,"column":null}},"53":{"start":{"line":138,"column":21},"end":{"line":138,"column":null}},"54":{"start":{"line":140,"column":4},"end":{"line":141,"column":null}},"55":{"start":{"line":141,"column":6},"end":{"line":141,"column":null}},"56":{"start":{"line":145,"column":25},"end":{"line":145,"column":80}},"57":{"start":{"line":146,"column":2},"end":{"line":146,"column":null}},"58":{"start":{"line":148,"column":19},"end":{"line":148,"column":62}},"59":{"start":{"line":149,"column":21},"end":{"line":149,"column":70}},"60":{"start":{"line":151,"column":31},"end":{"line":151,"column":null}},"61":{"start":{"line":152,"column":2},"end":{"line":159,"column":null}},"62":{"start":{"line":152,"column":24},"end":{"line":152,"column":62}},"63":{"start":{"line":153,"column":4},"end":{"line":153,"column":null}},"64":{"start":{"line":154,"column":13},"end":{"line":159,"column":null}},"65":{"start":{"line":154,"column":31},"end":{"line":154,"column":62}},"66":{"start":{"line":155,"column":4},"end":{"line":155,"column":null}},"67":{"start":{"line":156,"column":13},"end":{"line":159,"column":null}},"68":{"start":{"line":156,"column":31},"end":{"line":156,"column":62}},"69":{"start":{"line":157,"column":4},"end":{"line":157,"column":null}},"70":{"start":{"line":158,"column":13},"end":{"line":159,"column":null}},"71":{"start":{"line":158,"column":31},"end":{"line":158,"column":61}},"72":{"start":{"line":159,"column":4},"end":{"line":159,"column":null}},"73":{"start":{"line":162,"column":2},"end":{"line":171,"column":null}},"74":{"start":{"line":175,"column":36},"end":{"line":175,"column":38}},"75":{"start":{"line":177,"column":43},"end":{"line":177,"column":null}},"76":{"start":{"line":179,"column":2},"end":{"line":188,"column":null}},"77":{"start":{"line":180,"column":19},"end":{"line":180,"column":null}},"78":{"start":{"line":181,"column":19},"end":{"line":181,"column":null}},"79":{"start":{"line":182,"column":4},"end":{"line":188,"column":null}},"80":{"start":{"line":183,"column":6},"end":{"line":188,"column":null}},"81":{"start":{"line":192,"column":2},"end":{"line":192,"column":null}},"82":{"start":{"line":196,"column":36},"end":{"line":196,"column":null}},"83":{"start":{"line":197,"column":20},"end":{"line":197,"column":58}},"84":{"start":{"line":198,"column":17},"end":{"line":198,"column":51}},"85":{"start":{"line":199,"column":2},"end":{"line":199,"column":null}}},"fnMap":{"0":{"name":"classifyFieldChange","decl":{"start":{"line":18,"column":9},"end":{"line":18,"column":29}},"loc":{"start":{"line":18,"column":123},"end":{"line":46,"column":null}},"line":18},"1":{"name":"isSignificantNameChange","decl":{"start":{"line":49,"column":9},"end":{"line":49,"column":33}},"loc":{"start":{"line":49,"column":101},"end":{"line":59,"column":null}},"line":49},"2":{"name":"determineSeverity","decl":{"start":{"line":62,"column":9},"end":{"line":62,"column":27}},"loc":{"start":{"line":62,"column":89},"end":{"line":86,"column":null}},"line":62},"3":{"name":"computeChangeConfidence","decl":{"start":{"line":89,"column":9},"end":{"line":89,"column":33}},"loc":{"start":{"line":89,"column":93},"end":{"line":116,"column":null}},"line":89},"4":{"name":"detectChanges","decl":{"start":{"line":119,"column":16},"end":{"line":119,"column":null}},"loc":{"start":{"line":123,"column":25},"end":{"line":171,"column":null}},"line":123},"5":{"name":"(anonymous_5)","decl":{"start":{"line":152,"column":14},"end":{"line":152,"column":19}},"loc":{"start":{"line":152,"column":24},"end":{"line":152,"column":62}},"line":152},"6":{"name":"(anonymous_6)","decl":{"start":{"line":154,"column":21},"end":{"line":154,"column":26}},"loc":{"start":{"line":154,"column":31},"end":{"line":154,"column":62}},"line":154},"7":{"name":"(anonymous_7)","decl":{"start":{"line":156,"column":21},"end":{"line":156,"column":26}},"loc":{"start":{"line":156,"column":31},"end":{"line":156,"column":62}},"line":156},"8":{"name":"(anonymous_8)","decl":{"start":{"line":158,"column":21},"end":{"line":158,"column":26}},"loc":{"start":{"line":158,"column":31},"end":{"line":158,"column":61}},"line":158},"9":{"name":"detectAddressChanges","decl":{"start":{"line":174,"column":9},"end":{"line":174,"column":30}},"loc":{"start":{"line":174,"column":84},"end":{"line":192,"column":null}},"line":174},"10":{"name":"shouldTriggerAlert","decl":{"start":{"line":195,"column":16},"end":{"line":195,"column":35}},"loc":{"start":{"line":195,"column":111},"end":{"line":199,"column":null}},"line":195}},"branchMap":{"0":{"loc":{"start":{"line":21,"column":2},"end":{"line":43,"column":null}},"type":"switch","locations":[{"start":{"line":22,"column":4},"end":{"line":29,"column":null}},{"start":{"line":30,"column":4},"end":{"line":32,"column":null}},{"start":{"line":33,"column":4},"end":{"line":35,"column":null}},{"start":{"line":36,"column":4},"end":{"line":38,"column":null}},{"start":{"line":39,"column":4},"end":{"line":41,"column":null}},{"start":{"line":42,"column":4},"end":{"line":43,"column":null}}],"line":21},"1":{"loc":{"start":{"line":24,"column":8},"end":{"line":28,"column":null}},"type":"cond-expr","locations":[{"start":{"line":25,"column":12},"end":{"line":27,"column":null}},{"start":{"line":28,"column":12},"end":{"line":28,"column":null}}],"line":24},"2":{"loc":{"start":{"line":24,"column":8},"end":{"line":24,"column":null}},"type":"binary-expr","locations":[{"start":{"line":24,"column":8},"end":{"line":24,"column":40}},{"start":{"line":24,"column":40},"end":{"line":24,"column":null}}],"line":24},"3":{"loc":{"start":{"line":25,"column":12},"end":{"line":27,"column":null}},"type":"cond-expr","locations":[{"start":{"line":26,"column":14},"end":{"line":26,"column":null}},{"start":{"line":27,"column":14},"end":{"line":27,"column":null}}],"line":25},"4":{"loc":{"start":{"line":37,"column":20},"end":{"line":37,"column":null}},"type":"cond-expr","locations":[{"start":{"line":37,"column":65},"end":{"line":37,"column":81}},{"start":{"line":37,"column":81},"end":{"line":37,"column":null}}],"line":37},"5":{"loc":{"start":{"line":63,"column":28},"end":{"line":63,"column":58}},"type":"binary-expr","locations":[{"start":{"line":63,"column":28},"end":{"line":63,"column":56}},{"start":{"line":63,"column":56},"end":{"line":63,"column":58}}],"line":63},"6":{"loc":{"start":{"line":66,"column":24},"end":{"line":66,"column":null}},"type":"binary-expr","locations":[{"start":{"line":66,"column":24},"end":{"line":66,"column":67}},{"start":{"line":66,"column":67},"end":{"line":66,"column":null}}],"line":66},"7":{"loc":{"start":{"line":67,"column":17},"end":{"line":67,"column":null}},"type":"binary-expr","locations":[{"start":{"line":67,"column":17},"end":{"line":67,"column":53}},{"start":{"line":67,"column":53},"end":{"line":67,"column":null}}],"line":67},"8":{"loc":{"start":{"line":68,"column":17},"end":{"line":68,"column":null}},"type":"binary-expr","locations":[{"start":{"line":68,"column":17},"end":{"line":68,"column":53}},{"start":{"line":68,"column":53},"end":{"line":68,"column":null}}],"line":68},"9":{"loc":{"start":{"line":69,"column":16},"end":{"line":69,"column":null}},"type":"binary-expr","locations":[{"start":{"line":69,"column":16},"end":{"line":69,"column":51}},{"start":{"line":69,"column":51},"end":{"line":69,"column":null}}],"line":69},"10":{"loc":{"start":{"line":70,"column":21},"end":{"line":70,"column":null}},"type":"binary-expr","locations":[{"start":{"line":70,"column":21},"end":{"line":70,"column":61}},{"start":{"line":70,"column":61},"end":{"line":70,"column":null}}],"line":70},"11":{"loc":{"start":{"line":78,"column":4},"end":{"line":78,"column":null}},"type":"if","locations":[{"start":{"line":78,"column":4},"end":{"line":78,"column":null}},{"start":{},"end":{}}],"line":78},"12":{"loc":{"start":{"line":83,"column":4},"end":{"line":83,"column":null}},"type":"if","locations":[{"start":{"line":83,"column":4},"end":{"line":83,"column":null}},{"start":{},"end":{}}],"line":83},"13":{"loc":{"start":{"line":90,"column":2},"end":{"line":90,"column":null}},"type":"if","locations":[{"start":{"line":90,"column":2},"end":{"line":90,"column":null}},{"start":{},"end":{}}],"line":90},"14":{"loc":{"start":{"line":94,"column":4},"end":{"line":112,"column":null}},"type":"switch","locations":[{"start":{"line":95,"column":6},"end":{"line":97,"column":null}},{"start":{"line":98,"column":6},"end":{"line":100,"column":null}},{"start":{"line":101,"column":6},"end":{"line":106,"column":null}},{"start":{"line":108,"column":6},"end":{"line":110,"column":null}},{"start":{"line":111,"column":6},"end":{"line":112,"column":null}}],"line":94},"15":{"loc":{"start":{"line":104,"column":26},"end":{"line":104,"column":null}},"type":"cond-expr","locations":[{"start":{"line":104,"column":35},"end":{"line":104,"column":78}},{"start":{"line":104,"column":78},"end":{"line":104,"column":null}}],"line":104},"16":{"loc":{"start":{"line":105,"column":27},"end":{"line":105,"column":null}},"type":"cond-expr","locations":[{"start":{"line":105,"column":72},"end":{"line":105,"column":79}},{"start":{"line":105,"column":79},"end":{"line":105,"column":null}}],"line":105},"17":{"loc":{"start":{"line":140,"column":4},"end":{"line":141,"column":null}},"type":"if","locations":[{"start":{"line":140,"column":4},"end":{"line":141,"column":null}},{"start":{},"end":{}}],"line":140},"18":{"loc":{"start":{"line":152,"column":2},"end":{"line":159,"column":null}},"type":"if","locations":[{"start":{"line":152,"column":2},"end":{"line":159,"column":null}},{"start":{"line":154,"column":13},"end":{"line":159,"column":null}}],"line":152},"19":{"loc":{"start":{"line":154,"column":13},"end":{"line":159,"column":null}},"type":"if","locations":[{"start":{"line":154,"column":13},"end":{"line":159,"column":null}},{"start":{"line":156,"column":13},"end":{"line":159,"column":null}}],"line":154},"20":{"loc":{"start":{"line":156,"column":13},"end":{"line":159,"column":null}},"type":"if","locations":[{"start":{"line":156,"column":13},"end":{"line":159,"column":null}},{"start":{"line":158,"column":13},"end":{"line":159,"column":null}}],"line":156},"21":{"loc":{"start":{"line":158,"column":13},"end":{"line":159,"column":null}},"type":"if","locations":[{"start":{"line":158,"column":13},"end":{"line":159,"column":null}},{"start":{},"end":{}}],"line":158},"22":{"loc":{"start":{"line":182,"column":4},"end":{"line":188,"column":null}},"type":"if","locations":[{"start":{"line":182,"column":4},"end":{"line":188,"column":null}},{"start":{},"end":{}}],"line":182},"23":{"loc":{"start":{"line":195,"column":66},"end":{"line":195,"column":111}},"type":"default-arg","locations":[{"start":{"line":195,"column":90},"end":{"line":195,"column":111}}],"line":195},"24":{"loc":{"start":{"line":199,"column":9},"end":{"line":199,"column":null}},"type":"binary-expr","locations":[{"start":{"line":199,"column":9},"end":{"line":199,"column":32}},{"start":{"line":199,"column":32},"end":{"line":199,"column":null}}],"line":199}},"s":{"0":1,"1":11,"2":5,"3":5,"4":2,"5":2,"6":2,"7":2,"8":1,"9":1,"10":1,"11":1,"12":0,"13":11,"14":5,"15":5,"16":5,"17":15,"18":15,"19":15,"20":15,"21":14,"22":14,"23":14,"24":5,"25":10,"26":9,"27":9,"28":5,"29":5,"30":15,"31":2,"32":13,"33":13,"34":16,"35":6,"36":6,"37":3,"38":3,"39":3,"40":3,"41":3,"42":3,"43":3,"44":2,"45":2,"46":2,"47":13,"48":11,"49":11,"50":11,"51":11,"52":66,"53":66,"54":66,"55":11,"56":11,"57":11,"58":11,"59":11,"60":11,"61":11,"62":10,"63":4,"64":7,"65":6,"66":2,"67":5,"68":4,"69":1,"70":4,"71":3,"72":1,"73":11,"74":11,"75":11,"76":11,"77":77,"78":77,"79":77,"80":1,"81":11,"82":5,"83":5,"84":5,"85":5},"f":{"0":11,"1":5,"2":15,"3":15,"4":11,"5":10,"6":6,"7":4,"8":3,"9":11,"10":5},"b":{"0":[5,2,2,1,1,0],"1":[5,0],"2":[5,5],"3":[4,1],"4":[1,0],"5":[15,14],"6":[15,15],"7":[15,15],"8":[15,15],"9":[15,14],"10":[15,15],"11":[5,9],"12":[5,4],"13":[2,13],"14":[6,3,3,2,2],"15":[3,0],"16":[3,0],"17":[11,55],"18":[4,7],"19":[2,5],"20":[1,4],"21":[1,3],"22":[1,76],"23":[5],"24":[5,4]},"meta":{"lastBranch":25,"lastFunction":11,"lastStatement":86,"seen":{"s:12:50:16:Infinity":0,"f:18:9:18:29":0,"b:22:4:29:Infinity:30:4:32:Infinity:33:4:35:Infinity:36:4:38:Infinity:39:4:41:Infinity:42:4:43:Infinity":0,"s:21:2:43:Infinity":1,"s:23:6:28:Infinity":2,"b:25:12:27:Infinity:28:12:28:Infinity":1,"b:24:8:24:40:24:40:24:Infinity":2,"b:26:14:26:Infinity:27:14:27:Infinity":3,"s:29:6:29:Infinity":3,"s:31:6:31:Infinity":4,"s:32:6:32:Infinity":5,"s:34:6:34:Infinity":6,"s:35:6:35:Infinity":7,"s:37:6:37:Infinity":8,"b:37:65:37:81:37:81:37:Infinity":4,"s:38:6:38:Infinity":9,"s:40:6:40:Infinity":10,"s:41:6:41:Infinity":11,"s:43:6:43:Infinity":12,"s:46:2:46:Infinity":13,"f:49:9:49:33":1,"s:50:32:56:Infinity":14,"s:58:8:58:75":15,"s:59:2:59:Infinity":16,"f:62:9:62:27":2,"s:63:28:63:58":17,"b:63:28:63:56:63:56:63:58":5,"s:65:55:71:Infinity":18,"b:66:24:66:67:66:67:66:Infinity":6,"b:67:17:67:53:67:53:67:Infinity":7,"b:68:17:68:53:68:53:68:Infinity":8,"b:69:16:69:51:69:51:69:Infinity":9,"b:70:21:70:61:70:61:70:Infinity":10,"s:73:36:73:Infinity":19,"s:75:2:78:Infinity":20,"s:76:16:76:Infinity":21,"s:77:16:77:42":22,"b:78:4:78:Infinity:undefined:undefined:undefined:undefined":11,"s:78:4:78:Infinity":23,"s:78:19:78:Infinity":24,"s:81:2:83:Infinity":25,"s:82:16:82:Infinity":26,"b:83:4:83:Infinity:undefined:undefined:undefined:undefined":12,"s:83:4:83:Infinity":27,"s:83:28:83:Infinity":28,"s:86:2:86:Infinity":29,"f:89:9:89:33":3,"b:90:2:90:Infinity:undefined:undefined:undefined:undefined":13,"s:90:2:90:Infinity":30,"s:90:28:90:Infinity":31,"s:92:24:92:Infinity":32,"s:93:2:112:Infinity":33,"b:95:6:97:Infinity:98:6:100:Infinity:101:6:106:Infinity:108:6:110:Infinity:111:6:112:Infinity":14,"s:94:4:112:Infinity":34,"s:96:8:96:Infinity":35,"s:97:8:97:Infinity":36,"s:99:8:99:Infinity":37,"s:100:8:100:Infinity":38,"s:102:23:102:Infinity":39,"s:103:23:103:Infinity":40,"s:104:26:104:Infinity":41,"b:104:35:104:78:104:78:104:Infinity":15,"s:105:8:105:Infinity":42,"b:105:72:105:79:105:79:105:Infinity":16,"s:106:8:106:Infinity":43,"s:109:8:109:Infinity":44,"s:110:8:110:Infinity":45,"s:112:8:112:Infinity":46,"s:116:2:116:Infinity":47,"f:119:16:119:Infinity":4,"s:124:26:124:Infinity":48,"s:125:36:125:38":49,"s:127:96:134:Infinity":50,"s:136:2:141:Infinity":51,"s:137:21:137:Infinity":52,"s:138:21:138:Infinity":53,"b:140:4:141:Infinity:undefined:undefined:undefined:undefined":17,"s:140:4:141:Infinity":54,"s:141:6:141:Infinity":55,"s:145:25:145:80":56,"s:146:2:146:Infinity":57,"s:148:19:148:62":58,"s:149:21:149:70":59,"s:151:31:151:Infinity":60,"b:152:2:159:Infinity:154:13:159:Infinity":18,"s:152:2:159:Infinity":61,"f:152:14:152:19":5,"s:152:24:152:62":62,"s:153:4:153:Infinity":63,"b:154:13:159:Infinity:156:13:159:Infinity":19,"s:154:13:159:Infinity":64,"f:154:21:154:26":6,"s:154:31:154:62":65,"s:155:4:155:Infinity":66,"b:156:13:159:Infinity:158:13:159:Infinity":20,"s:156:13:159:Infinity":67,"f:156:21:156:26":7,"s:156:31:156:62":68,"s:157:4:157:Infinity":69,"b:158:13:159:Infinity:undefined:undefined:undefined:undefined":21,"s:158:13:159:Infinity":70,"f:158:21:158:26":8,"s:158:31:158:61":71,"s:159:4:159:Infinity":72,"s:162:2:171:Infinity":73,"f:174:9:174:30":9,"s:175:36:175:38":74,"s:177:43:177:Infinity":75,"s:179:2:188:Infinity":76,"s:180:19:180:Infinity":77,"s:181:19:181:Infinity":78,"b:182:4:188:Infinity:undefined:undefined:undefined:undefined":22,"s:182:4:188:Infinity":79,"s:183:6:188:Infinity":80,"s:192:2:192:Infinity":81,"f:195:16:195:35":10,"b:195:90:195:111":23,"s:196:36:196:Infinity":82,"s:197:20:197:58":83,"s:198:17:198:51":84,"s:199:2:199:Infinity":85,"b:199:9:199:32:199:32:199:Infinity":24}}} -,"/home/mike/code/ShieldAI/services/hometitle/src/index.ts": {"path":"/home/mike/code/ShieldAI/services/hometitle/src/index.ts","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} -,"/home/mike/code/ShieldAI/services/hometitle/src/matcher.service.ts": {"path":"/home/mike/code/ShieldAI/services/hometitle/src/matcher.service.ts","statementMap":{"0":{"start":{"line":11,"column":39},"end":{"line":16,"column":null}},"1":{"start":{"line":18,"column":24},"end":{"line":21,"column":2}},"2":{"start":{"line":23,"column":24},"end":{"line":26,"column":2}},"3":{"start":{"line":28,"column":48},"end":{"line":44,"column":null}},"4":{"start":{"line":46,"column":77},"end":{"line":51,"column":null}},"5":{"start":{"line":54,"column":29},"end":{"line":55,"column":null}},"6":{"start":{"line":55,"column":4},"end":{"line":55,"column":83}},"7":{"start":{"line":55,"column":52},"end":{"line":55,"column":83}},"8":{"start":{"line":58,"column":2},"end":{"line":65,"column":null}},"9":{"start":{"line":58,"column":15},"end":{"line":58,"column":18}},"10":{"start":{"line":59,"column":4},"end":{"line":65,"column":null}},"11":{"start":{"line":59,"column":17},"end":{"line":59,"column":20}},"12":{"start":{"line":60,"column":19},"end":{"line":60,"column":null}},"13":{"start":{"line":61,"column":6},"end":{"line":65,"column":null}},"14":{"start":{"line":69,"column":2},"end":{"line":69,"column":null}},"15":{"start":{"line":73,"column":2},"end":{"line":73,"column":null}},"16":{"start":{"line":73,"column":20},"end":{"line":73,"column":null}},"17":{"start":{"line":74,"column":2},"end":{"line":74,"column":null}},"18":{"start":{"line":78,"column":2},"end":{"line":83,"column":null}},"19":{"start":{"line":87,"column":16},"end":{"line":87,"column":37}},"20":{"start":{"line":88,"column":16},"end":{"line":88,"column":48}},"21":{"start":{"line":90,"column":18},"end":{"line":90,"column":null}},"22":{"start":{"line":91,"column":17},"end":{"line":91,"column":null}},"23":{"start":{"line":92,"column":19},"end":{"line":92,"column":null}},"24":{"start":{"line":93,"column":29},"end":{"line":93,"column":31}},"25":{"start":{"line":95,"column":2},"end":{"line":95,"column":null}},"26":{"start":{"line":95,"column":26},"end":{"line":95,"column":null}},"27":{"start":{"line":97,"column":17},"end":{"line":97,"column":null}},"28":{"start":{"line":98,"column":2},"end":{"line":99,"column":null}},"29":{"start":{"line":99,"column":4},"end":{"line":99,"column":null}},"30":{"start":{"line":102,"column":15},"end":{"line":102,"column":null}},"31":{"start":{"line":103,"column":2},"end":{"line":104,"column":null}},"32":{"start":{"line":104,"column":4},"end":{"line":104,"column":null}},"33":{"start":{"line":107,"column":20},"end":{"line":107,"column":49}},"34":{"start":{"line":109,"column":2},"end":{"line":117,"column":null}},"35":{"start":{"line":110,"column":4},"end":{"line":110,"column":null}},"36":{"start":{"line":111,"column":13},"end":{"line":117,"column":null}},"37":{"start":{"line":112,"column":4},"end":{"line":112,"column":null}},"38":{"start":{"line":113,"column":4},"end":{"line":113,"column":null}},"39":{"start":{"line":115,"column":4},"end":{"line":115,"column":null}},"40":{"start":{"line":116,"column":4},"end":{"line":116,"column":null}},"41":{"start":{"line":117,"column":4},"end":{"line":117,"column":null}},"42":{"start":{"line":120,"column":2},"end":{"line":121,"column":null}},"43":{"start":{"line":121,"column":4},"end":{"line":121,"column":null}},"44":{"start":{"line":123,"column":2},"end":{"line":126,"column":null}},"45":{"start":{"line":124,"column":24},"end":{"line":124,"column":45}},"46":{"start":{"line":125,"column":4},"end":{"line":126,"column":null}},"47":{"start":{"line":126,"column":6},"end":{"line":126,"column":null}},"48":{"start":{"line":126,"column":27},"end":{"line":126,"column":null}},"49":{"start":{"line":130,"column":2},"end":{"line":130,"column":null}},"50":{"start":{"line":134,"column":16},"end":{"line":134,"column":37}},"51":{"start":{"line":135,"column":2},"end":{"line":135,"column":null}},"52":{"start":{"line":139,"column":16},"end":{"line":147,"column":19}},"53":{"start":{"line":148,"column":2},"end":{"line":148,"column":null}},"54":{"start":{"line":152,"column":17},"end":{"line":152,"column":null}},"55":{"start":{"line":153,"column":22},"end":{"line":153,"column":36}},"56":{"start":{"line":154,"column":22},"end":{"line":154,"column":36}},"57":{"start":{"line":156,"column":2},"end":{"line":156,"column":null}},"58":{"start":{"line":156,"column":36},"end":{"line":156,"column":null}},"59":{"start":{"line":157,"column":2},"end":{"line":157,"column":null}},"60":{"start":{"line":157,"column":36},"end":{"line":157,"column":null}},"61":{"start":{"line":159,"column":2},"end":{"line":159,"column":null}},"62":{"start":{"line":159,"column":35},"end":{"line":159,"column":null}},"63":{"start":{"line":161,"column":15},"end":{"line":161,"column":60}},"64":{"start":{"line":162,"column":17},"end":{"line":162,"column":65}},"65":{"start":{"line":163,"column":16},"end":{"line":163,"column":45}},"66":{"start":{"line":165,"column":2},"end":{"line":165,"column":null}},"67":{"start":{"line":169,"column":12},"end":{"line":169,"column":null}},"68":{"start":{"line":170,"column":8},"end":{"line":170,"column":null}},"69":{"start":{"line":171,"column":8},"end":{"line":171,"column":null}},"70":{"start":{"line":173,"column":4},"end":{"line":177,"column":24}},"71":{"start":{"line":178,"column":12},"end":{"line":178,"column":58}},"72":{"start":{"line":179,"column":2},"end":{"line":179,"column":null}},"73":{"start":{"line":183,"column":21},"end":{"line":183,"column":null}},"74":{"start":{"line":184,"column":20},"end":{"line":184,"column":null}},"75":{"start":{"line":185,"column":22},"end":{"line":185,"column":null}},"76":{"start":{"line":187,"column":26},"end":{"line":187,"column":null}},"77":{"start":{"line":188,"column":2},"end":{"line":196,"column":null}},"78":{"start":{"line":189,"column":25},"end":{"line":189,"column":76}},"79":{"start":{"line":189,"column":59},"end":{"line":189,"column":74}},"80":{"start":{"line":190,"column":25},"end":{"line":190,"column":76}},"81":{"start":{"line":190,"column":59},"end":{"line":190,"column":74}},"82":{"start":{"line":191,"column":18},"end":{"line":191,"column":null}},"83":{"start":{"line":192,"column":4},"end":{"line":193,"column":null}},"84":{"start":{"line":193,"column":6},"end":{"line":193,"column":null}},"85":{"start":{"line":193,"column":34},"end":{"line":193,"column":null}},"86":{"start":{"line":195,"column":18},"end":{"line":195,"column":64}},"87":{"start":{"line":196,"column":4},"end":{"line":196,"column":null}},"88":{"start":{"line":199,"column":20},"end":{"line":199,"column":null}},"89":{"start":{"line":200,"column":2},"end":{"line":200,"column":null}},"90":{"start":{"line":204,"column":22},"end":{"line":204,"column":null}},"91":{"start":{"line":205,"column":22},"end":{"line":205,"column":null}},"92":{"start":{"line":206,"column":20},"end":{"line":209,"column":null}},"93":{"start":{"line":210,"column":20},"end":{"line":210,"column":null}},"94":{"start":{"line":211,"column":20},"end":{"line":211,"column":null}},"95":{"start":{"line":212,"column":21},"end":{"line":212,"column":null}},"96":{"start":{"line":213,"column":19},"end":{"line":213,"column":null}},"97":{"start":{"line":216,"column":17},"end":{"line":216,"column":null}},"98":{"start":{"line":218,"column":2},"end":{"line":221,"column":null}},"99":{"start":{"line":219,"column":4},"end":{"line":219,"column":null}},"100":{"start":{"line":220,"column":20},"end":{"line":220,"column":null}},"101":{"start":{"line":221,"column":4},"end":{"line":221,"column":null}},"102":{"start":{"line":225,"column":5},"end":{"line":232,"column":null}},"103":{"start":{"line":234,"column":2},"end":{"line":234,"column":null}},"104":{"start":{"line":244,"column":26},"end":{"line":244,"column":null}},"105":{"start":{"line":246,"column":18},"end":{"line":246,"column":34}},"106":{"start":{"line":247,"column":18},"end":{"line":247,"column":34}},"107":{"start":{"line":249,"column":20},"end":{"line":249,"column":54}},"108":{"start":{"line":251,"column":53},"end":{"line":251,"column":109}},"109":{"start":{"line":253,"column":28},"end":{"line":253,"column":null}},"110":{"start":{"line":255,"column":21},"end":{"line":255,"column":76}},"111":{"start":{"line":256,"column":20},"end":{"line":256,"column":73}},"112":{"start":{"line":257,"column":22},"end":{"line":257,"column":79}},"113":{"start":{"line":258,"column":22},"end":{"line":258,"column":85}},"114":{"start":{"line":259,"column":22},"end":{"line":259,"column":98}},"115":{"start":{"line":260,"column":20},"end":{"line":262,"column":null}},"116":{"start":{"line":264,"column":20},"end":{"line":264,"column":79}},"117":{"start":{"line":265,"column":20},"end":{"line":265,"column":67}},"118":{"start":{"line":266,"column":21},"end":{"line":266,"column":70}},"119":{"start":{"line":267,"column":19},"end":{"line":267,"column":64}},"120":{"start":{"line":269,"column":22},"end":{"line":269,"column":48}},"121":{"start":{"line":270,"column":22},"end":{"line":270,"column":48}},"122":{"start":{"line":272,"column":15},"end":{"line":274,"column":null}},"123":{"start":{"line":277,"column":32},"end":{"line":294,"column":null}},"124":{"start":{"line":296,"column":2},"end":{"line":302,"column":null}},"125":{"start":{"line":306,"column":2},"end":{"line":306,"column":null}}},"fnMap":{"0":{"name":"levenshteinDistance","decl":{"start":{"line":53,"column":9},"end":{"line":53,"column":29}},"loc":{"start":{"line":53,"column":59},"end":{"line":69,"column":null}},"line":53},"1":{"name":"(anonymous_1)","decl":{"start":{"line":54,"column":64},"end":{"line":54,"column":67}},"loc":{"start":{"line":55,"column":4},"end":{"line":55,"column":83}},"line":55},"2":{"name":"(anonymous_2)","decl":{"start":{"line":55,"column":39},"end":{"line":55,"column":42}},"loc":{"start":{"line":55,"column":52},"end":{"line":55,"column":83}},"line":55},"3":{"name":"similarityScore","decl":{"start":{"line":72,"column":9},"end":{"line":72,"column":25}},"loc":{"start":{"line":72,"column":67},"end":{"line":74,"column":null}},"line":72},"4":{"name":"normalizeString","decl":{"start":{"line":77,"column":9},"end":{"line":77,"column":25}},"loc":{"start":{"line":77,"column":46},"end":{"line":83,"column":null}},"line":77},"5":{"name":"parseName","decl":{"start":{"line":86,"column":9},"end":{"line":86,"column":19}},"loc":{"start":{"line":86,"column":51},"end":{"line":130,"column":null}},"line":86},"6":{"name":"normalizeStreetType","decl":{"start":{"line":133,"column":9},"end":{"line":133,"column":29}},"loc":{"start":{"line":133,"column":51},"end":{"line":135,"column":null}},"line":133},"7":{"name":"normalizeAddress","decl":{"start":{"line":138,"column":9},"end":{"line":138,"column":26}},"loc":{"start":{"line":138,"column":49},"end":{"line":148,"column":null}},"line":138},"8":{"name":"computeFieldMatch","decl":{"start":{"line":151,"column":9},"end":{"line":151,"column":27}},"loc":{"start":{"line":151,"column":108},"end":{"line":165,"column":null}},"line":151},"9":{"name":"haversineDistance","decl":{"start":{"line":168,"column":9},"end":{"line":168,"column":27}},"loc":{"start":{"line":168,"column":91},"end":{"line":179,"column":null}},"line":168},"10":{"name":"computeNameScore","decl":{"start":{"line":182,"column":9},"end":{"line":182,"column":26}},"loc":{"start":{"line":182,"column":88},"end":{"line":200,"column":null}},"line":182},"11":{"name":"(anonymous_11)","decl":{"start":{"line":189,"column":50},"end":{"line":189,"column":54}},"loc":{"start":{"line":189,"column":59},"end":{"line":189,"column":74}},"line":189},"12":{"name":"(anonymous_12)","decl":{"start":{"line":190,"column":50},"end":{"line":190,"column":54}},"loc":{"start":{"line":190,"column":59},"end":{"line":190,"column":74}},"line":190},"13":{"name":"computeAddressScore","decl":{"start":{"line":203,"column":9},"end":{"line":203,"column":29}},"loc":{"start":{"line":203,"column":132},"end":{"line":234,"column":null}},"line":203},"14":{"name":"matchRecords","decl":{"start":{"line":237,"column":16},"end":{"line":237,"column":null}},"loc":{"start":{"line":243,"column":15},"end":{"line":302,"column":null}},"line":243},"15":{"name":"getConfigForPropertyType","decl":{"start":{"line":305,"column":16},"end":{"line":305,"column":41}},"loc":{"start":{"line":305,"column":77},"end":{"line":306,"column":null}},"line":305}},"branchMap":{"0":{"loc":{"start":{"line":55,"column":52},"end":{"line":55,"column":83}},"type":"cond-expr","locations":[{"start":{"line":55,"column":62},"end":{"line":55,"column":66}},{"start":{"line":55,"column":66},"end":{"line":55,"column":83}}],"line":55},"1":{"loc":{"start":{"line":55,"column":66},"end":{"line":55,"column":83}},"type":"cond-expr","locations":[{"start":{"line":55,"column":76},"end":{"line":55,"column":80}},{"start":{"line":55,"column":80},"end":{"line":55,"column":83}}],"line":55},"2":{"loc":{"start":{"line":60,"column":19},"end":{"line":60,"column":null}},"type":"cond-expr","locations":[{"start":{"line":60,"column":43},"end":{"line":60,"column":47}},{"start":{"line":60,"column":47},"end":{"line":60,"column":null}}],"line":60},"3":{"loc":{"start":{"line":73,"column":2},"end":{"line":73,"column":null}},"type":"if","locations":[{"start":{"line":73,"column":2},"end":{"line":73,"column":null}},{"start":{},"end":{}}],"line":73},"4":{"loc":{"start":{"line":95,"column":2},"end":{"line":95,"column":null}},"type":"if","locations":[{"start":{"line":95,"column":2},"end":{"line":95,"column":null}},{"start":{},"end":{}}],"line":95},"5":{"loc":{"start":{"line":98,"column":9},"end":{"line":98,"column":72}},"type":"binary-expr","locations":[{"start":{"line":98,"column":9},"end":{"line":98,"column":36}},{"start":{"line":98,"column":36},"end":{"line":98,"column":72}}],"line":98},"6":{"loc":{"start":{"line":103,"column":9},"end":{"line":103,"column":72}},"type":"binary-expr","locations":[{"start":{"line":103,"column":9},"end":{"line":103,"column":34}},{"start":{"line":103,"column":34},"end":{"line":103,"column":72}}],"line":103},"7":{"loc":{"start":{"line":109,"column":2},"end":{"line":117,"column":null}},"type":"if","locations":[{"start":{"line":109,"column":2},"end":{"line":117,"column":null}},{"start":{"line":111,"column":13},"end":{"line":117,"column":null}}],"line":109},"8":{"loc":{"start":{"line":111,"column":13},"end":{"line":117,"column":null}},"type":"if","locations":[{"start":{"line":111,"column":13},"end":{"line":117,"column":null}},{"start":{"line":114,"column":9},"end":{"line":117,"column":null}}],"line":111},"9":{"loc":{"start":{"line":120,"column":2},"end":{"line":121,"column":null}},"type":"if","locations":[{"start":{"line":120,"column":2},"end":{"line":121,"column":null}},{"start":{},"end":{}}],"line":120},"10":{"loc":{"start":{"line":123,"column":2},"end":{"line":126,"column":null}},"type":"if","locations":[{"start":{"line":123,"column":2},"end":{"line":126,"column":null}},{"start":{},"end":{}}],"line":123},"11":{"loc":{"start":{"line":126,"column":6},"end":{"line":126,"column":null}},"type":"if","locations":[{"start":{"line":126,"column":6},"end":{"line":126,"column":null}},{"start":{},"end":{}}],"line":126},"12":{"loc":{"start":{"line":135,"column":9},"end":{"line":135,"column":null}},"type":"binary-expr","locations":[{"start":{"line":135,"column":9},"end":{"line":135,"column":35}},{"start":{"line":135,"column":35},"end":{"line":135,"column":null}}],"line":135},"13":{"loc":{"start":{"line":142,"column":4},"end":{"line":142,"column":null}},"type":"cond-expr","locations":[{"start":{"line":142,"column":22},"end":{"line":142,"column":58}},{"start":{"line":142,"column":61},"end":{"line":142,"column":null}}],"line":142},"14":{"loc":{"start":{"line":143,"column":4},"end":{"line":143,"column":null}},"type":"cond-expr","locations":[{"start":{"line":143,"column":16},"end":{"line":143,"column":42}},{"start":{"line":143,"column":45},"end":{"line":143,"column":null}}],"line":143},"15":{"loc":{"start":{"line":152,"column":17},"end":{"line":152,"column":null}},"type":"binary-expr","locations":[{"start":{"line":152,"column":17},"end":{"line":152,"column":32}},{"start":{"line":152,"column":32},"end":{"line":152,"column":null}}],"line":152},"16":{"loc":{"start":{"line":156,"column":2},"end":{"line":156,"column":null}},"type":"if","locations":[{"start":{"line":156,"column":2},"end":{"line":156,"column":null}},{"start":{},"end":{}}],"line":156},"17":{"loc":{"start":{"line":156,"column":6},"end":{"line":156,"column":36}},"type":"binary-expr","locations":[{"start":{"line":156,"column":6},"end":{"line":156,"column":22}},{"start":{"line":156,"column":22},"end":{"line":156,"column":36}}],"line":156},"18":{"loc":{"start":{"line":157,"column":2},"end":{"line":157,"column":null}},"type":"if","locations":[{"start":{"line":157,"column":2},"end":{"line":157,"column":null}},{"start":{},"end":{}}],"line":157},"19":{"loc":{"start":{"line":157,"column":6},"end":{"line":157,"column":36}},"type":"binary-expr","locations":[{"start":{"line":157,"column":6},"end":{"line":157,"column":22}},{"start":{"line":157,"column":22},"end":{"line":157,"column":36}}],"line":157},"20":{"loc":{"start":{"line":159,"column":2},"end":{"line":159,"column":null}},"type":"if","locations":[{"start":{"line":159,"column":2},"end":{"line":159,"column":null}},{"start":{},"end":{}}],"line":159},"21":{"loc":{"start":{"line":188,"column":2},"end":{"line":196,"column":null}},"type":"if","locations":[{"start":{"line":188,"column":2},"end":{"line":196,"column":null}},{"start":{},"end":{}}],"line":188},"22":{"loc":{"start":{"line":188,"column":6},"end":{"line":188,"column":66}},"type":"binary-expr","locations":[{"start":{"line":188,"column":6},"end":{"line":188,"column":37}},{"start":{"line":188,"column":37},"end":{"line":188,"column":66}}],"line":188},"23":{"loc":{"start":{"line":193,"column":6},"end":{"line":193,"column":null}},"type":"if","locations":[{"start":{"line":193,"column":6},"end":{"line":193,"column":null}},{"start":{},"end":{}}],"line":193},"24":{"loc":{"start":{"line":196,"column":24},"end":{"line":196,"column":null}},"type":"cond-expr","locations":[{"start":{"line":196,"column":36},"end":{"line":196,"column":54}},{"start":{"line":196,"column":54},"end":{"line":196,"column":null}}],"line":196},"25":{"loc":{"start":{"line":207,"column":4},"end":{"line":207,"column":null}},"type":"cond-expr","locations":[{"start":{"line":207,"column":23},"end":{"line":207,"column":60}},{"start":{"line":207,"column":63},"end":{"line":207,"column":null}}],"line":207},"26":{"loc":{"start":{"line":208,"column":4},"end":{"line":208,"column":null}},"type":"cond-expr","locations":[{"start":{"line":208,"column":23},"end":{"line":208,"column":60}},{"start":{"line":208,"column":63},"end":{"line":208,"column":null}}],"line":208},"27":{"loc":{"start":{"line":210,"column":38},"end":{"line":210,"column":56}},"type":"binary-expr","locations":[{"start":{"line":210,"column":38},"end":{"line":210,"column":52}},{"start":{"line":210,"column":52},"end":{"line":210,"column":56}}],"line":210},"28":{"loc":{"start":{"line":210,"column":56},"end":{"line":210,"column":73}},"type":"binary-expr","locations":[{"start":{"line":210,"column":56},"end":{"line":210,"column":70}},{"start":{"line":210,"column":70},"end":{"line":210,"column":73}}],"line":210},"29":{"loc":{"start":{"line":218,"column":2},"end":{"line":221,"column":null}},"type":"if","locations":[{"start":{"line":218,"column":2},"end":{"line":221,"column":null}},{"start":{},"end":{}}],"line":218},"30":{"loc":{"start":{"line":218,"column":6},"end":{"line":218,"column":78}},"type":"binary-expr","locations":[{"start":{"line":218,"column":6},"end":{"line":218,"column":24}},{"start":{"line":218,"column":24},"end":{"line":218,"column":43}},{"start":{"line":218,"column":43},"end":{"line":218,"column":61}},{"start":{"line":218,"column":61},"end":{"line":218,"column":78}}],"line":218},"31":{"loc":{"start":{"line":221,"column":15},"end":{"line":221,"column":116}},"type":"cond-expr","locations":[{"start":{"line":221,"column":46},"end":{"line":221,"column":52}},{"start":{"line":221,"column":52},"end":{"line":221,"column":116}}],"line":221},"32":{"loc":{"start":{"line":232,"column":17},"end":{"line":232,"column":null}},"type":"cond-expr","locations":[{"start":{"line":232,"column":51},"end":{"line":232,"column":58}},{"start":{"line":232,"column":58},"end":{"line":232,"column":null}}],"line":232},"33":{"loc":{"start":{"line":261,"column":4},"end":{"line":261,"column":null}},"type":"cond-expr","locations":[{"start":{"line":261,"column":26},"end":{"line":261,"column":66}},{"start":{"line":261,"column":69},"end":{"line":261,"column":null}}],"line":261},"34":{"loc":{"start":{"line":262,"column":4},"end":{"line":262,"column":null}},"type":"cond-expr","locations":[{"start":{"line":262,"column":26},"end":{"line":262,"column":66}},{"start":{"line":262,"column":69},"end":{"line":262,"column":null}}],"line":262},"35":{"loc":{"start":{"line":264,"column":38},"end":{"line":264,"column":59}},"type":"binary-expr","locations":[{"start":{"line":264,"column":38},"end":{"line":264,"column":55}},{"start":{"line":264,"column":55},"end":{"line":264,"column":59}}],"line":264},"36":{"loc":{"start":{"line":264,"column":59},"end":{"line":264,"column":79}},"type":"binary-expr","locations":[{"start":{"line":264,"column":59},"end":{"line":264,"column":76}},{"start":{"line":264,"column":76},"end":{"line":264,"column":79}}],"line":264}},"s":{"0":2,"1":2,"2":2,"3":2,"4":2,"5":56,"6":379,"7":2622,"8":56,"9":56,"10":323,"11":323,"12":1950,"13":1950,"14":56,"15":39,"16":1,"17":38,"18":848,"19":37,"20":37,"21":37,"22":37,"23":37,"24":37,"25":37,"26":1,"27":36,"28":36,"29":2,"30":36,"31":36,"32":2,"33":36,"34":36,"35":1,"36":35,"37":31,"38":31,"39":4,"40":4,"41":4,"42":36,"43":0,"44":36,"45":4,"46":4,"47":4,"48":2,"49":36,"50":69,"51":69,"52":30,"53":30,"54":300,"55":300,"56":300,"57":300,"58":48,"59":252,"60":2,"61":250,"62":250,"63":36,"64":36,"65":36,"66":36,"67":9,"68":9,"69":9,"70":9,"71":9,"72":9,"73":15,"74":15,"75":15,"76":15,"77":15,"78":1,"79":1,"80":1,"81":0,"82":1,"83":1,"84":1,"85":0,"86":1,"87":1,"88":15,"89":15,"90":15,"91":15,"92":15,"93":15,"94":15,"95":15,"96":15,"97":15,"98":15,"99":9,"100":9,"101":9,"102":15,"103":15,"104":15,"105":15,"106":15,"107":15,"108":15,"109":15,"110":15,"111":15,"112":15,"113":15,"114":15,"115":15,"116":15,"117":15,"118":15,"119":15,"120":15,"121":15,"122":15,"123":15,"124":15,"125":3},"f":{"0":56,"1":379,"2":2622,"3":39,"4":848,"5":37,"6":69,"7":30,"8":300,"9":9,"10":15,"11":1,"12":0,"13":15,"14":15,"15":3},"b":{"0":[349,2273],"1":[323,1950],"2":[212,1738],"3":[1,38],"4":[1,36],"5":[36,38],"6":[36,37],"7":[1,35],"8":[31,4],"9":[0,36],"10":[4,32],"11":[2,2],"12":[69,0],"13":[20,10],"14":[19,11],"15":[300,270],"16":[48,252],"17":[300,48],"18":[2,250],"19":[252,252],"20":[214,36],"21":[1,14],"22":[15,14],"23":[0,1],"24":[1,0],"25":[10,5],"26":[10,5],"27":[15,5],"28":[15,6],"29":[9,6],"30":[15,10,10,9],"31":[9,0],"32":[9,6],"33":[10,5],"34":[10,5],"35":[15,5],"36":[15,6]},"meta":{"lastBranch":37,"lastFunction":16,"lastStatement":126,"seen":{"s:11:39:16:Infinity":0,"s:18:24:21:2":1,"s:23:24:26:2":2,"s:28:48:44:Infinity":3,"s:46:77:51:Infinity":4,"f:53:9:53:29":0,"s:54:29:55:Infinity":5,"f:54:64:54:67":1,"s:55:4:55:83":6,"f:55:39:55:42":2,"s:55:52:55:83":7,"b:55:62:55:66:55:66:55:83":0,"b:55:76:55:80:55:80:55:83":1,"s:58:2:65:Infinity":8,"s:58:15:58:18":9,"s:59:4:65:Infinity":10,"s:59:17:59:20":11,"s:60:19:60:Infinity":12,"b:60:43:60:47:60:47:60:Infinity":2,"s:61:6:65:Infinity":13,"s:69:2:69:Infinity":14,"f:72:9:72:25":3,"b:73:2:73:Infinity:undefined:undefined:undefined:undefined":3,"s:73:2:73:Infinity":15,"s:73:20:73:Infinity":16,"s:74:2:74:Infinity":17,"f:77:9:77:25":4,"s:78:2:83:Infinity":18,"f:86:9:86:19":5,"s:87:16:87:37":19,"s:88:16:88:48":20,"s:90:18:90:Infinity":21,"s:91:17:91:Infinity":22,"s:92:19:92:Infinity":23,"s:93:29:93:31":24,"b:95:2:95:Infinity:undefined:undefined:undefined:undefined":4,"s:95:2:95:Infinity":25,"s:95:26:95:Infinity":26,"s:97:17:97:Infinity":27,"s:98:2:99:Infinity":28,"b:98:9:98:36:98:36:98:72":5,"s:99:4:99:Infinity":29,"s:102:15:102:Infinity":30,"s:103:2:104:Infinity":31,"b:103:9:103:34:103:34:103:72":6,"s:104:4:104:Infinity":32,"s:107:20:107:49":33,"b:109:2:117:Infinity:111:13:117:Infinity":7,"s:109:2:117:Infinity":34,"s:110:4:110:Infinity":35,"b:111:13:117:Infinity:114:9:117:Infinity":8,"s:111:13:117:Infinity":36,"s:112:4:112:Infinity":37,"s:113:4:113:Infinity":38,"s:115:4:115:Infinity":39,"s:116:4:116:Infinity":40,"s:117:4:117:Infinity":41,"b:120:2:121:Infinity:undefined:undefined:undefined:undefined":9,"s:120:2:121:Infinity":42,"s:121:4:121:Infinity":43,"b:123:2:126:Infinity:undefined:undefined:undefined:undefined":10,"s:123:2:126:Infinity":44,"s:124:24:124:45":45,"s:125:4:126:Infinity":46,"b:126:6:126:Infinity:undefined:undefined:undefined:undefined":11,"s:126:6:126:Infinity":47,"s:126:27:126:Infinity":48,"s:130:2:130:Infinity":49,"f:133:9:133:29":6,"s:134:16:134:37":50,"s:135:2:135:Infinity":51,"b:135:9:135:35:135:35:135:Infinity":12,"f:138:9:138:26":7,"s:139:16:147:19":52,"b:142:22:142:58:142:61:142:Infinity":13,"b:143:16:143:42:143:45:143:Infinity":14,"s:148:2:148:Infinity":53,"f:151:9:151:27":8,"s:152:17:152:Infinity":54,"b:152:17:152:32:152:32:152:Infinity":15,"s:153:22:153:36":55,"s:154:22:154:36":56,"b:156:2:156:Infinity:undefined:undefined:undefined:undefined":16,"s:156:2:156:Infinity":57,"b:156:6:156:22:156:22:156:36":17,"s:156:36:156:Infinity":58,"b:157:2:157:Infinity:undefined:undefined:undefined:undefined":18,"s:157:2:157:Infinity":59,"b:157:6:157:22:157:22:157:36":19,"s:157:36:157:Infinity":60,"b:159:2:159:Infinity:undefined:undefined:undefined:undefined":20,"s:159:2:159:Infinity":61,"s:159:35:159:Infinity":62,"s:161:15:161:60":63,"s:162:17:162:65":64,"s:163:16:163:45":65,"s:165:2:165:Infinity":66,"f:168:9:168:27":9,"s:169:12:169:Infinity":67,"s:170:8:170:Infinity":68,"s:171:8:171:Infinity":69,"s:173:4:177:24":70,"s:178:12:178:58":71,"s:179:2:179:Infinity":72,"f:182:9:182:26":10,"s:183:21:183:Infinity":73,"s:184:20:184:Infinity":74,"s:185:22:185:Infinity":75,"s:187:26:187:Infinity":76,"b:188:2:196:Infinity:undefined:undefined:undefined:undefined":21,"s:188:2:196:Infinity":77,"b:188:6:188:37:188:37:188:66":22,"s:189:25:189:76":78,"f:189:50:189:54":11,"s:189:59:189:74":79,"s:190:25:190:76":80,"f:190:50:190:54":12,"s:190:59:190:74":81,"s:191:18:191:Infinity":82,"s:192:4:193:Infinity":83,"b:193:6:193:Infinity:undefined:undefined:undefined:undefined":23,"s:193:6:193:Infinity":84,"s:193:34:193:Infinity":85,"s:195:18:195:64":86,"s:196:4:196:Infinity":87,"b:196:36:196:54:196:54:196:Infinity":24,"s:199:20:199:Infinity":88,"s:200:2:200:Infinity":89,"f:203:9:203:29":13,"s:204:22:204:Infinity":90,"s:205:22:205:Infinity":91,"s:206:20:209:Infinity":92,"b:207:23:207:60:207:63:207:Infinity":25,"b:208:23:208:60:208:63:208:Infinity":26,"s:210:20:210:Infinity":93,"b:210:38:210:52:210:52:210:56":27,"b:210:56:210:70:210:70:210:73":28,"s:211:20:211:Infinity":94,"s:212:21:212:Infinity":95,"s:213:19:213:Infinity":96,"s:216:17:216:Infinity":97,"b:218:2:221:Infinity:undefined:undefined:undefined:undefined":29,"s:218:2:221:Infinity":98,"b:218:6:218:24:218:24:218:43:218:43:218:61:218:61:218:78":30,"s:219:4:219:Infinity":99,"s:220:20:220:Infinity":100,"s:221:4:221:Infinity":101,"b:221:46:221:52:221:52:221:116":31,"s:225:5:232:Infinity":102,"b:232:51:232:58:232:58:232:Infinity":32,"s:234:2:234:Infinity":103,"f:237:16:237:Infinity":14,"s:244:26:244:Infinity":104,"s:246:18:246:34":105,"s:247:18:247:34":106,"s:249:20:249:54":107,"s:251:53:251:109":108,"s:253:28:253:Infinity":109,"s:255:21:255:76":110,"s:256:20:256:73":111,"s:257:22:257:79":112,"s:258:22:258:85":113,"s:259:22:259:98":114,"s:260:20:262:Infinity":115,"b:261:26:261:66:261:69:261:Infinity":33,"b:262:26:262:66:262:69:262:Infinity":34,"s:264:20:264:79":116,"b:264:38:264:55:264:55:264:59":35,"b:264:59:264:76:264:76:264:79":36,"s:265:20:265:67":117,"s:266:21:266:70":118,"s:267:19:267:64":119,"s:269:22:269:48":120,"s:270:22:270:48":121,"s:272:15:274:Infinity":122,"s:277:32:294:Infinity":123,"s:296:2:302:Infinity":124,"f:305:16:305:41":15,"s:306:2:306:Infinity":125}}} -,"/home/mike/code/ShieldAI/services/hometitle/src/types.ts": {"path":"/home/mike/code/ShieldAI/services/hometitle/src/types.ts","statementMap":{},"fnMap":{},"branchMap":{},"s":{},"f":{},"b":{},"meta":{"lastBranch":0,"lastFunction":0,"lastStatement":0,"seen":{}}} -} diff --git a/services/hometitle/coverage/favicon.png b/services/hometitle/coverage/favicon.png deleted file mode 100644 index c1525b8..0000000 Binary files a/services/hometitle/coverage/favicon.png and /dev/null differ diff --git a/services/hometitle/coverage/index.html b/services/hometitle/coverage/index.html deleted file mode 100644 index 3822708..0000000 --- a/services/hometitle/coverage/index.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - Code coverage report for All files - - - - - - - - - -
          -
          -

          All files

          -
          - -
          - 98.11% - Statements - 208/212 -
          - - -
          - 92.42% - Branches - 122/132 -
          - - -
          - 96.29% - Functions - 26/27 -
          - - -
          - 98.96% - Lines - 191/193 -
          - - -
          -

          - Press n or j to go to the next uncovered block, b, p or k for the previous block. -

          - -
          -
          -
          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          FileStatementsBranchesFunctionsLines
          change-detector.ts -
          -
          98.83%85/8691.07%51/56100%11/1198.73%78/79
          index.ts -
          -
          0%0/00%0/00%0/00%0/0
          matcher.service.ts -
          -
          97.61%123/12693.42%71/7693.75%15/1699.12%113/114
          types.ts -
          -
          0%0/00%0/00%0/00%0/0
          -
          -
          -
          - - - - - - - - \ No newline at end of file diff --git a/services/hometitle/coverage/index.ts.html b/services/hometitle/coverage/index.ts.html deleted file mode 100644 index d516e68..0000000 --- a/services/hometitle/coverage/index.ts.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - Code coverage report for index.ts - - - - - - - - - -
          -
          -

          All files index.ts

          -
          - -
          - 0% - Statements - 0/0 -
          - - -
          - 0% - Branches - 0/0 -
          - - -
          - 0% - Functions - 0/0 -
          - - -
          - 0% - Lines - 0/0 -
          - - -
          -

          - Press n or j to go to the next uncovered block, b, p or k for the previous block. -

          - -
          -
          -
          
          -
          1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
          export {
          -  matchRecords,
          -  getConfigForPropertyType,
          -  parseName,
          -  normalizeString,
          -  normalizeStreetType,
          -  levenshteinDistance,
          -  similarityScore,
          -} from './matcher.service';
          - 
          -export {
          -  detectChanges,
          -  shouldTriggerAlert,
          -  classifyFieldChange,
          -  determineSeverity,
          -  computeChangeConfidence,
          -} from './change-detector';
          - 
          -export type {
          -  PropertyRecord,
          -  Address,
          -  PropertyType,
          -  PropertySnapshot,
          -  MatchResult,
          -  MatchDetails,
          -  FieldMatch,
          -  ChangeDetectionResult,
          -  ChangeType,
          -  Severity,
          -  PropertyChange,
          -  MatchingConfig,
          -  DetectionConfig,
          -  NormalizedTokens,
          -} from './types';
          - 
          - -
          -
          - - - - - - - - \ No newline at end of file diff --git a/services/hometitle/coverage/lcov-report/base.css b/services/hometitle/coverage/lcov-report/base.css deleted file mode 100644 index f418035..0000000 --- a/services/hometitle/coverage/lcov-report/base.css +++ /dev/null @@ -1,224 +0,0 @@ -body, html { - margin:0; padding: 0; - height: 100%; -} -body { - font-family: Helvetica Neue, Helvetica, Arial; - font-size: 14px; - color:#333; -} -.small { font-size: 12px; } -*, *:after, *:before { - -webkit-box-sizing:border-box; - -moz-box-sizing:border-box; - box-sizing:border-box; - } -h1 { font-size: 20px; margin: 0;} -h2 { font-size: 14px; } -pre { - font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace; - margin: 0; - padding: 0; - -moz-tab-size: 2; - -o-tab-size: 2; - tab-size: 2; -} -a { color:#0074D9; text-decoration:none; } -a:hover { text-decoration:underline; } -.strong { font-weight: bold; } -.space-top1 { padding: 10px 0 0 0; } -.pad2y { padding: 20px 0; } -.pad1y { padding: 10px 0; } -.pad2x { padding: 0 20px; } -.pad2 { padding: 20px; } -.pad1 { padding: 10px; } -.space-left2 { padding-left:55px; } -.space-right2 { padding-right:20px; } -.center { text-align:center; } -.clearfix { display:block; } -.clearfix:after { - content:''; - display:block; - height:0; - clear:both; - visibility:hidden; - } -.fl { float: left; } -@media only screen and (max-width:640px) { - .col3 { width:100%; max-width:100%; } - .hide-mobile { display:none!important; } -} - -.quiet { - color: #7f7f7f; - color: rgba(0,0,0,0.5); -} -.quiet a { opacity: 0.7; } - -.fraction { - font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace; - font-size: 10px; - color: #555; - background: #E8E8E8; - padding: 4px 5px; - border-radius: 3px; - vertical-align: middle; -} - -div.path a:link, div.path a:visited { color: #333; } -table.coverage { - border-collapse: collapse; - margin: 10px 0 0 0; - padding: 0; -} - -table.coverage td { - margin: 0; - padding: 0; - vertical-align: top; -} -table.coverage td.line-count { - text-align: right; - padding: 0 5px 0 20px; -} -table.coverage td.line-coverage { - text-align: right; - padding-right: 10px; - min-width:20px; -} - -table.coverage td span.cline-any { - display: inline-block; - padding: 0 5px; - width: 100%; -} -.missing-if-branch { - display: inline-block; - margin-right: 5px; - border-radius: 3px; - position: relative; - padding: 0 4px; - background: #333; - color: yellow; -} - -.skip-if-branch { - display: none; - margin-right: 10px; - position: relative; - padding: 0 4px; - background: #ccc; - color: white; -} -.missing-if-branch .typ, .skip-if-branch .typ { - color: inherit !important; -} -.coverage-summary { - border-collapse: collapse; - width: 100%; -} -.coverage-summary tr { border-bottom: 1px solid #bbb; } -.keyline-all { border: 1px solid #ddd; } -.coverage-summary td, .coverage-summary th { padding: 10px; } -.coverage-summary tbody { border: 1px solid #bbb; } -.coverage-summary td { border-right: 1px solid #bbb; } -.coverage-summary td:last-child { border-right: none; } -.coverage-summary th { - text-align: left; - font-weight: normal; - white-space: nowrap; -} -.coverage-summary th.file { border-right: none !important; } -.coverage-summary th.pct { } -.coverage-summary th.pic, -.coverage-summary th.abs, -.coverage-summary td.pct, -.coverage-summary td.abs { text-align: right; } -.coverage-summary td.file { white-space: nowrap; } -.coverage-summary td.pic { min-width: 120px !important; } -.coverage-summary tfoot td { } - -.coverage-summary .sorter { - height: 10px; - width: 7px; - display: inline-block; - margin-left: 0.5em; - background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent; -} -.coverage-summary .sorted .sorter { - background-position: 0 -20px; -} -.coverage-summary .sorted-desc .sorter { - background-position: 0 -10px; -} -.status-line { height: 10px; } -/* yellow */ -.cbranch-no { background: yellow !important; color: #111; } -/* dark red */ -.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 } -.low .chart { border:1px solid #C21F39 } -.highlighted, -.highlighted .cstat-no, .highlighted .fstat-no, .highlighted .cbranch-no{ - background: #C21F39 !important; -} -/* medium red */ -.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE } -/* light red */ -.low, .cline-no { background:#FCE1E5 } -/* light green */ -.high, .cline-yes { background:rgb(230,245,208) } -/* medium green */ -.cstat-yes { background:rgb(161,215,106) } -/* dark green */ -.status-line.high, .high .cover-fill { background:rgb(77,146,33) } -.high .chart { border:1px solid rgb(77,146,33) } -/* dark yellow (gold) */ -.status-line.medium, .medium .cover-fill { background: #f9cd0b; } -.medium .chart { border:1px solid #f9cd0b; } -/* light yellow */ -.medium { background: #fff4c2; } - -.cstat-skip { background: #ddd; color: #111; } -.fstat-skip { background: #ddd; color: #111 !important; } -.cbranch-skip { background: #ddd !important; color: #111; } - -span.cline-neutral { background: #eaeaea; } - -.coverage-summary td.empty { - opacity: .5; - padding-top: 4px; - padding-bottom: 4px; - line-height: 1; - color: #888; -} - -.cover-fill, .cover-empty { - display:inline-block; - height: 12px; -} -.chart { - line-height: 0; -} -.cover-empty { - background: white; -} -.cover-full { - border-right: none !important; -} -pre.prettyprint { - border: none !important; - padding: 0 !important; - margin: 0 !important; -} -.com { color: #999 !important; } -.ignore-none { color: #999; font-weight: normal; } - -.wrapper { - min-height: 100%; - height: auto !important; - height: 100%; - margin: 0 auto -48px; -} -.footer, .push { - height: 48px; -} diff --git a/services/hometitle/coverage/lcov-report/block-navigation.js b/services/hometitle/coverage/lcov-report/block-navigation.js deleted file mode 100644 index 530d1ed..0000000 --- a/services/hometitle/coverage/lcov-report/block-navigation.js +++ /dev/null @@ -1,87 +0,0 @@ -/* eslint-disable */ -var jumpToCode = (function init() { - // Classes of code we would like to highlight in the file view - var missingCoverageClasses = ['.cbranch-no', '.cstat-no', '.fstat-no']; - - // Elements to highlight in the file listing view - var fileListingElements = ['td.pct.low']; - - // We don't want to select elements that are direct descendants of another match - var notSelector = ':not(' + missingCoverageClasses.join('):not(') + ') > '; // becomes `:not(a):not(b) > ` - - // Selector that finds elements on the page to which we can jump - var selector = - fileListingElements.join(', ') + - ', ' + - notSelector + - missingCoverageClasses.join(', ' + notSelector); // becomes `:not(a):not(b) > a, :not(a):not(b) > b` - - // The NodeList of matching elements - var missingCoverageElements = document.querySelectorAll(selector); - - var currentIndex; - - function toggleClass(index) { - missingCoverageElements - .item(currentIndex) - .classList.remove('highlighted'); - missingCoverageElements.item(index).classList.add('highlighted'); - } - - function makeCurrent(index) { - toggleClass(index); - currentIndex = index; - missingCoverageElements.item(index).scrollIntoView({ - behavior: 'smooth', - block: 'center', - inline: 'center' - }); - } - - function goToPrevious() { - var nextIndex = 0; - if (typeof currentIndex !== 'number' || currentIndex === 0) { - nextIndex = missingCoverageElements.length - 1; - } else if (missingCoverageElements.length > 1) { - nextIndex = currentIndex - 1; - } - - makeCurrent(nextIndex); - } - - function goToNext() { - var nextIndex = 0; - - if ( - typeof currentIndex === 'number' && - currentIndex < missingCoverageElements.length - 1 - ) { - nextIndex = currentIndex + 1; - } - - makeCurrent(nextIndex); - } - - return function jump(event) { - if ( - document.getElementById('fileSearch') === document.activeElement && - document.activeElement != null - ) { - // if we're currently focused on the search input, we don't want to navigate - return; - } - - switch (event.which) { - case 78: // n - case 74: // j - goToNext(); - break; - case 66: // b - case 75: // k - case 80: // p - goToPrevious(); - break; - } - }; -})(); -window.addEventListener('keydown', jumpToCode); diff --git a/services/hometitle/coverage/lcov-report/change-detector.ts.html b/services/hometitle/coverage/lcov-report/change-detector.ts.html deleted file mode 100644 index f10287e..0000000 --- a/services/hometitle/coverage/lcov-report/change-detector.ts.html +++ /dev/null @@ -1,691 +0,0 @@ - - - - - - Code coverage report for change-detector.ts - - - - - - - - - -
          -
          -

          All files change-detector.ts

          -
          - -
          - 98.83% - Statements - 85/86 -
          - - -
          - 91.07% - Branches - 51/56 -
          - - -
          - 100% - Functions - 11/11 -
          - - -
          - 98.73% - Lines - 78/79 -
          - - -
          -

          - Press n or j to go to the next uncovered block, b, p or k for the previous block. -

          - -
          -
          -
          
          -
          1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203  -  -  -  -  -  -  -  -  -  -  -1x -  -  -  -  -  -  -  -  -11x -  -5x -  -  -  -  -  -5x -  -2x -2x -  -2x -2x -  -1x -1x -  -1x -1x -  -  -  -  -11x -  -  -  -5x -  -  -  -  -  -  -  -5x -5x -  -  -  -15x -  -15x -  -  -  -  -  -  -  -15x -  -15x -14x -14x -14x -  -  -10x -9x -9x -  -  -5x -  -  -  -15x -  -13x -13x -16x -  -6x -6x -  -3x -3x -  -3x -3x -3x -3x -3x -  -  -2x -2x -  -2x -  -  -  -13x -  -  -  -  -  -  -  -11x -11x -  -11x -  -  -  -  -  -  -  -  -11x -66x -66x -  -66x -11x -  -  -  -11x -11x -  -11x -11x -  -11x -11x -4x -7x -2x -5x -1x -4x -1x -  -  -11x -  -  -  -  -  -  -  -  -  -  -  -  -11x -  -11x -  -11x -77x -77x -77x -1x -  -  -  -  -  -  -  -  -11x -  -  -  -5x -5x -5x -5x -  -  -  - 
          import {
          -  PropertySnapshot,
          -  ChangeDetectionResult,
          -  ChangeType,
          -  Severity,
          -  PropertyChange,
          -  DetectionConfig,
          -  Address,
          -} from './types';
          -import { matchRecords } from './matcher.service';
          - 
          -const DEFAULT_DETECTION_CONFIG: DetectionConfig = {
          -  ownershipNameThreshold: 0.7,
          -  deedDateSensitivity: 0.9,
          -  taxAmountChangePercent: 15,
          -};
          - 
          -function classifyFieldChange(field: string, oldValue: unknown, newValue: unknown, config: DetectionConfig): PropertyChange {
          -  let changeType: ChangeType;
          - 
          -  switch (field) {
          -    case 'ownerName':
          -      changeType =
          -        typeof oldValue === 'string' && typeof newValue === 'string'
          -          ? isSignificantNameChange(oldValue, newValue, config)
          -            ? 'ownership_transfer'
          -            : 'metadata_change'
          -          : 'ownership_transfer';
          -      break;
          -    case 'deedDate':
          -      changeType = 'deed_change';
          -      break;
          -    case 'taxAmount':
          -      changeType = 'tax_change';
          -      break;
          -    case 'lienCount':
          -      changeType = (newValue as number) > (oldValue as number) ? 'lien_filing' : 'metadata_change';
          -      break;
          -    case 'taxId':
          -      changeType = 'deed_change';
          -      break;
          -    default:
          -      changeType = 'metadata_change';
          -  }
          - 
          -  return { field, oldValue, newValue, changeType };
          -}
          - 
          -function isSignificantNameChange(oldName: string, newName: string, config: DetectionConfig): boolean {
          -  const dummyAddress: Address = {
          -    streetNumber: '0',
          -    streetName: 'dummy',
          -    city: 'dummy',
          -    state: 'XX',
          -    zip: '00000',
          -  };
          - 
          -  const result = matchRecords(oldName, dummyAddress, newName, dummyAddress);
          -  return result.nameScore < config.ownershipNameThreshold;
          -}
          - 
          -function determineSeverity(changes: PropertyChange[], config: DetectionConfig): Severity {
          -  const severityOverrides = config.severityOverrides || {};
          - 
          -  const typeToSeverity: Record<ChangeType, Severity> = {
          -    ownership_transfer: severityOverrides['ownership_transfer'] || 'major',
          -    deed_change: severityOverrides['deed_change'] || 'moderate',
          -    lien_filing: severityOverrides['lien_filing'] || 'moderate',
          -    tax_change: severityOverrides['tax_change'] || 'minor',
          -    metadata_change: severityOverrides['metadata_change'] || 'minor',
          -  };
          - 
          -  const severityOrder: Severity[] = ['major', 'moderate', 'minor'];
          - 
          -  for (const change of changes) {
          -    const sev = typeToSeverity[change.changeType];
          -    const idx = severityOrder.indexOf(sev);
          -    if (idx === 0) return 'major';
          -  }
          - 
          -  for (const change of changes) {
          -    const sev = typeToSeverity[change.changeType];
          -    if (sev === 'moderate') return 'moderate';
          -  }
          - 
          -  return 'minor';
          -}
          - 
          -function computeChangeConfidence(changes: PropertyChange[], config: DetectionConfig): number {
          -  if (changes.length === 0) return 0;
          - 
          -  let totalConfidence = 0;
          -  for (const change of changes) {
          -    switch (change.changeType) {
          -      case 'ownership_transfer':
          -        totalConfidence += 0.95;
          -        break;
          -      case 'deed_change':
          -        totalConfidence += config.deedDateSensitivity;
          -        break;
          -      case 'tax_change': {
          -        const oldVal = change.oldValue as number;
          -        const newVal = change.newValue as number;
          -        const pctChange = oldVal ? Math.abs(newVal - oldVal) / oldVal * 100 : 100;
          -        totalConfidence += pctChange >= config.taxAmountChangePercent ? 0.85 : 0.5;
          -        break;
          -      }
          -      case 'lien_filing':
          -        totalConfidence += 0.9;
          -        break;
          -      default:
          -        totalConfidence += 0.4;
          -    }
          -  }
          - 
          -  return Math.round((totalConfidence / changes.length) * 1000) / 1000;
          -}
          - 
          -export function detectChanges(
          -  previous: PropertySnapshot,
          -  current: PropertySnapshot,
          -  config?: Partial<DetectionConfig>,
          -): ChangeDetectionResult {
          -  const effectiveConfig = { ...DEFAULT_DETECTION_CONFIG, ...config };
          -  const changes: PropertyChange[] = [];
          - 
          -  const fieldsToCompare: (keyof Omit<PropertySnapshot, 'id' | 'capturedAt' | 'propertyId'>)[] = [
          -    'ownerName',
          -    'deedDate',
          -    'taxId',
          -    'taxAmount',
          -    'lienCount',
          -    'propertyType',
          -  ];
          - 
          -  for (const field of fieldsToCompare) {
          -    const oldValue = previous[field];
          -    const newValue = current[field];
          - 
          -    if (oldValue !== newValue) {
          -      changes.push(classifyFieldChange(field, oldValue, newValue, effectiveConfig));
          -    }
          -  }
          - 
          -  const addressChanges = detectAddressChanges(previous.address, current.address);
          -  changes.push(...addressChanges);
          - 
          -  const severity = determineSeverity(changes, effectiveConfig);
          -  const confidence = computeChangeConfidence(changes, effectiveConfig);
          - 
          -  let changeType: ChangeType = 'metadata_change';
          -  if (changes.some(c => c.changeType === 'ownership_transfer')) {
          -    changeType = 'ownership_transfer';
          -  } else if (changes.some(c => c.changeType === 'deed_change')) {
          -    changeType = 'deed_change';
          -  } else if (changes.some(c => c.changeType === 'lien_filing')) {
          -    changeType = 'lien_filing';
          -  } else if (changes.some(c => c.changeType === 'tax_change')) {
          -    changeType = 'tax_change';
          -  }
          - 
          -  return {
          -    propertyId: previous.propertyId,
          -    changeType,
          -    severity,
          -    confidence,
          -    changes,
          -    previousSnapshot: previous,
          -    currentSnapshot: current,
          -    detectedAt: new Date().toISOString(),
          -  };
          -}
          - 
          -function detectAddressChanges(oldAddr: Address, newAddr: Address): PropertyChange[] {
          -  const changes: PropertyChange[] = [];
          - 
          -  const addressFields: (keyof Address)[] = ['streetNumber', 'streetName', 'streetType', 'unit', 'city', 'state', 'zip'];
          - 
          -  for (const field of addressFields) {
          -    const oldVal = oldAddr[field];
          -    const newVal = newAddr[field];
          -    if (oldVal !== newVal) {
          -      changes.push({
          -        field: `address.${field}`,
          -        oldValue: oldVal,
          -        newValue: newVal,
          -        changeType: 'metadata_change',
          -      });
          -    }
          -  }
          - 
          -  return changes;
          -}
          - 
          -export function shouldTriggerAlert(result: ChangeDetectionResult, minSeverity: Severity = 'moderate'): boolean {
          -  const severityOrder: Severity[] = ['minor', 'moderate', 'major'];
          -  const resultIdx = severityOrder.indexOf(result.severity);
          -  const minIdx = severityOrder.indexOf(minSeverity);
          -  return resultIdx >= minIdx && result.confidence >= 0.7;
          -}
          - 
          -export { classifyFieldChange, determineSeverity, computeChangeConfidence };
          - 
          - -
          -
          - - - - - - - - \ No newline at end of file diff --git a/services/hometitle/coverage/lcov-report/favicon.png b/services/hometitle/coverage/lcov-report/favicon.png deleted file mode 100644 index c1525b8..0000000 Binary files a/services/hometitle/coverage/lcov-report/favicon.png and /dev/null differ diff --git a/services/hometitle/coverage/lcov-report/index.html b/services/hometitle/coverage/lcov-report/index.html deleted file mode 100644 index f9d2698..0000000 --- a/services/hometitle/coverage/lcov-report/index.html +++ /dev/null @@ -1,161 +0,0 @@ - - - - - - Code coverage report for All files - - - - - - - - - -
          -
          -

          All files

          -
          - -
          - 98.11% - Statements - 208/212 -
          - - -
          - 92.42% - Branches - 122/132 -
          - - -
          - 96.29% - Functions - 26/27 -
          - - -
          - 98.96% - Lines - 191/193 -
          - - -
          -

          - Press n or j to go to the next uncovered block, b, p or k for the previous block. -

          - -
          -
          -
          - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
          FileStatementsBranchesFunctionsLines
          change-detector.ts -
          -
          98.83%85/8691.07%51/56100%11/1198.73%78/79
          index.ts -
          -
          0%0/00%0/00%0/00%0/0
          matcher.service.ts -
          -
          97.61%123/12693.42%71/7693.75%15/1699.12%113/114
          types.ts -
          -
          0%0/00%0/00%0/00%0/0
          -
          -
          -
          - - - - - - - - \ No newline at end of file diff --git a/services/hometitle/coverage/lcov-report/index.ts.html b/services/hometitle/coverage/lcov-report/index.ts.html deleted file mode 100644 index 0adb0d0..0000000 --- a/services/hometitle/coverage/lcov-report/index.ts.html +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - Code coverage report for index.ts - - - - - - - - - -
          -
          -

          All files index.ts

          -
          - -
          - 0% - Statements - 0/0 -
          - - -
          - 0% - Branches - 0/0 -
          - - -
          - 0% - Functions - 0/0 -
          - - -
          - 0% - Lines - 0/0 -
          - - -
          -

          - Press n or j to go to the next uncovered block, b, p or k for the previous block. -

          - -
          -
          -
          
          -
          1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
          export {
          -  matchRecords,
          -  getConfigForPropertyType,
          -  parseName,
          -  normalizeString,
          -  normalizeStreetType,
          -  levenshteinDistance,
          -  similarityScore,
          -} from './matcher.service';
          - 
          -export {
          -  detectChanges,
          -  shouldTriggerAlert,
          -  classifyFieldChange,
          -  determineSeverity,
          -  computeChangeConfidence,
          -} from './change-detector';
          - 
          -export type {
          -  PropertyRecord,
          -  Address,
          -  PropertyType,
          -  PropertySnapshot,
          -  MatchResult,
          -  MatchDetails,
          -  FieldMatch,
          -  ChangeDetectionResult,
          -  ChangeType,
          -  Severity,
          -  PropertyChange,
          -  MatchingConfig,
          -  DetectionConfig,
          -  NormalizedTokens,
          -} from './types';
          - 
          - -
          -
          - - - - - - - - \ No newline at end of file diff --git a/services/hometitle/coverage/lcov-report/matcher.service.ts.html b/services/hometitle/coverage/lcov-report/matcher.service.ts.html deleted file mode 100644 index 9000dbd..0000000 --- a/services/hometitle/coverage/lcov-report/matcher.service.ts.html +++ /dev/null @@ -1,1012 +0,0 @@ - - - - - - Code coverage report for matcher.service.ts - - - - - - - - - -
          -
          -

          All files matcher.service.ts

          -
          - -
          - 97.61% - Statements - 123/126 -
          - - -
          - 93.42% - Branches - 71/76 -
          - - -
          - 93.75% - Functions - 15/16 -
          - - -
          - 99.12% - Lines - 113/114 -
          - - -
          -

          - Press n or j to go to the next uncovered block, b, p or k for the previous block. -

          - -
          -
          -
          
          -
          1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310  -  -  -  -  -  -  -  -  -  -2x -  -  -  -  -  -  -2x -  -  -  -  -2x -  -  -  -  -2x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -2x -  -  -  -  -  -  -  -56x -2622x -  -  -56x -323x -1950x -1950x -  -  -  -  -  -  -  -56x -  -  -  -39x -38x -  -  -  -848x -  -  -  -  -  -  -  -  -37x -37x -  -37x -37x -37x -37x -  -37x -  -36x -36x -2x -  -  -36x -36x -2x -  -  -36x -  -36x -1x -35x -31x -31x -  -4x -4x -4x -  -  -36x -  -  -36x -4x -4x -4x -  -  -  -36x -  -  -  -69x -69x -  -  -  -30x -  -  -  -  -  -  -  -  -30x -  -  -  -300x -300x -300x -  -300x -252x -  -250x -  -36x -36x -36x -  -36x -  -  -  -9x -9x -9x -  -9x -  -  -  -  -9x -9x -  -  -  -15x -15x -15x -  -15x -15x -1x -1x -1x -1x -1x -  -1x -1x -  -  -15x -15x -  -  -  -15x -15x -15x -  -  -  -15x -15x -15x -15x -  -  -15x -  -15x -9x -9x -9x -  -  -  -15x -  -  -  -  -  -  -  -  -15x -  -  -  -  -  -  -  -  -  -15x -  -15x -15x -  -15x -  -15x -  -15x -  -15x -15x -15x -15x -15x -15x -  -  -  -15x -15x -15x -15x -  -15x -15x -  -15x -  -  -  -  -15x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -15x -  -  -  -  -  -  -  -  -  -3x -  -  -  - 
          import {
          -  Address,
          -  MatchResult,
          -  MatchDetails,
          -  FieldMatch,
          -  MatchingConfig,
          -  NormalizedTokens,
          -  PropertyType,
          -} from './types';
          - 
          -const DEFAULT_CONFIG: MatchingConfig = {
          -  nameThreshold: 0.85,
          -  addressThreshold: 0.9,
          -  overallThreshold: 0.85,
          -  geocodingRadiusMeters: 100,
          -};
          - 
          -const COMMON_PREFIXES = new Set([
          -  'mr', 'mrs', 'ms', 'miss', 'dr', 'prof', 'jr', 'sr', 'junior', 'senior',
          -  'ii', 'iii', 'iv', 'rev', 'st', 'hon', 'esq',
          -]);
          - 
          -const COMMON_SUFFIXES = new Set([
          -  'jr', 'sr', 'junior', 'senior', 'ii', 'iii', 'iv', 'v', 'esq',
          -  'phd', 'md', 'llm', 'cpa',
          -]);
          - 
          -const STREET_TYPE_MAP: Record<string, string> = {
          -  'st': 'street', 'street': 'street',
          -  'ave': 'avenue', 'avenue': 'avenue',
          -  'blvd': 'boulevard', 'boulevard': 'boulevard',
          -  'dr': 'drive', 'drive': 'drive',
          -  'ln': 'lane', 'lane': 'lane',
          -  'ct': 'court', 'court': 'court',
          -  'pl': 'place', 'place': 'place',
          -  'rd': 'road', 'road': 'road',
          -  'way': 'way',
          -  'trl': 'trail', 'trail': 'trail',
          -  'hwy': 'highway', 'highway': 'highway',
          -  'pkwy': 'parkway', 'parkway': 'parkway',
          -  'cir': 'circle', 'circle': 'circle',
          -  'sq': 'square', 'square': 'square',
          -  'ter': 'terrace', 'terrace': 'terrace',
          -};
          - 
          -const PROPERTY_TYPE_CONFIGS: Record<PropertyType, Partial<MatchingConfig>> = {
          -  'residential': { nameThreshold: 0.85, addressThreshold: 0.9 },
          -  'commercial': { nameThreshold: 0.8, addressThreshold: 0.9 },
          -  'land': { nameThreshold: 0.8, addressThreshold: 0.85 },
          -  'multi-family': { nameThreshold: 0.8, addressThreshold: 0.9 },
          -};
          - 
          -function levenshteinDistance(a: string, b: string): number {
          -  const matrix: number[][] = Array.from({ length: b.length + 1 }, (_, i) =>
          -    Array.from({ length: a.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
          -  );
          - 
          -  for (let i = 1; i <= b.length; i++) {
          -    for (let j = 1; j <= a.length; j++) {
          -      const cost = a[j - 1] === b[i - 1] ? 0 : 1;
          -      matrix[i][j] = Math.min(
          -        matrix[i - 1][j] + 1,
          -        matrix[i][j - 1] + 1,
          -        matrix[i - 1][j - 1] + cost,
          -      );
          -    }
          -  }
          - 
          -  return matrix[b.length][a.length];
          -}
          - 
          -function similarityScore(distance: number, maxLen: number): number {
          -  if (maxLen === 0) return 1.0;
          -  return 1.0 - distance / maxLen;
          -}
          - 
          -function normalizeString(str: string): string {
          -  return str
          -    .toLowerCase()
          -    .replace(/[''']/g, '')
          -    .replace(/[^a-z0-9\s]/g, ' ')
          -    .replace(/\s+/g, ' ')
          -    .trim();
          -}
          - 
          -function parseName(name: string): NormalizedTokens {
          -  const clean = normalizeString(name);
          -  const parts = clean.split(' ').filter(Boolean);
          - 
          -  let firstName = '';
          -  let lastName = '';
          -  let middleName = '';
          -  const initials: string[] = [];
          - 
          -  if (parts.length === 0) return { firstName, lastName, middleName, initials };
          - 
          -  let startIdx = 0;
          -  while (startIdx < parts.length && COMMON_PREFIXES.has(parts[startIdx])) {
          -    startIdx++;
          -  }
          - 
          -  let endIdx = parts.length;
          -  while (endIdx > startIdx + 1 && COMMON_SUFFIXES.has(parts[endIdx - 1])) {
          -    endIdx--;
          -  }
          - 
          -  const coreParts = parts.slice(startIdx, endIdx);
          - 
          -  if (coreParts.length === 1) {
          -    lastName = coreParts[0];
          -  } else if (coreParts.length === 2) {
          -    firstName = coreParts[0];
          -    lastName = coreParts[1];
          -  } else {
          -    firstName = coreParts[0];
          -    lastName = coreParts[coreParts.length - 1];
          -    middleName = coreParts.slice(1, -1).join(' ');
          -  }
          - 
          -  Iif (firstName.length === 1) {
          -    initials.push(firstName);
          -  }
          -  if (middleName) {
          -    const middleParts = middleName.split(' ');
          -    for (const mp of middleParts) {
          -      if (mp.length === 1) initials.push(mp);
          -    }
          -  }
          - 
          -  return { firstName, lastName, middleName, initials };
          -}
          - 
          -function normalizeStreetType(type: string): string {
          -  const clean = normalizeString(type);
          -  return STREET_TYPE_MAP[clean] || clean;
          -}
          - 
          -function normalizeAddress(addr: Address): string {
          -  const parts = [
          -    addr.streetNumber,
          -    normalizeString(addr.streetName),
          -    addr.streetType ? normalizeStreetType(addr.streetType) : '',
          -    addr.unit ? normalizeString(addr.unit) : '',
          -    normalizeString(addr.city),
          -    addr.state.toLowerCase(),
          -    addr.zip,
          -  ].filter(Boolean);
          -  return parts.join(' ');
          -}
          - 
          -function computeFieldMatch(valueA: string, valueB: string, normalizeFn?: (v: string) => string): FieldMatch {
          -  const normFn = normalizeFn || normalizeString;
          -  const normalizedA = normFn(valueA);
          -  const normalizedB = normFn(valueB);
          - 
          -  if (!normalizedA && !normalizedB) return { valueA, valueB, normalizedA, normalizedB, score: 1.0 };
          -  if (!normalizedA || !normalizedB) return { valueA, valueB, normalizedA, normalizedB, score: 0.0 };
          - 
          -  if (normalizedA === normalizedB) return { valueA, valueB, normalizedA, normalizedB, score: 1.0 };
          - 
          -  const dist = levenshteinDistance(normalizedA, normalizedB);
          -  const maxLen = Math.max(normalizedA.length, normalizedB.length);
          -  const score = similarityScore(dist, maxLen);
          - 
          -  return { valueA, valueB, normalizedA, normalizedB, score: Math.round(score * 1000) / 1000 };
          -}
          - 
          -function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
          -  const R = 6371000;
          -  const dLat = ((lat2 - lat1) * Math.PI) / 180;
          -  const dLon = ((lon2 - lon1) * Math.PI) / 180;
          -  const a =
          -    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
          -    Math.cos((lat1 * Math.PI) / 180) *
          -      Math.cos((lat2 * Math.PI) / 180) *
          -      Math.sin(dLon / 2) *
          -      Math.sin(dLon / 2);
          -  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
          -  return R * c;
          -}
          - 
          -function computeNameScore(tokensA: NormalizedTokens, tokensB: NormalizedTokens): number {
          -  const firstScore = computeFieldMatch(tokensA.firstName, tokensB.firstName).score;
          -  const lastScore = computeFieldMatch(tokensA.lastName, tokensB.lastName).score;
          -  const middleScore = computeFieldMatch(tokensA.middleName, tokensB.middleName).score;
          - 
          -  let initialMatchScore = 1.0;
          -  if (tokensA.initials.length > 0 || tokensB.initials.length > 0) {
          -    const allInitialsA = new Set(tokensA.initials.map(i => i.toLowerCase()));
          -    const allInitialsB = new Set(tokensB.initials.map(i => i.toLowerCase()));
          -    let matched = 0;
          -    for (const init of allInitialsA) {
          -      Iif (allInitialsB.has(init)) matched++;
          -    }
          -    const total = Math.max(allInitialsA.size, allInitialsB.size);
          -    initialMatchScore = total > 0 ? matched / total : 1.0;
          -  }
          - 
          -  const weighted = (lastScore * 0.45) + (firstScore * 0.35) + (middleScore * 0.1) + (initialMatchScore * 0.1);
          -  return Math.round(weighted * 1000) / 1000;
          -}
          - 
          -function computeAddressScore(addrA: Address, addrB: Address, config: MatchingConfig): { score: number; geocodingDistance?: number } {
          -  const numberMatch = computeFieldMatch(addrA.streetNumber, addrB.streetNumber).score;
          -  const streetMatch = computeFieldMatch(addrA.streetName, addrB.streetName, normalizeString).score;
          -  const typeMatch = computeFieldMatch(
          -    addrA.streetType ? normalizeStreetType(addrA.streetType) : '',
          -    addrB.streetType ? normalizeStreetType(addrB.streetType) : '',
          -  ).score;
          -  const unitMatch = computeFieldMatch(addrA.unit || '', addrB.unit || '').score;
          -  const cityMatch = computeFieldMatch(addrA.city, addrB.city).score;
          -  const stateMatch = computeFieldMatch(addrA.state, addrB.state).score;
          -  const zipMatch = computeFieldMatch(addrA.zip, addrB.zip).score;
          - 
          -  let geocodingDistance: number | undefined;
          -  let geoScore = 0.0;
          - 
          -  if (addrA.latitude && addrA.longitude && addrB.latitude && addrB.longitude) {
          -    geocodingDistance = haversineDistance(addrA.latitude, addrA.longitude, addrB.latitude, addrB.longitude);
          -    const maxDist = config.geocodingRadiusMeters;
          -    geoScore = geocodingDistance <= maxDist ? 1.0 : Math.max(0, 1.0 - (geocodingDistance - maxDist) / (maxDist * 5));
          -  }
          - 
          -  const weighted =
          -    (numberMatch * 0.2) +
          -    (streetMatch * 0.25) +
          -    (typeMatch * 0.1) +
          -    (unitMatch * 0.1) +
          -    (cityMatch * 0.1) +
          -    (stateMatch * 0.1) +
          -    (zipMatch * 0.1) +
          -    (geoScore * (geocodingDistance !== undefined ? 0.05 : 0));
          - 
          -  return { score: Math.round(weighted * 1000) / 1000, geocodingDistance };
          -}
          - 
          -export function matchRecords(
          -  nameA: string,
          -  addressA: Address,
          -  nameB: string,
          -  addressB: Address,
          -  config?: Partial<MatchingConfig>,
          -): MatchResult {
          -  const effectiveConfig = { ...DEFAULT_CONFIG, ...config };
          - 
          -  const tokensA = parseName(nameA);
          -  const tokensB = parseName(nameB);
          - 
          -  const nameScore = computeNameScore(tokensA, tokensB);
          - 
          -  const { score: addressScore, geocodingDistance } = computeAddressScore(addressA, addressB, effectiveConfig);
          - 
          -  const overallConfidence = Math.round((nameScore * 0.5 + addressScore * 0.5) * 1000) / 1000;
          - 
          -  const firstMatch = computeFieldMatch(tokensA.firstName, tokensB.firstName);
          -  const lastMatch = computeFieldMatch(tokensA.lastName, tokensB.lastName);
          -  const middleMatch = computeFieldMatch(tokensA.middleName, tokensB.middleName);
          -  const numberMatch = computeFieldMatch(addressA.streetNumber, addressB.streetNumber);
          -  const streetMatch = computeFieldMatch(addressA.streetName, addressB.streetName, normalizeString);
          -  const typeMatch = computeFieldMatch(
          -    addressA.streetType ? normalizeStreetType(addressA.streetType) : '',
          -    addressB.streetType ? normalizeStreetType(addressB.streetType) : '',
          -  );
          -  const unitMatch = computeFieldMatch(addressA.unit || '', addressB.unit || '');
          -  const cityMatch = computeFieldMatch(addressA.city, addressB.city);
          -  const stateMatch = computeFieldMatch(addressA.state, addressB.state);
          -  const zipMatch = computeFieldMatch(addressA.zip, addressB.zip);
          - 
          -  const normalizedA = normalizeAddress(addressA);
          -  const normalizedB = normalizeAddress(addressB);
          - 
          -  const dist = levenshteinDistance(
          -    normalizeString(nameA),
          -    normalizeString(nameB),
          -  );
          - 
          -  const details: MatchDetails = {
          -    nameNormalized: [normalizeString(nameA), normalizeString(nameB)],
          -    addressNormalized: [normalizedA, normalizedB],
          -    levenshteinDistance: dist,
          -    geocodingDistance,
          -    fields: {
          -      firstName: firstMatch,
          -      lastName: lastMatch,
          -      middleName: middleMatch,
          -      streetNumber: numberMatch,
          -      streetName: streetMatch,
          -      streetType: typeMatch,
          -      unit: unitMatch,
          -      city: cityMatch,
          -      state: stateMatch,
          -      zip: zipMatch,
          -    },
          -  };
          - 
          -  return {
          -    nameScore,
          -    addressScore,
          -    overallConfidence,
          -    isMatch: overallConfidence >= effectiveConfig.overallThreshold,
          -    details,
          -  };
          -}
          - 
          -export function getConfigForPropertyType(type: PropertyType): MatchingConfig {
          -  return { ...DEFAULT_CONFIG, ...PROPERTY_TYPE_CONFIGS[type] };
          -}
          - 
          -export { parseName, normalizeString, normalizeStreetType, levenshteinDistance, similarityScore };
          - 
          - -
          -
          - - - - - - - - \ No newline at end of file diff --git a/services/hometitle/coverage/lcov-report/prettify.css b/services/hometitle/coverage/lcov-report/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/services/hometitle/coverage/lcov-report/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/services/hometitle/coverage/lcov-report/prettify.js b/services/hometitle/coverage/lcov-report/prettify.js deleted file mode 100644 index b322523..0000000 --- a/services/hometitle/coverage/lcov-report/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/services/hometitle/coverage/lcov-report/sort-arrow-sprite.png b/services/hometitle/coverage/lcov-report/sort-arrow-sprite.png deleted file mode 100644 index 6ed6831..0000000 Binary files a/services/hometitle/coverage/lcov-report/sort-arrow-sprite.png and /dev/null differ diff --git a/services/hometitle/coverage/lcov-report/sorter.js b/services/hometitle/coverage/lcov-report/sorter.js deleted file mode 100644 index 4ed70ae..0000000 --- a/services/hometitle/coverage/lcov-report/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/services/hometitle/coverage/lcov-report/types.ts.html b/services/hometitle/coverage/lcov-report/types.ts.html deleted file mode 100644 index c1f861a..0000000 --- a/services/hometitle/coverage/lcov-report/types.ts.html +++ /dev/null @@ -1,427 +0,0 @@ - - - - - - Code coverage report for types.ts - - - - - - - - - -
          -
          -

          All files types.ts

          -
          - -
          - 0% - Statements - 0/0 -
          - - -
          - 0% - Branches - 0/0 -
          - - -
          - 0% - Functions - 0/0 -
          - - -
          - 0% - Lines - 0/0 -
          - - -
          -

          - Press n or j to go to the next uncovered block, b, p or k for the previous block. -

          - -
          -
          -
          
          -
          1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
          export interface PropertyRecord {
          -  id: string;
          -  ownerName: string;
          -  address: Address;
          -  deedDate?: string;
          -  taxId?: string;
          -  propertyType: PropertyType;
          -  metadata?: Record<string, unknown>;
          -}
          - 
          -export interface Address {
          -  streetNumber: string;
          -  streetName: string;
          -  streetType?: string;
          -  unit?: string;
          -  city: string;
          -  state: string;
          -  zip: string;
          -  latitude?: number;
          -  longitude?: number;
          -}
          - 
          -export type PropertyType = 'residential' | 'commercial' | 'land' | 'multi-family';
          - 
          -export interface PropertySnapshot {
          -  id: string;
          -  propertyId: string;
          -  capturedAt: string;
          -  ownerName: string;
          -  address: Address;
          -  deedDate?: string;
          -  taxId?: string;
          -  propertyType: PropertyType;
          -  taxAmount?: number;
          -  lienCount?: number;
          -}
          - 
          -export interface MatchResult {
          -  nameScore: number;
          -  addressScore: number;
          -  overallConfidence: number;
          -  isMatch: boolean;
          -  details: MatchDetails;
          -}
          - 
          -export interface MatchDetails {
          -  nameNormalized: string[];
          -  addressNormalized: string[];
          -  levenshteinDistance: number;
          -  geocodingDistance?: number;
          -  fields: {
          -    firstName: FieldMatch;
          -    lastName: FieldMatch;
          -    middleName: FieldMatch;
          -    streetNumber: FieldMatch;
          -    streetName: FieldMatch;
          -    streetType: FieldMatch;
          -    unit: FieldMatch;
          -    city: FieldMatch;
          -    state: FieldMatch;
          -    zip: FieldMatch;
          -  };
          -}
          - 
          -export interface FieldMatch {
          -  valueA: string;
          -  valueB: string;
          -  normalizedA: string;
          -  normalizedB: string;
          -  score: number;
          -}
          - 
          -export interface ChangeDetectionResult {
          -  propertyId: string;
          -  changeType: ChangeType;
          -  severity: Severity;
          -  confidence: number;
          -  changes: PropertyChange[];
          -  previousSnapshot: PropertySnapshot;
          -  currentSnapshot: PropertySnapshot;
          -  detectedAt: string;
          -}
          - 
          -export type ChangeType = 'tax_change' | 'deed_change' | 'ownership_transfer' | 'lien_filing' | 'metadata_change';
          - 
          -export type Severity = 'minor' | 'moderate' | 'major';
          - 
          -export interface PropertyChange {
          -  field: string;
          -  oldValue: unknown;
          -  newValue: unknown;
          -  changeType: ChangeType;
          -}
          - 
          -export interface MatchingConfig {
          -  nameThreshold: number;
          -  addressThreshold: number;
          -  overallThreshold: number;
          -  geocodingRadiusMeters: number;
          -}
          - 
          -export interface DetectionConfig {
          -  ownershipNameThreshold: number;
          -  deedDateSensitivity: number;
          -  taxAmountChangePercent: number;
          -  severityOverrides?: Record<ChangeType, Severity>;
          -}
          - 
          -export interface NormalizedTokens {
          -  firstName: string;
          -  lastName: string;
          -  middleName: string;
          -  initials: string[];
          -}
          - 
          - -
          -
          - - - - - - - - \ No newline at end of file diff --git a/services/hometitle/coverage/lcov.info b/services/hometitle/coverage/lcov.info deleted file mode 100644 index 689c408..0000000 --- a/services/hometitle/coverage/lcov.info +++ /dev/null @@ -1,415 +0,0 @@ -TN: -SF:src/change-detector.ts -FN:18,classifyFieldChange -FN:49,isSignificantNameChange -FN:62,determineSeverity -FN:89,computeChangeConfidence -FN:119,detectChanges -FN:152,(anonymous_5) -FN:154,(anonymous_6) -FN:156,(anonymous_7) -FN:158,(anonymous_8) -FN:174,detectAddressChanges -FN:195,shouldTriggerAlert -FNF:11 -FNH:11 -FNDA:11,classifyFieldChange -FNDA:5,isSignificantNameChange -FNDA:15,determineSeverity -FNDA:15,computeChangeConfidence -FNDA:11,detectChanges -FNDA:10,(anonymous_5) -FNDA:6,(anonymous_6) -FNDA:4,(anonymous_7) -FNDA:3,(anonymous_8) -FNDA:11,detectAddressChanges -FNDA:5,shouldTriggerAlert -DA:12,1 -DA:21,11 -DA:23,5 -DA:29,5 -DA:31,2 -DA:32,2 -DA:34,2 -DA:35,2 -DA:37,1 -DA:38,1 -DA:40,1 -DA:41,1 -DA:43,0 -DA:46,11 -DA:50,5 -DA:58,5 -DA:59,5 -DA:63,15 -DA:65,15 -DA:73,15 -DA:75,15 -DA:76,14 -DA:77,14 -DA:78,14 -DA:81,10 -DA:82,9 -DA:83,9 -DA:86,5 -DA:90,15 -DA:92,13 -DA:93,13 -DA:94,16 -DA:96,6 -DA:97,6 -DA:99,3 -DA:100,3 -DA:102,3 -DA:103,3 -DA:104,3 -DA:105,3 -DA:106,3 -DA:109,2 -DA:110,2 -DA:112,2 -DA:116,13 -DA:124,11 -DA:125,11 -DA:127,11 -DA:136,11 -DA:137,66 -DA:138,66 -DA:140,66 -DA:141,11 -DA:145,11 -DA:146,11 -DA:148,11 -DA:149,11 -DA:151,11 -DA:152,11 -DA:153,4 -DA:154,7 -DA:155,2 -DA:156,5 -DA:157,1 -DA:158,4 -DA:159,1 -DA:162,11 -DA:175,11 -DA:177,11 -DA:179,11 -DA:180,77 -DA:181,77 -DA:182,77 -DA:183,1 -DA:192,11 -DA:196,5 -DA:197,5 -DA:198,5 -DA:199,5 -LF:79 -LH:78 -BRDA:21,0,0,5 -BRDA:21,0,1,2 -BRDA:21,0,2,2 -BRDA:21,0,3,1 -BRDA:21,0,4,1 -BRDA:21,0,5,0 -BRDA:24,1,0,5 -BRDA:24,1,1,0 -BRDA:24,2,0,5 -BRDA:24,2,1,5 -BRDA:25,3,0,4 -BRDA:25,3,1,1 -BRDA:37,4,0,1 -BRDA:37,4,1,0 -BRDA:63,5,0,15 -BRDA:63,5,1,14 -BRDA:66,6,0,15 -BRDA:66,6,1,15 -BRDA:67,7,0,15 -BRDA:67,7,1,15 -BRDA:68,8,0,15 -BRDA:68,8,1,15 -BRDA:69,9,0,15 -BRDA:69,9,1,14 -BRDA:70,10,0,15 -BRDA:70,10,1,15 -BRDA:78,11,0,5 -BRDA:78,11,1,9 -BRDA:83,12,0,5 -BRDA:83,12,1,4 -BRDA:90,13,0,2 -BRDA:90,13,1,13 -BRDA:94,14,0,6 -BRDA:94,14,1,3 -BRDA:94,14,2,3 -BRDA:94,14,3,2 -BRDA:94,14,4,2 -BRDA:104,15,0,3 -BRDA:104,15,1,0 -BRDA:105,16,0,3 -BRDA:105,16,1,0 -BRDA:140,17,0,11 -BRDA:140,17,1,55 -BRDA:152,18,0,4 -BRDA:152,18,1,7 -BRDA:154,19,0,2 -BRDA:154,19,1,5 -BRDA:156,20,0,1 -BRDA:156,20,1,4 -BRDA:158,21,0,1 -BRDA:158,21,1,3 -BRDA:182,22,0,1 -BRDA:182,22,1,76 -BRDA:195,23,0,5 -BRDA:199,24,0,5 -BRDA:199,24,1,4 -BRF:56 -BRH:51 -end_of_record -TN: -SF:src/index.ts -FNF:0 -FNH:0 -LF:0 -LH:0 -BRF:0 -BRH:0 -end_of_record -TN: -SF:src/matcher.service.ts -FN:53,levenshteinDistance -FN:54,(anonymous_1) -FN:55,(anonymous_2) -FN:72,similarityScore -FN:77,normalizeString -FN:86,parseName -FN:133,normalizeStreetType -FN:138,normalizeAddress -FN:151,computeFieldMatch -FN:168,haversineDistance -FN:182,computeNameScore -FN:189,(anonymous_11) -FN:190,(anonymous_12) -FN:203,computeAddressScore -FN:237,matchRecords -FN:305,getConfigForPropertyType -FNF:16 -FNH:15 -FNDA:56,levenshteinDistance -FNDA:379,(anonymous_1) -FNDA:2622,(anonymous_2) -FNDA:39,similarityScore -FNDA:848,normalizeString -FNDA:37,parseName -FNDA:69,normalizeStreetType -FNDA:30,normalizeAddress -FNDA:300,computeFieldMatch -FNDA:9,haversineDistance -FNDA:15,computeNameScore -FNDA:1,(anonymous_11) -FNDA:0,(anonymous_12) -FNDA:15,computeAddressScore -FNDA:15,matchRecords -FNDA:3,getConfigForPropertyType -DA:11,2 -DA:18,2 -DA:23,2 -DA:28,2 -DA:46,2 -DA:54,56 -DA:55,2622 -DA:58,56 -DA:59,323 -DA:60,1950 -DA:61,1950 -DA:69,56 -DA:73,39 -DA:74,38 -DA:78,848 -DA:87,37 -DA:88,37 -DA:90,37 -DA:91,37 -DA:92,37 -DA:93,37 -DA:95,37 -DA:97,36 -DA:98,36 -DA:99,2 -DA:102,36 -DA:103,36 -DA:104,2 -DA:107,36 -DA:109,36 -DA:110,1 -DA:111,35 -DA:112,31 -DA:113,31 -DA:115,4 -DA:116,4 -DA:117,4 -DA:120,36 -DA:121,0 -DA:123,36 -DA:124,4 -DA:125,4 -DA:126,4 -DA:130,36 -DA:134,69 -DA:135,69 -DA:139,30 -DA:148,30 -DA:152,300 -DA:153,300 -DA:154,300 -DA:156,300 -DA:157,252 -DA:159,250 -DA:161,36 -DA:162,36 -DA:163,36 -DA:165,36 -DA:169,9 -DA:170,9 -DA:171,9 -DA:173,9 -DA:178,9 -DA:179,9 -DA:183,15 -DA:184,15 -DA:185,15 -DA:187,15 -DA:188,15 -DA:189,1 -DA:190,1 -DA:191,1 -DA:192,1 -DA:193,1 -DA:195,1 -DA:196,1 -DA:199,15 -DA:200,15 -DA:204,15 -DA:205,15 -DA:206,15 -DA:210,15 -DA:211,15 -DA:212,15 -DA:213,15 -DA:216,15 -DA:218,15 -DA:219,9 -DA:220,9 -DA:221,9 -DA:225,15 -DA:234,15 -DA:244,15 -DA:246,15 -DA:247,15 -DA:249,15 -DA:251,15 -DA:253,15 -DA:255,15 -DA:256,15 -DA:257,15 -DA:258,15 -DA:259,15 -DA:260,15 -DA:264,15 -DA:265,15 -DA:266,15 -DA:267,15 -DA:269,15 -DA:270,15 -DA:272,15 -DA:277,15 -DA:296,15 -DA:306,3 -LF:114 -LH:113 -BRDA:55,0,0,349 -BRDA:55,0,1,2273 -BRDA:55,1,0,323 -BRDA:55,1,1,1950 -BRDA:60,2,0,212 -BRDA:60,2,1,1738 -BRDA:73,3,0,1 -BRDA:73,3,1,38 -BRDA:95,4,0,1 -BRDA:95,4,1,36 -BRDA:98,5,0,36 -BRDA:98,5,1,38 -BRDA:103,6,0,36 -BRDA:103,6,1,37 -BRDA:109,7,0,1 -BRDA:109,7,1,35 -BRDA:111,8,0,31 -BRDA:111,8,1,4 -BRDA:120,9,0,0 -BRDA:120,9,1,36 -BRDA:123,10,0,4 -BRDA:123,10,1,32 -BRDA:126,11,0,2 -BRDA:126,11,1,2 -BRDA:135,12,0,69 -BRDA:135,12,1,0 -BRDA:142,13,0,20 -BRDA:142,13,1,10 -BRDA:143,14,0,19 -BRDA:143,14,1,11 -BRDA:152,15,0,300 -BRDA:152,15,1,270 -BRDA:156,16,0,48 -BRDA:156,16,1,252 -BRDA:156,17,0,300 -BRDA:156,17,1,48 -BRDA:157,18,0,2 -BRDA:157,18,1,250 -BRDA:157,19,0,252 -BRDA:157,19,1,252 -BRDA:159,20,0,214 -BRDA:159,20,1,36 -BRDA:188,21,0,1 -BRDA:188,21,1,14 -BRDA:188,22,0,15 -BRDA:188,22,1,14 -BRDA:193,23,0,0 -BRDA:193,23,1,1 -BRDA:196,24,0,1 -BRDA:196,24,1,0 -BRDA:207,25,0,10 -BRDA:207,25,1,5 -BRDA:208,26,0,10 -BRDA:208,26,1,5 -BRDA:210,27,0,15 -BRDA:210,27,1,5 -BRDA:210,28,0,15 -BRDA:210,28,1,6 -BRDA:218,29,0,9 -BRDA:218,29,1,6 -BRDA:218,30,0,15 -BRDA:218,30,1,10 -BRDA:218,30,2,10 -BRDA:218,30,3,9 -BRDA:221,31,0,9 -BRDA:221,31,1,0 -BRDA:232,32,0,9 -BRDA:232,32,1,6 -BRDA:261,33,0,10 -BRDA:261,33,1,5 -BRDA:262,34,0,10 -BRDA:262,34,1,5 -BRDA:264,35,0,15 -BRDA:264,35,1,5 -BRDA:264,36,0,15 -BRDA:264,36,1,6 -BRF:76 -BRH:71 -end_of_record -TN: -SF:src/types.ts -FNF:0 -FNH:0 -LF:0 -LH:0 -BRF:0 -BRH:0 -end_of_record diff --git a/services/hometitle/coverage/matcher.service.ts.html b/services/hometitle/coverage/matcher.service.ts.html deleted file mode 100644 index 90ab31c..0000000 --- a/services/hometitle/coverage/matcher.service.ts.html +++ /dev/null @@ -1,1012 +0,0 @@ - - - - - - Code coverage report for matcher.service.ts - - - - - - - - - -
          -
          -

          All files matcher.service.ts

          -
          - -
          - 97.61% - Statements - 123/126 -
          - - -
          - 93.42% - Branches - 71/76 -
          - - -
          - 93.75% - Functions - 15/16 -
          - - -
          - 99.12% - Lines - 113/114 -
          - - -
          -

          - Press n or j to go to the next uncovered block, b, p or k for the previous block. -

          - -
          -
          -
          
          -
          1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115 -116 -117 -118 -119 -120 -121 -122 -123 -124 -125 -126 -127 -128 -129 -130 -131 -132 -133 -134 -135 -136 -137 -138 -139 -140 -141 -142 -143 -144 -145 -146 -147 -148 -149 -150 -151 -152 -153 -154 -155 -156 -157 -158 -159 -160 -161 -162 -163 -164 -165 -166 -167 -168 -169 -170 -171 -172 -173 -174 -175 -176 -177 -178 -179 -180 -181 -182 -183 -184 -185 -186 -187 -188 -189 -190 -191 -192 -193 -194 -195 -196 -197 -198 -199 -200 -201 -202 -203 -204 -205 -206 -207 -208 -209 -210 -211 -212 -213 -214 -215 -216 -217 -218 -219 -220 -221 -222 -223 -224 -225 -226 -227 -228 -229 -230 -231 -232 -233 -234 -235 -236 -237 -238 -239 -240 -241 -242 -243 -244 -245 -246 -247 -248 -249 -250 -251 -252 -253 -254 -255 -256 -257 -258 -259 -260 -261 -262 -263 -264 -265 -266 -267 -268 -269 -270 -271 -272 -273 -274 -275 -276 -277 -278 -279 -280 -281 -282 -283 -284 -285 -286 -287 -288 -289 -290 -291 -292 -293 -294 -295 -296 -297 -298 -299 -300 -301 -302 -303 -304 -305 -306 -307 -308 -309 -310  -  -  -  -  -  -  -  -  -  -2x -  -  -  -  -  -  -2x -  -  -  -  -2x -  -  -  -  -2x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -2x -  -  -  -  -  -  -  -56x -2622x -  -  -56x -323x -1950x -1950x -  -  -  -  -  -  -  -56x -  -  -  -39x -38x -  -  -  -848x -  -  -  -  -  -  -  -  -37x -37x -  -37x -37x -37x -37x -  -37x -  -36x -36x -2x -  -  -36x -36x -2x -  -  -36x -  -36x -1x -35x -31x -31x -  -4x -4x -4x -  -  -36x -  -  -36x -4x -4x -4x -  -  -  -36x -  -  -  -69x -69x -  -  -  -30x -  -  -  -  -  -  -  -  -30x -  -  -  -300x -300x -300x -  -300x -252x -  -250x -  -36x -36x -36x -  -36x -  -  -  -9x -9x -9x -  -9x -  -  -  -  -9x -9x -  -  -  -15x -15x -15x -  -15x -15x -1x -1x -1x -1x -1x -  -1x -1x -  -  -15x -15x -  -  -  -15x -15x -15x -  -  -  -15x -15x -15x -15x -  -  -15x -  -15x -9x -9x -9x -  -  -  -15x -  -  -  -  -  -  -  -  -15x -  -  -  -  -  -  -  -  -  -15x -  -15x -15x -  -15x -  -15x -  -15x -  -15x -15x -15x -15x -15x -15x -  -  -  -15x -15x -15x -15x -  -15x -15x -  -15x -  -  -  -  -15x -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -15x -  -  -  -  -  -  -  -  -  -3x -  -  -  - 
          import {
          -  Address,
          -  MatchResult,
          -  MatchDetails,
          -  FieldMatch,
          -  MatchingConfig,
          -  NormalizedTokens,
          -  PropertyType,
          -} from './types';
          - 
          -const DEFAULT_CONFIG: MatchingConfig = {
          -  nameThreshold: 0.85,
          -  addressThreshold: 0.9,
          -  overallThreshold: 0.85,
          -  geocodingRadiusMeters: 100,
          -};
          - 
          -const COMMON_PREFIXES = new Set([
          -  'mr', 'mrs', 'ms', 'miss', 'dr', 'prof', 'jr', 'sr', 'junior', 'senior',
          -  'ii', 'iii', 'iv', 'rev', 'st', 'hon', 'esq',
          -]);
          - 
          -const COMMON_SUFFIXES = new Set([
          -  'jr', 'sr', 'junior', 'senior', 'ii', 'iii', 'iv', 'v', 'esq',
          -  'phd', 'md', 'llm', 'cpa',
          -]);
          - 
          -const STREET_TYPE_MAP: Record<string, string> = {
          -  'st': 'street', 'street': 'street',
          -  'ave': 'avenue', 'avenue': 'avenue',
          -  'blvd': 'boulevard', 'boulevard': 'boulevard',
          -  'dr': 'drive', 'drive': 'drive',
          -  'ln': 'lane', 'lane': 'lane',
          -  'ct': 'court', 'court': 'court',
          -  'pl': 'place', 'place': 'place',
          -  'rd': 'road', 'road': 'road',
          -  'way': 'way',
          -  'trl': 'trail', 'trail': 'trail',
          -  'hwy': 'highway', 'highway': 'highway',
          -  'pkwy': 'parkway', 'parkway': 'parkway',
          -  'cir': 'circle', 'circle': 'circle',
          -  'sq': 'square', 'square': 'square',
          -  'ter': 'terrace', 'terrace': 'terrace',
          -};
          - 
          -const PROPERTY_TYPE_CONFIGS: Record<PropertyType, Partial<MatchingConfig>> = {
          -  'residential': { nameThreshold: 0.85, addressThreshold: 0.9 },
          -  'commercial': { nameThreshold: 0.8, addressThreshold: 0.9 },
          -  'land': { nameThreshold: 0.8, addressThreshold: 0.85 },
          -  'multi-family': { nameThreshold: 0.8, addressThreshold: 0.9 },
          -};
          - 
          -function levenshteinDistance(a: string, b: string): number {
          -  const matrix: number[][] = Array.from({ length: b.length + 1 }, (_, i) =>
          -    Array.from({ length: a.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0))
          -  );
          - 
          -  for (let i = 1; i <= b.length; i++) {
          -    for (let j = 1; j <= a.length; j++) {
          -      const cost = a[j - 1] === b[i - 1] ? 0 : 1;
          -      matrix[i][j] = Math.min(
          -        matrix[i - 1][j] + 1,
          -        matrix[i][j - 1] + 1,
          -        matrix[i - 1][j - 1] + cost,
          -      );
          -    }
          -  }
          - 
          -  return matrix[b.length][a.length];
          -}
          - 
          -function similarityScore(distance: number, maxLen: number): number {
          -  if (maxLen === 0) return 1.0;
          -  return 1.0 - distance / maxLen;
          -}
          - 
          -function normalizeString(str: string): string {
          -  return str
          -    .toLowerCase()
          -    .replace(/[''']/g, '')
          -    .replace(/[^a-z0-9\s]/g, ' ')
          -    .replace(/\s+/g, ' ')
          -    .trim();
          -}
          - 
          -function parseName(name: string): NormalizedTokens {
          -  const clean = normalizeString(name);
          -  const parts = clean.split(' ').filter(Boolean);
          - 
          -  let firstName = '';
          -  let lastName = '';
          -  let middleName = '';
          -  const initials: string[] = [];
          - 
          -  if (parts.length === 0) return { firstName, lastName, middleName, initials };
          - 
          -  let startIdx = 0;
          -  while (startIdx < parts.length && COMMON_PREFIXES.has(parts[startIdx])) {
          -    startIdx++;
          -  }
          - 
          -  let endIdx = parts.length;
          -  while (endIdx > startIdx + 1 && COMMON_SUFFIXES.has(parts[endIdx - 1])) {
          -    endIdx--;
          -  }
          - 
          -  const coreParts = parts.slice(startIdx, endIdx);
          - 
          -  if (coreParts.length === 1) {
          -    lastName = coreParts[0];
          -  } else if (coreParts.length === 2) {
          -    firstName = coreParts[0];
          -    lastName = coreParts[1];
          -  } else {
          -    firstName = coreParts[0];
          -    lastName = coreParts[coreParts.length - 1];
          -    middleName = coreParts.slice(1, -1).join(' ');
          -  }
          - 
          -  Iif (firstName.length === 1) {
          -    initials.push(firstName);
          -  }
          -  if (middleName) {
          -    const middleParts = middleName.split(' ');
          -    for (const mp of middleParts) {
          -      if (mp.length === 1) initials.push(mp);
          -    }
          -  }
          - 
          -  return { firstName, lastName, middleName, initials };
          -}
          - 
          -function normalizeStreetType(type: string): string {
          -  const clean = normalizeString(type);
          -  return STREET_TYPE_MAP[clean] || clean;
          -}
          - 
          -function normalizeAddress(addr: Address): string {
          -  const parts = [
          -    addr.streetNumber,
          -    normalizeString(addr.streetName),
          -    addr.streetType ? normalizeStreetType(addr.streetType) : '',
          -    addr.unit ? normalizeString(addr.unit) : '',
          -    normalizeString(addr.city),
          -    addr.state.toLowerCase(),
          -    addr.zip,
          -  ].filter(Boolean);
          -  return parts.join(' ');
          -}
          - 
          -function computeFieldMatch(valueA: string, valueB: string, normalizeFn?: (v: string) => string): FieldMatch {
          -  const normFn = normalizeFn || normalizeString;
          -  const normalizedA = normFn(valueA);
          -  const normalizedB = normFn(valueB);
          - 
          -  if (!normalizedA && !normalizedB) return { valueA, valueB, normalizedA, normalizedB, score: 1.0 };
          -  if (!normalizedA || !normalizedB) return { valueA, valueB, normalizedA, normalizedB, score: 0.0 };
          - 
          -  if (normalizedA === normalizedB) return { valueA, valueB, normalizedA, normalizedB, score: 1.0 };
          - 
          -  const dist = levenshteinDistance(normalizedA, normalizedB);
          -  const maxLen = Math.max(normalizedA.length, normalizedB.length);
          -  const score = similarityScore(dist, maxLen);
          - 
          -  return { valueA, valueB, normalizedA, normalizedB, score: Math.round(score * 1000) / 1000 };
          -}
          - 
          -function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
          -  const R = 6371000;
          -  const dLat = ((lat2 - lat1) * Math.PI) / 180;
          -  const dLon = ((lon2 - lon1) * Math.PI) / 180;
          -  const a =
          -    Math.sin(dLat / 2) * Math.sin(dLat / 2) +
          -    Math.cos((lat1 * Math.PI) / 180) *
          -      Math.cos((lat2 * Math.PI) / 180) *
          -      Math.sin(dLon / 2) *
          -      Math.sin(dLon / 2);
          -  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
          -  return R * c;
          -}
          - 
          -function computeNameScore(tokensA: NormalizedTokens, tokensB: NormalizedTokens): number {
          -  const firstScore = computeFieldMatch(tokensA.firstName, tokensB.firstName).score;
          -  const lastScore = computeFieldMatch(tokensA.lastName, tokensB.lastName).score;
          -  const middleScore = computeFieldMatch(tokensA.middleName, tokensB.middleName).score;
          - 
          -  let initialMatchScore = 1.0;
          -  if (tokensA.initials.length > 0 || tokensB.initials.length > 0) {
          -    const allInitialsA = new Set(tokensA.initials.map(i => i.toLowerCase()));
          -    const allInitialsB = new Set(tokensB.initials.map(i => i.toLowerCase()));
          -    let matched = 0;
          -    for (const init of allInitialsA) {
          -      Iif (allInitialsB.has(init)) matched++;
          -    }
          -    const total = Math.max(allInitialsA.size, allInitialsB.size);
          -    initialMatchScore = total > 0 ? matched / total : 1.0;
          -  }
          - 
          -  const weighted = (lastScore * 0.45) + (firstScore * 0.35) + (middleScore * 0.1) + (initialMatchScore * 0.1);
          -  return Math.round(weighted * 1000) / 1000;
          -}
          - 
          -function computeAddressScore(addrA: Address, addrB: Address, config: MatchingConfig): { score: number; geocodingDistance?: number } {
          -  const numberMatch = computeFieldMatch(addrA.streetNumber, addrB.streetNumber).score;
          -  const streetMatch = computeFieldMatch(addrA.streetName, addrB.streetName, normalizeString).score;
          -  const typeMatch = computeFieldMatch(
          -    addrA.streetType ? normalizeStreetType(addrA.streetType) : '',
          -    addrB.streetType ? normalizeStreetType(addrB.streetType) : '',
          -  ).score;
          -  const unitMatch = computeFieldMatch(addrA.unit || '', addrB.unit || '').score;
          -  const cityMatch = computeFieldMatch(addrA.city, addrB.city).score;
          -  const stateMatch = computeFieldMatch(addrA.state, addrB.state).score;
          -  const zipMatch = computeFieldMatch(addrA.zip, addrB.zip).score;
          - 
          -  let geocodingDistance: number | undefined;
          -  let geoScore = 0.0;
          - 
          -  if (addrA.latitude && addrA.longitude && addrB.latitude && addrB.longitude) {
          -    geocodingDistance = haversineDistance(addrA.latitude, addrA.longitude, addrB.latitude, addrB.longitude);
          -    const maxDist = config.geocodingRadiusMeters;
          -    geoScore = geocodingDistance <= maxDist ? 1.0 : Math.max(0, 1.0 - (geocodingDistance - maxDist) / (maxDist * 5));
          -  }
          - 
          -  const weighted =
          -    (numberMatch * 0.2) +
          -    (streetMatch * 0.25) +
          -    (typeMatch * 0.1) +
          -    (unitMatch * 0.1) +
          -    (cityMatch * 0.1) +
          -    (stateMatch * 0.1) +
          -    (zipMatch * 0.1) +
          -    (geoScore * (geocodingDistance !== undefined ? 0.05 : 0));
          - 
          -  return { score: Math.round(weighted * 1000) / 1000, geocodingDistance };
          -}
          - 
          -export function matchRecords(
          -  nameA: string,
          -  addressA: Address,
          -  nameB: string,
          -  addressB: Address,
          -  config?: Partial<MatchingConfig>,
          -): MatchResult {
          -  const effectiveConfig = { ...DEFAULT_CONFIG, ...config };
          - 
          -  const tokensA = parseName(nameA);
          -  const tokensB = parseName(nameB);
          - 
          -  const nameScore = computeNameScore(tokensA, tokensB);
          - 
          -  const { score: addressScore, geocodingDistance } = computeAddressScore(addressA, addressB, effectiveConfig);
          - 
          -  const overallConfidence = Math.round((nameScore * 0.5 + addressScore * 0.5) * 1000) / 1000;
          - 
          -  const firstMatch = computeFieldMatch(tokensA.firstName, tokensB.firstName);
          -  const lastMatch = computeFieldMatch(tokensA.lastName, tokensB.lastName);
          -  const middleMatch = computeFieldMatch(tokensA.middleName, tokensB.middleName);
          -  const numberMatch = computeFieldMatch(addressA.streetNumber, addressB.streetNumber);
          -  const streetMatch = computeFieldMatch(addressA.streetName, addressB.streetName, normalizeString);
          -  const typeMatch = computeFieldMatch(
          -    addressA.streetType ? normalizeStreetType(addressA.streetType) : '',
          -    addressB.streetType ? normalizeStreetType(addressB.streetType) : '',
          -  );
          -  const unitMatch = computeFieldMatch(addressA.unit || '', addressB.unit || '');
          -  const cityMatch = computeFieldMatch(addressA.city, addressB.city);
          -  const stateMatch = computeFieldMatch(addressA.state, addressB.state);
          -  const zipMatch = computeFieldMatch(addressA.zip, addressB.zip);
          - 
          -  const normalizedA = normalizeAddress(addressA);
          -  const normalizedB = normalizeAddress(addressB);
          - 
          -  const dist = levenshteinDistance(
          -    normalizeString(nameA),
          -    normalizeString(nameB),
          -  );
          - 
          -  const details: MatchDetails = {
          -    nameNormalized: [normalizeString(nameA), normalizeString(nameB)],
          -    addressNormalized: [normalizedA, normalizedB],
          -    levenshteinDistance: dist,
          -    geocodingDistance,
          -    fields: {
          -      firstName: firstMatch,
          -      lastName: lastMatch,
          -      middleName: middleMatch,
          -      streetNumber: numberMatch,
          -      streetName: streetMatch,
          -      streetType: typeMatch,
          -      unit: unitMatch,
          -      city: cityMatch,
          -      state: stateMatch,
          -      zip: zipMatch,
          -    },
          -  };
          - 
          -  return {
          -    nameScore,
          -    addressScore,
          -    overallConfidence,
          -    isMatch: overallConfidence >= effectiveConfig.overallThreshold,
          -    details,
          -  };
          -}
          - 
          -export function getConfigForPropertyType(type: PropertyType): MatchingConfig {
          -  return { ...DEFAULT_CONFIG, ...PROPERTY_TYPE_CONFIGS[type] };
          -}
          - 
          -export { parseName, normalizeString, normalizeStreetType, levenshteinDistance, similarityScore };
          - 
          - -
          -
          - - - - - - - - \ No newline at end of file diff --git a/services/hometitle/coverage/prettify.css b/services/hometitle/coverage/prettify.css deleted file mode 100644 index b317a7c..0000000 --- a/services/hometitle/coverage/prettify.css +++ /dev/null @@ -1 +0,0 @@ -.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/services/hometitle/coverage/prettify.js b/services/hometitle/coverage/prettify.js deleted file mode 100644 index b322523..0000000 --- a/services/hometitle/coverage/prettify.js +++ /dev/null @@ -1,2 +0,0 @@ -/* eslint-disable */ -window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/services/hometitle/coverage/sort-arrow-sprite.png b/services/hometitle/coverage/sort-arrow-sprite.png deleted file mode 100644 index 6ed6831..0000000 Binary files a/services/hometitle/coverage/sort-arrow-sprite.png and /dev/null differ diff --git a/services/hometitle/coverage/sorter.js b/services/hometitle/coverage/sorter.js deleted file mode 100644 index 4ed70ae..0000000 --- a/services/hometitle/coverage/sorter.js +++ /dev/null @@ -1,210 +0,0 @@ -/* eslint-disable */ -var addSorting = (function() { - 'use strict'; - var cols, - currentSort = { - index: 0, - desc: false - }; - - // returns the summary table element - function getTable() { - return document.querySelector('.coverage-summary'); - } - // returns the thead element of the summary table - function getTableHeader() { - return getTable().querySelector('thead tr'); - } - // returns the tbody element of the summary table - function getTableBody() { - return getTable().querySelector('tbody'); - } - // returns the th element for nth column - function getNthColumn(n) { - return getTableHeader().querySelectorAll('th')[n]; - } - - function onFilterInput() { - const searchValue = document.getElementById('fileSearch').value; - const rows = document.getElementsByTagName('tbody')[0].children; - - // Try to create a RegExp from the searchValue. If it fails (invalid regex), - // it will be treated as a plain text search - let searchRegex; - try { - searchRegex = new RegExp(searchValue, 'i'); // 'i' for case-insensitive - } catch (error) { - searchRegex = null; - } - - for (let i = 0; i < rows.length; i++) { - const row = rows[i]; - let isMatch = false; - - if (searchRegex) { - // If a valid regex was created, use it for matching - isMatch = searchRegex.test(row.textContent); - } else { - // Otherwise, fall back to the original plain text search - isMatch = row.textContent - .toLowerCase() - .includes(searchValue.toLowerCase()); - } - - row.style.display = isMatch ? '' : 'none'; - } - } - - // loads the search box - function addSearchBox() { - var template = document.getElementById('filterTemplate'); - var templateClone = template.content.cloneNode(true); - templateClone.getElementById('fileSearch').oninput = onFilterInput; - template.parentElement.appendChild(templateClone); - } - - // loads all columns - function loadColumns() { - var colNodes = getTableHeader().querySelectorAll('th'), - colNode, - cols = [], - col, - i; - - for (i = 0; i < colNodes.length; i += 1) { - colNode = colNodes[i]; - col = { - key: colNode.getAttribute('data-col'), - sortable: !colNode.getAttribute('data-nosort'), - type: colNode.getAttribute('data-type') || 'string' - }; - cols.push(col); - if (col.sortable) { - col.defaultDescSort = col.type === 'number'; - colNode.innerHTML = - colNode.innerHTML + ''; - } - } - return cols; - } - // attaches a data attribute to every tr element with an object - // of data values keyed by column name - function loadRowData(tableRow) { - var tableCols = tableRow.querySelectorAll('td'), - colNode, - col, - data = {}, - i, - val; - for (i = 0; i < tableCols.length; i += 1) { - colNode = tableCols[i]; - col = cols[i]; - val = colNode.getAttribute('data-value'); - if (col.type === 'number') { - val = Number(val); - } - data[col.key] = val; - } - return data; - } - // loads all row data - function loadData() { - var rows = getTableBody().querySelectorAll('tr'), - i; - - for (i = 0; i < rows.length; i += 1) { - rows[i].data = loadRowData(rows[i]); - } - } - // sorts the table using the data for the ith column - function sortByIndex(index, desc) { - var key = cols[index].key, - sorter = function(a, b) { - a = a.data[key]; - b = b.data[key]; - return a < b ? -1 : a > b ? 1 : 0; - }, - finalSorter = sorter, - tableBody = document.querySelector('.coverage-summary tbody'), - rowNodes = tableBody.querySelectorAll('tr'), - rows = [], - i; - - if (desc) { - finalSorter = function(a, b) { - return -1 * sorter(a, b); - }; - } - - for (i = 0; i < rowNodes.length; i += 1) { - rows.push(rowNodes[i]); - tableBody.removeChild(rowNodes[i]); - } - - rows.sort(finalSorter); - - for (i = 0; i < rows.length; i += 1) { - tableBody.appendChild(rows[i]); - } - } - // removes sort indicators for current column being sorted - function removeSortIndicators() { - var col = getNthColumn(currentSort.index), - cls = col.className; - - cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, ''); - col.className = cls; - } - // adds sort indicators for current column being sorted - function addSortIndicators() { - getNthColumn(currentSort.index).className += currentSort.desc - ? ' sorted-desc' - : ' sorted'; - } - // adds event listeners for all sorter widgets - function enableUI() { - var i, - el, - ithSorter = function ithSorter(i) { - var col = cols[i]; - - return function() { - var desc = col.defaultDescSort; - - if (currentSort.index === i) { - desc = !currentSort.desc; - } - sortByIndex(i, desc); - removeSortIndicators(); - currentSort.index = i; - currentSort.desc = desc; - addSortIndicators(); - }; - }; - for (i = 0; i < cols.length; i += 1) { - if (cols[i].sortable) { - // add the click event handler on the th so users - // dont have to click on those tiny arrows - el = getNthColumn(i).querySelector('.sorter').parentElement; - if (el.addEventListener) { - el.addEventListener('click', ithSorter(i)); - } else { - el.attachEvent('onclick', ithSorter(i)); - } - } - } - } - // adds sorting functionality to the UI - return function() { - if (!getTable()) { - return; - } - cols = loadColumns(); - loadData(); - addSearchBox(); - addSortIndicators(); - enableUI(); - }; -})(); - -window.addEventListener('load', addSorting); diff --git a/services/hometitle/coverage/types.ts.html b/services/hometitle/coverage/types.ts.html deleted file mode 100644 index 4fce112..0000000 --- a/services/hometitle/coverage/types.ts.html +++ /dev/null @@ -1,427 +0,0 @@ - - - - - - Code coverage report for types.ts - - - - - - - - - -
          -
          -

          All files types.ts

          -
          - -
          - 0% - Statements - 0/0 -
          - - -
          - 0% - Branches - 0/0 -
          - - -
          - 0% - Functions - 0/0 -
          - - -
          - 0% - Lines - 0/0 -
          - - -
          -

          - Press n or j to go to the next uncovered block, b, p or k for the previous block. -

          - -
          -
          -
          
          -
          1 -2 -3 -4 -5 -6 -7 -8 -9 -10 -11 -12 -13 -14 -15 -16 -17 -18 -19 -20 -21 -22 -23 -24 -25 -26 -27 -28 -29 -30 -31 -32 -33 -34 -35 -36 -37 -38 -39 -40 -41 -42 -43 -44 -45 -46 -47 -48 -49 -50 -51 -52 -53 -54 -55 -56 -57 -58 -59 -60 -61 -62 -63 -64 -65 -66 -67 -68 -69 -70 -71 -72 -73 -74 -75 -76 -77 -78 -79 -80 -81 -82 -83 -84 -85 -86 -87 -88 -89 -90 -91 -92 -93 -94 -95 -96 -97 -98 -99 -100 -101 -102 -103 -104 -105 -106 -107 -108 -109 -110 -111 -112 -113 -114 -115  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  -  - 
          export interface PropertyRecord {
          -  id: string;
          -  ownerName: string;
          -  address: Address;
          -  deedDate?: string;
          -  taxId?: string;
          -  propertyType: PropertyType;
          -  metadata?: Record<string, unknown>;
          -}
          - 
          -export interface Address {
          -  streetNumber: string;
          -  streetName: string;
          -  streetType?: string;
          -  unit?: string;
          -  city: string;
          -  state: string;
          -  zip: string;
          -  latitude?: number;
          -  longitude?: number;
          -}
          - 
          -export type PropertyType = 'residential' | 'commercial' | 'land' | 'multi-family';
          - 
          -export interface PropertySnapshot {
          -  id: string;
          -  propertyId: string;
          -  capturedAt: string;
          -  ownerName: string;
          -  address: Address;
          -  deedDate?: string;
          -  taxId?: string;
          -  propertyType: PropertyType;
          -  taxAmount?: number;
          -  lienCount?: number;
          -}
          - 
          -export interface MatchResult {
          -  nameScore: number;
          -  addressScore: number;
          -  overallConfidence: number;
          -  isMatch: boolean;
          -  details: MatchDetails;
          -}
          - 
          -export interface MatchDetails {
          -  nameNormalized: string[];
          -  addressNormalized: string[];
          -  levenshteinDistance: number;
          -  geocodingDistance?: number;
          -  fields: {
          -    firstName: FieldMatch;
          -    lastName: FieldMatch;
          -    middleName: FieldMatch;
          -    streetNumber: FieldMatch;
          -    streetName: FieldMatch;
          -    streetType: FieldMatch;
          -    unit: FieldMatch;
          -    city: FieldMatch;
          -    state: FieldMatch;
          -    zip: FieldMatch;
          -  };
          -}
          - 
          -export interface FieldMatch {
          -  valueA: string;
          -  valueB: string;
          -  normalizedA: string;
          -  normalizedB: string;
          -  score: number;
          -}
          - 
          -export interface ChangeDetectionResult {
          -  propertyId: string;
          -  changeType: ChangeType;
          -  severity: Severity;
          -  confidence: number;
          -  changes: PropertyChange[];
          -  previousSnapshot: PropertySnapshot;
          -  currentSnapshot: PropertySnapshot;
          -  detectedAt: string;
          -}
          - 
          -export type ChangeType = 'tax_change' | 'deed_change' | 'ownership_transfer' | 'lien_filing' | 'metadata_change';
          - 
          -export type Severity = 'minor' | 'moderate' | 'major';
          - 
          -export interface PropertyChange {
          -  field: string;
          -  oldValue: unknown;
          -  newValue: unknown;
          -  changeType: ChangeType;
          -}
          - 
          -export interface MatchingConfig {
          -  nameThreshold: number;
          -  addressThreshold: number;
          -  overallThreshold: number;
          -  geocodingRadiusMeters: number;
          -}
          - 
          -export interface DetectionConfig {
          -  ownershipNameThreshold: number;
          -  deedDateSensitivity: number;
          -  taxAmountChangePercent: number;
          -  severityOverrides?: Record<ChangeType, Severity>;
          -}
          - 
          -export interface NormalizedTokens {
          -  firstName: string;
          -  lastName: string;
          -  middleName: string;
          -  initials: string[];
          -}
          - 
          - -
          -
          - - - - - - - - \ No newline at end of file diff --git a/services/hometitle/package.json b/services/hometitle/package.json deleted file mode 100644 index 75bb3ca..0000000 --- a/services/hometitle/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@shieldai/hometitle", - "version": "0.1.0", - "main": "./dist/index.js", - "types": "./dist/index.js", - "scripts": { - "build": "tsc", - "test": "vitest run", - "test:coverage": "vitest run --coverage", - "lint": "eslint src/" - }, - "dependencies": { - "@shieldai/db": "workspace:*", - "@shieldai/types": "workspace:*", - "@shieldai/shared-notifications": "workspace:*", - "@shieldai/correlation": "workspace:*", - "uuid": "^11.1.0", - "rate-limiter-flexible": "^5.0.5", - "cache-manager": "^6.4.2", - "cacheable": "^1.10.0" - }, - "devDependencies": { - "vitest": "^4.1.5", - "@vitest/coverage-v8": "^4.1.5", - "@types/uuid": "^10.0.0" - }, - "exports": { - ".": "./src/index.ts" - } -} diff --git a/services/hometitle/src/alert.pipeline.ts b/services/hometitle/src/alert.pipeline.ts deleted file mode 100644 index 0bed31c..0000000 --- a/services/hometitle/src/alert.pipeline.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { prisma, AlertSeverity, AlertChannel } from '@shieldai/db'; -import { - NotificationService, - loadNotificationConfig, -} from '@shieldai/shared-notifications'; -import { - ChangeDetectionResult, - ChangeType, - Severity, - PropertyAlert, - AlertSeverityLevel, - NotificationChannel, - AlertPipelineConfig, -} from './types'; - -const DEFAULT_CONFIG: AlertPipelineConfig = { - dedupWindowMs: 24 * 60 * 60 * 1000, - minSeverity: 'warning', - premiumTierChannels: ['email', 'push', 'sms'], - defaultChannels: ['email'], -}; - -const SEVERITY_MAP: Record = { - critical: 'critical', - warning: 'warning', - info: 'info', -}; - -const CHANGE_TYPE_LABELS: Record = { - ownership_transfer: 'Ownership Transfer', - deed_change: 'Deed Change', - lien_filing: 'Lien Filing', - tax_change: 'Tax Assessment Change', - metadata_change: 'Property Metadata Change', -}; - -export class HomeTitleAlertPipeline { - private notificationService: NotificationService; - private config: AlertPipelineConfig; - private pendingDedup = new Map(); - - constructor(config?: Partial) { - this.config = { ...DEFAULT_CONFIG, ...config }; - this.notificationService = NotificationService.getInstance(); - } - - async processChangeDetection( - result: ChangeDetectionResult, - subscriptionId: string, - userId: string, - ): Promise { - const severity = this.mapSeverity(result.severity); - const shouldAlert = this.shouldAlert(result, severity); - - if (!shouldAlert) { - return null; - } - - const dedupKey = this.buildDedupKey(userId, result.propertyId, result.changeType); - - const isDuplicate = await this.checkDedup(dedupKey); - if (isDuplicate) { - return null; - } - - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { tier: true }, - }); - - if (!subscription) { - return null; - } - - const channels = this.getChannelsForTier(subscription.tier); - const title = this.buildTitle(result); - const message = this.buildMessage(result); - - const alert = await prisma.alert.create({ - data: { - subscriptionId, - userId, - type: 'system_warning', - title, - message, - severity: severity as AlertSeverity, - channel: channels as AlertChannel[], - }, - }); - - await this.recordDedup(dedupKey); - - const propertyAlert: PropertyAlert = { - id: alert.id, - propertyId: result.propertyId, - subscriptionId, - userId, - changeType: result.changeType, - severity, - title, - message, - changeDetectionResult: result, - channel: channels, - dedupKey, - createdAt: alert.createdAt.toISOString(), - }; - - await this.createNormalizedAlert(result, userId, subscriptionId, alert.id, severity); - - if (subscription.tier === 'premium') { - await this.dispatchNotification(propertyAlert, userId); - } - - return propertyAlert; - } - - async processBatch( - results: ChangeDetectionResult[], - subscriptionId: string, - userId: string, - ): Promise { - const alerts: PropertyAlert[] = []; - - for (const result of results) { - const alert = await this.processChangeDetection(result, subscriptionId, userId); - if (alert) { - alerts.push(alert); - } - } - - if (alerts.length > 1) { - await this.createCorrelationGroup(alerts, userId); - } - - return alerts; - } - - private shouldAlert(result: ChangeDetectionResult, severity: AlertSeverityLevel): boolean { - const severityOrder: Severity[] = ['info', 'warning', 'critical']; - const minSeverityOrder: Severity[] = ['info', 'warning', 'critical']; - const resultIdx = severityOrder.indexOf(result.severity); - const minIdx = minSeverityOrder.indexOf(this.config.minSeverity); - - return resultIdx >= minIdx && result.confidence >= 0.7; - } - - private mapSeverity(severity: Severity): AlertSeverityLevel { - return SEVERITY_MAP[severity] || 'info'; - } - - private buildDedupKey(userId: string, propertyId: string, changeType: ChangeType): string { - return `hometitle:${userId}:${propertyId}:${changeType}`; - } - - private async checkDedup(dedupKey: string): Promise { - const parts = dedupKey.split(':'); - const userId = parts[1] ?? ''; - const propertyId = parts[2] ?? ''; - - const recentAlert = await prisma.alert.findFirst({ - where: { - userId: userId, - title: { - contains: propertyId, - }, - createdAt: { - gte: new Date(Date.now() - this.config.dedupWindowMs), - }, - }, - orderBy: { createdAt: 'desc' }, - }); - - if (recentAlert) { - return true; - } - - const inMemoryExpiry = this.pendingDedup.get(dedupKey); - if (inMemoryExpiry && Date.now() < inMemoryExpiry) { - return true; - } - - return false; - } - - private async recordDedup(dedupKey: string): Promise { - this.pendingDedup.set( - dedupKey, - Date.now() + this.config.dedupWindowMs, - ); - } - - private getChannelsForTier(tier: string): NotificationChannel[] { - if (tier === 'premium') { - return [...this.config.premiumTierChannels]; - } - return [...this.config.defaultChannels]; - } - - private buildTitle(result: ChangeDetectionResult): string { - const label = CHANGE_TYPE_LABELS[result.changeType] || 'Property Change'; - const severityUpper = result.severity.toUpperCase(); - return `[${severityUpper}] ${label} detected`; - } - - private buildMessage(result: ChangeDetectionResult): string { - const changes = result.changes - .map(c => `- ${c.field}: ${String(c.oldValue)} → ${String(c.newValue)}`) - .join('\n'); - - return `Change detected on property ${result.propertyId}.\n\nChanges:\n${changes}\n\nConfidence: ${(result.confidence * 100).toFixed(1)}%`; - } - - private async createNormalizedAlert( - result: ChangeDetectionResult, - userId: string, - subscriptionId: string, - sourceAlertId: string, - severity: AlertSeverityLevel, - ): Promise { - const normalizedSeverity = this.mapToNormalizedSeverity(severity); - - await prisma.normalizedAlert.create({ - data: { - source: 'DARKWATCH' as any, - category: this.mapToAlertCategory(result.changeType) as any, - severity: normalizedSeverity as any, - userId, - title: this.buildTitle(result), - description: this.buildMessage(result), - entities: JSON.stringify({ - propertyId: result.propertyId, - changeType: result.changeType, - subscriptionId, - }), - sourceAlertId, - payload: JSON.stringify({ - confidence: result.confidence, - changes: result.changes, - detectedAt: result.detectedAt, - }), - createdAt: new Date(result.detectedAt), - }, - }); - } - - private async createCorrelationGroup( - alerts: PropertyAlert[], - userId: string, - ): Promise { - const entities = JSON.stringify({ - propertyIds: [...new Set(alerts.map(a => a.propertyId))], - changeTypes: [...new Set(alerts.map(a => a.changeType))], - }); - - const highestSeverity = alerts.reduce((max, alert) => { - const order: AlertSeverityLevel[] = ['info', 'warning', 'critical']; - return order.indexOf(alert.severity) > order.indexOf(max) ? alert.severity : max; - }, 'info' as AlertSeverityLevel); - - const group = await prisma.correlationGroup.create({ - data: { - userId, - entities, - highestSeverity: this.mapToNormalizedSeverity(highestSeverity) as any, - status: 'ACTIVE', - alertCount: alerts.length, - summary: `${alerts.length} property change alert${alerts.length > 1 ? 's' : ''} correlated`, - }, - }); - - await prisma.normalizedAlert.updateMany({ - where: { - sourceAlertId: { in: alerts.map(a => a.id) }, - }, - data: { - groupId: group.id, - }, - }); - } - - private async dispatchNotification( - alert: PropertyAlert, - userId: string, - ): Promise { - try { - const user = await prisma.user.findUnique({ - where: { id: userId }, - select: { email: true, name: true, phone: true }, - }); - - if (!user?.email) { - return; - } - - const htmlMessage = `

          ${alert.message.replace(/\n/g, '
          ')}

          -

          Property: ${alert.propertyId}

          -

          Change Type: ${CHANGE_TYPE_LABELS[alert.changeType]}

          -

          Severity: ${alert.severity.toUpperCase()}

          `; - - for (const channel of alert.channel) { - switch (channel) { - case 'email': - await this.notificationService.send({ - channel: 'email', - to: user.email, - subject: alert.title, - htmlBody: htmlMessage, - textBody: alert.message, - }); - break; - case 'push': - await this.notificationService.send({ - channel: 'push', - userId, - title: alert.title, - body: alert.message.slice(0, 200), - }); - break; - case 'sms': - await this.notificationService.send({ - channel: 'sms', - to: user.phone ?? '', - body: `[ShieldAI] ${alert.title}: ${alert.message.slice(0, 140)}`, - }); - break; - } - } - } catch (error) { - console.error('[HomeTitleAlertPipeline] Notification dispatch error:', error); - } - } - - private mapToNormalizedSeverity(severity: AlertSeverityLevel): string { - const map: Record = { - info: 'INFO', - warning: 'WARNING', - critical: 'CRITICAL', - }; - return map[severity] || 'INFO'; - } - - private mapToAlertCategory(changeType: ChangeType): string { - const map: Record = { - ownership_transfer: 'HOME_TITLE', - deed_change: 'HOME_TITLE', - lien_filing: 'HOME_TITLE', - tax_change: 'HOME_TITLE', - metadata_change: 'HOME_TITLE', - }; - return map[changeType] || 'HOME_TITLE'; - } - - cleanupExpiredDedups(): number { - const now = Date.now(); - let cleaned = 0; - for (const [key, expiry] of this.pendingDedup) { - if (now >= expiry) { - this.pendingDedup.delete(key); - cleaned++; - } - } - return cleaned; - } -} - -export const homeTitleAlertPipeline = new HomeTitleAlertPipeline(); diff --git a/services/hometitle/src/change-detector.ts b/services/hometitle/src/change-detector.ts deleted file mode 100644 index d15f431..0000000 --- a/services/hometitle/src/change-detector.ts +++ /dev/null @@ -1,202 +0,0 @@ -import { - PropertySnapshot, - ChangeDetectionResult, - ChangeType, - Severity, - PropertyChange, - DetectionConfig, - Address, -} from './types'; -import { matchRecords } from './matcher.service'; - -const DEFAULT_DETECTION_CONFIG: DetectionConfig = { - ownershipNameThreshold: 0.7, - deedDateSensitivity: 0.9, - taxAmountChangePercent: 15, -}; - -function classifyFieldChange(field: string, oldValue: unknown, newValue: unknown, config: DetectionConfig): PropertyChange { - let changeType: ChangeType; - - switch (field) { - case 'ownerName': - changeType = - typeof oldValue === 'string' && typeof newValue === 'string' - ? isSignificantNameChange(oldValue, newValue, config) - ? 'ownership_transfer' - : 'metadata_change' - : 'ownership_transfer'; - break; - case 'deedDate': - changeType = 'deed_change'; - break; - case 'taxAmount': - changeType = 'tax_change'; - break; - case 'lienCount': - changeType = (newValue as number) > (oldValue as number) ? 'lien_filing' : 'metadata_change'; - break; - case 'taxId': - changeType = 'deed_change'; - break; - default: - changeType = 'metadata_change'; - } - - return { field, oldValue, newValue, changeType }; -} - -function isSignificantNameChange(oldName: string, newName: string, config: DetectionConfig): boolean { - const dummyAddress: Address = { - streetNumber: '0', - streetName: 'dummy', - city: 'dummy', - state: 'XX', - zip: '00000', - }; - - const result = matchRecords(oldName, dummyAddress, newName, dummyAddress); - return result.nameScore < config.ownershipNameThreshold; -} - -function determineSeverity(changes: PropertyChange[], config: DetectionConfig): Severity { - const severityOverrides = config.severityOverrides || {}; - - const typeToSeverity: Record = { - ownership_transfer: (severityOverrides as Record)['ownership_transfer'] || 'critical', - deed_change: (severityOverrides as Record)['deed_change'] || 'warning', - lien_filing: (severityOverrides as Record)['lien_filing'] || 'warning', - tax_change: (severityOverrides as Record)['tax_change'] || 'info', - metadata_change: (severityOverrides as Record)['metadata_change'] || 'info', - }; - - const severityOrder: Severity[] = ['critical', 'warning', 'info']; - - for (const change of changes) { - const sev = typeToSeverity[change.changeType]; - const idx = severityOrder.indexOf(sev); - if (idx === 0) return 'critical'; - } - - for (const change of changes) { - const sev = typeToSeverity[change.changeType]; - if (sev === 'warning') return 'warning'; - } - - return 'info'; -} - -function computeChangeConfidence(changes: PropertyChange[], config: DetectionConfig): number { - if (changes.length === 0) return 0; - - let totalConfidence = 0; - for (const change of changes) { - switch (change.changeType) { - case 'ownership_transfer': - totalConfidence += 0.95; - break; - case 'deed_change': - totalConfidence += config.deedDateSensitivity; - break; - case 'tax_change': { - const oldVal = change.oldValue as number; - const newVal = change.newValue as number; - const pctChange = oldVal ? Math.abs(newVal - oldVal) / oldVal * 100 : 100; - totalConfidence += pctChange >= config.taxAmountChangePercent ? 0.85 : 0.5; - break; - } - case 'lien_filing': - totalConfidence += 0.9; - break; - default: - totalConfidence += 0.4; - } - } - - return Math.round((totalConfidence / changes.length) * 1000) / 1000; -} - -export function detectChanges( - previous: PropertySnapshot, - current: PropertySnapshot, - config?: Partial, -): ChangeDetectionResult { - const effectiveConfig = { ...DEFAULT_DETECTION_CONFIG, ...config }; - const changes: PropertyChange[] = []; - - const fieldsToCompare: (keyof Omit)[] = [ - 'ownerName', - 'deedDate', - 'taxId', - 'taxAmount', - 'lienCount', - 'propertyType', - ]; - - for (const field of fieldsToCompare) { - const oldValue = previous[field]; - const newValue = current[field]; - - if (oldValue !== newValue) { - changes.push(classifyFieldChange(field, oldValue, newValue, effectiveConfig)); - } - } - - const addressChanges = detectAddressChanges(previous.address, current.address); - changes.push(...addressChanges); - - const severity = determineSeverity(changes, effectiveConfig); - const confidence = computeChangeConfidence(changes, effectiveConfig); - - let changeType: ChangeType = 'metadata_change'; - if (changes.some(c => c.changeType === 'ownership_transfer')) { - changeType = 'ownership_transfer'; - } else if (changes.some(c => c.changeType === 'deed_change')) { - changeType = 'deed_change'; - } else if (changes.some(c => c.changeType === 'lien_filing')) { - changeType = 'lien_filing'; - } else if (changes.some(c => c.changeType === 'tax_change')) { - changeType = 'tax_change'; - } - - return { - propertyId: previous.propertyId, - changeType, - severity, - confidence, - changes, - previousSnapshot: previous, - currentSnapshot: current, - detectedAt: new Date().toISOString(), - }; -} - -function detectAddressChanges(oldAddr: Address, newAddr: Address): PropertyChange[] { - const changes: PropertyChange[] = []; - - const addressFields: (keyof Address)[] = ['streetNumber', 'streetName', 'streetType', 'unit', 'city', 'state', 'zip']; - - for (const field of addressFields) { - const oldVal = oldAddr[field]; - const newVal = newAddr[field]; - if (oldVal !== newVal) { - changes.push({ - field: `address.${field}`, - oldValue: oldVal, - newValue: newVal, - changeType: 'metadata_change', - }); - } - } - - return changes; -} - -export function shouldTriggerAlert(result: ChangeDetectionResult, minSeverity: Severity = 'warning'): boolean { - const severityOrder: Severity[] = ['info', 'warning', 'critical']; - const resultIdx = severityOrder.indexOf(result.severity); - const minIdx = severityOrder.indexOf(minSeverity); - return resultIdx >= minIdx && result.confidence >= 0.7; -} - -export { classifyFieldChange, determineSeverity, computeChangeConfidence }; diff --git a/services/hometitle/src/index.ts b/services/hometitle/src/index.ts deleted file mode 100644 index c5e7dfc..0000000 --- a/services/hometitle/src/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -export { - matchRecords, - getConfigForPropertyType, - parseName, - normalizeString, - normalizeStreetType, - levenshteinDistance, - similarityScore, -} from './matcher.service'; - -export { - detectChanges, - shouldTriggerAlert, - classifyFieldChange, - determineSeverity, - computeChangeConfidence, -} from './change-detector'; - -export type { - PropertyRecord, - Address, - PropertyType, - PropertySnapshot, - MatchResult, - MatchDetails, - FieldMatch, - ChangeDetectionResult, - ChangeType, - Severity, - PropertyChange, - MatchingConfig, - DetectionConfig, - NormalizedTokens, - AlertSeverityLevel, - PropertyAlert, - NotificationChannel, - AlertPipelineConfig, - SchedulerConfig, - ScheduledScanResult, -} from './types'; - -export { HomeTitleAlertPipeline, homeTitleAlertPipeline } from './alert.pipeline'; -export { HomeTitleSchedulerService, homeTitleScheduler } from './scheduler.service'; -export { - PropertyWatchlistService, - propertyWatchlistService, - normalizeAddressValue, - hashAddressValue, -} from './watchlist.service'; -export { - PropertyScannerService, - propertyScannerService, -} from './scanner.service'; -export type { - CountyDeedRecord, - DataSourceType, -} from './types'; diff --git a/services/hometitle/src/matcher.service.ts b/services/hometitle/src/matcher.service.ts deleted file mode 100644 index 311b63d..0000000 --- a/services/hometitle/src/matcher.service.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { - Address, - MatchResult, - MatchDetails, - FieldMatch, - MatchingConfig, - NormalizedTokens, - PropertyType, -} from './types'; - -const DEFAULT_CONFIG: MatchingConfig = { - nameThreshold: 0.85, - addressThreshold: 0.9, - overallThreshold: 0.85, - geocodingRadiusMeters: 100, -}; - -const COMMON_PREFIXES = new Set([ - 'mr', 'mrs', 'ms', 'miss', 'dr', 'prof', 'jr', 'sr', 'junior', 'senior', - 'ii', 'iii', 'iv', 'rev', 'st', 'hon', 'esq', -]); - -const COMMON_SUFFIXES = new Set([ - 'jr', 'sr', 'junior', 'senior', 'ii', 'iii', 'iv', 'v', 'esq', - 'phd', 'md', 'llm', 'cpa', -]); - -const STREET_TYPE_MAP: Record = { - 'st': 'street', 'street': 'street', - 'ave': 'avenue', 'avenue': 'avenue', - 'blvd': 'boulevard', 'boulevard': 'boulevard', - 'dr': 'drive', 'drive': 'drive', - 'ln': 'lane', 'lane': 'lane', - 'ct': 'court', 'court': 'court', - 'pl': 'place', 'place': 'place', - 'rd': 'road', 'road': 'road', - 'way': 'way', - 'trl': 'trail', 'trail': 'trail', - 'hwy': 'highway', 'highway': 'highway', - 'pkwy': 'parkway', 'parkway': 'parkway', - 'cir': 'circle', 'circle': 'circle', - 'sq': 'square', 'square': 'square', - 'ter': 'terrace', 'terrace': 'terrace', -}; - -const PROPERTY_TYPE_CONFIGS: Record> = { - 'residential': { nameThreshold: 0.85, addressThreshold: 0.9 }, - 'commercial': { nameThreshold: 0.8, addressThreshold: 0.9 }, - 'land': { nameThreshold: 0.8, addressThreshold: 0.85 }, - 'multi-family': { nameThreshold: 0.8, addressThreshold: 0.9 }, -}; - -function levenshteinDistance(a: string, b: string): number { - const matrix: number[][] = Array.from({ length: b.length + 1 }, (_, i) => - Array.from({ length: a.length + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)) - ); - - for (let i = 1; i <= b.length; i++) { - for (let j = 1; j <= a.length; j++) { - const cost = a[j - 1] === b[i - 1] ? 0 : 1; - matrix[i][j] = Math.min( - matrix[i - 1][j] + 1, - matrix[i][j - 1] + 1, - matrix[i - 1][j - 1] + cost, - ); - } - } - - return matrix[b.length][a.length]; -} - -function similarityScore(distance: number, maxLen: number): number { - if (maxLen === 0) return 1.0; - return 1.0 - distance / maxLen; -} - -function normalizeString(str: string): string { - return str - .toLowerCase() - .replace(/[''']/g, '') - .replace(/[^a-z0-9\s]/g, ' ') - .replace(/\s+/g, ' ') - .trim(); -} - -function parseName(name: string): NormalizedTokens { - const clean = normalizeString(name); - const parts = clean.split(' ').filter(Boolean); - - let firstName = ''; - let lastName = ''; - let middleName = ''; - const initials: string[] = []; - - if (parts.length === 0) return { firstName, lastName, middleName, initials }; - - let startIdx = 0; - while (startIdx < parts.length && COMMON_PREFIXES.has(parts[startIdx])) { - startIdx++; - } - - let endIdx = parts.length; - while (endIdx > startIdx + 1 && COMMON_SUFFIXES.has(parts[endIdx - 1])) { - endIdx--; - } - - const coreParts = parts.slice(startIdx, endIdx); - - if (coreParts.length === 1) { - lastName = coreParts[0]; - } else if (coreParts.length === 2) { - firstName = coreParts[0]; - lastName = coreParts[1]; - } else { - firstName = coreParts[0]; - lastName = coreParts[coreParts.length - 1]; - middleName = coreParts.slice(1, -1).join(' '); - } - - if (firstName.length === 1) { - initials.push(firstName); - } - if (middleName) { - const middleParts = middleName.split(' '); - for (const mp of middleParts) { - if (mp.length === 1) initials.push(mp); - } - } - - return { firstName, lastName, middleName, initials }; -} - -function normalizeStreetType(type: string): string { - const clean = normalizeString(type); - return STREET_TYPE_MAP[clean] || clean; -} - -function normalizeAddress(addr: Address): string { - const parts = [ - addr.streetNumber, - normalizeString(addr.streetName), - addr.streetType ? normalizeStreetType(addr.streetType) : '', - addr.unit ? normalizeString(addr.unit) : '', - normalizeString(addr.city), - addr.state.toLowerCase(), - addr.zip, - ].filter(Boolean); - return parts.join(' '); -} - -function computeFieldMatch(valueA: string, valueB: string, normalizeFn?: (v: string) => string): FieldMatch { - const normFn = normalizeFn || normalizeString; - const normalizedA = normFn(valueA); - const normalizedB = normFn(valueB); - - if (!normalizedA && !normalizedB) return { valueA, valueB, normalizedA, normalizedB, score: 1.0 }; - if (!normalizedA || !normalizedB) return { valueA, valueB, normalizedA, normalizedB, score: 0.0 }; - - if (normalizedA === normalizedB) return { valueA, valueB, normalizedA, normalizedB, score: 1.0 }; - - const dist = levenshteinDistance(normalizedA, normalizedB); - const maxLen = Math.max(normalizedA.length, normalizedB.length); - const score = similarityScore(dist, maxLen); - - return { valueA, valueB, normalizedA, normalizedB, score: Math.round(score * 1000) / 1000 }; -} - -function haversineDistance(lat1: number, lon1: number, lat2: number, lon2: number): number { - const R = 6371000; - const dLat = ((lat2 - lat1) * Math.PI) / 180; - const dLon = ((lon2 - lon1) * Math.PI) / 180; - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos((lat1 * Math.PI) / 180) * - Math.cos((lat2 * Math.PI) / 180) * - Math.sin(dLon / 2) * - Math.sin(dLon / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - return R * c; -} - -function computeNameScore(tokensA: NormalizedTokens, tokensB: NormalizedTokens): number { - const firstScore = computeFieldMatch(tokensA.firstName, tokensB.firstName).score; - const lastScore = computeFieldMatch(tokensA.lastName, tokensB.lastName).score; - const middleScore = computeFieldMatch(tokensA.middleName, tokensB.middleName).score; - - let initialMatchScore = 1.0; - if (tokensA.initials.length > 0 || tokensB.initials.length > 0) { - const allInitialsA = new Set(tokensA.initials.map(i => i.toLowerCase())); - const allInitialsB = new Set(tokensB.initials.map(i => i.toLowerCase())); - let matched = 0; - for (const init of allInitialsA) { - if (allInitialsB.has(init)) matched++; - } - const total = Math.max(allInitialsA.size, allInitialsB.size); - initialMatchScore = total > 0 ? matched / total : 1.0; - } - - const weighted = (lastScore * 0.45) + (firstScore * 0.35) + (middleScore * 0.1) + (initialMatchScore * 0.1); - return Math.round(weighted * 1000) / 1000; -} - -function computeAddressScore(addrA: Address, addrB: Address, config: MatchingConfig): { score: number; geocodingDistance?: number } { - const numberMatch = computeFieldMatch(addrA.streetNumber, addrB.streetNumber).score; - const streetMatch = computeFieldMatch(addrA.streetName, addrB.streetName, normalizeString).score; - const typeMatch = computeFieldMatch( - addrA.streetType ? normalizeStreetType(addrA.streetType) : '', - addrB.streetType ? normalizeStreetType(addrB.streetType) : '', - ).score; - const unitMatch = computeFieldMatch(addrA.unit || '', addrB.unit || '').score; - const cityMatch = computeFieldMatch(addrA.city, addrB.city).score; - const stateMatch = computeFieldMatch(addrA.state, addrB.state).score; - const zipMatch = computeFieldMatch(addrA.zip, addrB.zip).score; - - let geocodingDistance: number | undefined; - let geoScore = 0.0; - - if (addrA.latitude && addrA.longitude && addrB.latitude && addrB.longitude) { - geocodingDistance = haversineDistance(addrA.latitude, addrA.longitude, addrB.latitude, addrB.longitude); - const maxDist = config.geocodingRadiusMeters; - geoScore = geocodingDistance <= maxDist ? 1.0 : Math.max(0, 1.0 - (geocodingDistance - maxDist) / (maxDist * 5)); - } - - const weighted = - (numberMatch * 0.2) + - (streetMatch * 0.25) + - (typeMatch * 0.1) + - (unitMatch * 0.1) + - (cityMatch * 0.1) + - (stateMatch * 0.1) + - (zipMatch * 0.1) + - (geoScore * (geocodingDistance !== undefined ? 0.05 : 0)); - - return { score: Math.round(weighted * 1000) / 1000, geocodingDistance }; -} - -export function matchRecords( - nameA: string, - addressA: Address, - nameB: string, - addressB: Address, - config?: Partial, -): MatchResult { - const effectiveConfig = { ...DEFAULT_CONFIG, ...config }; - - const tokensA = parseName(nameA); - const tokensB = parseName(nameB); - - const nameScore = computeNameScore(tokensA, tokensB); - - const { score: addressScore, geocodingDistance } = computeAddressScore(addressA, addressB, effectiveConfig); - - const overallConfidence = Math.round((nameScore * 0.5 + addressScore * 0.5) * 1000) / 1000; - - const firstMatch = computeFieldMatch(tokensA.firstName, tokensB.firstName); - const lastMatch = computeFieldMatch(tokensA.lastName, tokensB.lastName); - const middleMatch = computeFieldMatch(tokensA.middleName, tokensB.middleName); - const numberMatch = computeFieldMatch(addressA.streetNumber, addressB.streetNumber); - const streetMatch = computeFieldMatch(addressA.streetName, addressB.streetName, normalizeString); - const typeMatch = computeFieldMatch( - addressA.streetType ? normalizeStreetType(addressA.streetType) : '', - addressB.streetType ? normalizeStreetType(addressB.streetType) : '', - ); - const unitMatch = computeFieldMatch(addressA.unit || '', addressB.unit || ''); - const cityMatch = computeFieldMatch(addressA.city, addressB.city); - const stateMatch = computeFieldMatch(addressA.state, addressB.state); - const zipMatch = computeFieldMatch(addressA.zip, addressB.zip); - - const normalizedA = normalizeAddress(addressA); - const normalizedB = normalizeAddress(addressB); - - const dist = levenshteinDistance( - normalizeString(nameA), - normalizeString(nameB), - ); - - const details: MatchDetails = { - nameNormalized: [normalizeString(nameA), normalizeString(nameB)], - addressNormalized: [normalizedA, normalizedB], - levenshteinDistance: dist, - geocodingDistance, - fields: { - firstName: firstMatch, - lastName: lastMatch, - middleName: middleMatch, - streetNumber: numberMatch, - streetName: streetMatch, - streetType: typeMatch, - unit: unitMatch, - city: cityMatch, - state: stateMatch, - zip: zipMatch, - }, - }; - - return { - nameScore, - addressScore, - overallConfidence, - isMatch: overallConfidence >= effectiveConfig.overallThreshold, - details, - }; -} - -export function getConfigForPropertyType(type: PropertyType): MatchingConfig { - return { ...DEFAULT_CONFIG, ...PROPERTY_TYPE_CONFIGS[type] }; -} - -export { parseName, normalizeString, normalizeStreetType, levenshteinDistance, similarityScore }; diff --git a/services/hometitle/src/scanner.service.ts b/services/hometitle/src/scanner.service.ts deleted file mode 100644 index 02b6d31..0000000 --- a/services/hometitle/src/scanner.service.ts +++ /dev/null @@ -1,503 +0,0 @@ -import { prisma } from '@shieldai/db'; -import { RateLimiterMemory, RateLimiterRes } from 'rate-limiter-flexible'; -import { createCache } from 'cache-manager'; -import { CacheableMemory } from 'cacheable'; -import { v4 as uuidv4 } from 'uuid'; -import type { PropertyRecord, CountyDeedRecord, DataSourceType } from './types'; - -export interface ATTOMPropertyResponse { - data: { - propertyId: string; - parcelNumber: string; - address: { - streetAddress: string; - city: string; - state: string; - zipCode: string; - latitude: number; - longitude: number; - }; - owner: { - ownerName1: string; - ownerName2?: string; - mailingAddress?: string; - }; - assessment: { - totalValue: number; - landValue: number; - improvementValue: number; - taxYear: number; - taxAmount: number; - }; - propertyDetails: { - propertyType: string; - yearBuilt: number; - squareFeet: number; - bedrooms?: number; - bathrooms?: number; - }; - salesHistory: Array<{ - saleDate: string; - salePrice: number; - documentType: string; - }>; - }; -} - -export interface USPSStandardizedAddress { - streetNumber: string; - streetName: string; - streetSuffix?: string; - unitType?: string; - unitValue?: string; - city: string; - state: string; - zip5: string; - zip4?: string; - deliveryPointBarCode?: string; - isDeliverable: boolean; -} - -interface ScannerConfig { - attomApiKey: string; - uspsApiKey: string; - countyScraperEnabled: boolean; - cacheTTL: number; - rateLimitPoints: number; - rateLimitDuration: number; - retryAttempts: number; - retryDelayMs: number; -} - -const DEFAULT_CONFIG: ScannerConfig = { - attomApiKey: process.env.ATTOM_API_KEY || '', - uspsApiKey: process.env.USPS_API_KEY || '', - countyScraperEnabled: process.env.COUNTY_SCRAPER_ENABLED === 'true', - cacheTTL: 3600, - rateLimitPoints: 100, - rateLimitDuration: 60, - retryAttempts: 3, - retryDelayMs: 1000, -}; - -export class PropertyScannerService { - private config: ScannerConfig; - private cache: ReturnType; - private attomRateLimiter: RateLimiterMemory; - private countyRateLimiter: RateLimiterMemory; - - constructor(config?: Partial) { - this.config = { ...DEFAULT_CONFIG, ...config }; - const memoryStore = new CacheableMemory({ - ttl: this.config.cacheTTL * 1000, - lruSize: 10000, - }); - this.cache = createCache({ - stores: [memoryStore as any], - }); - this.attomRateLimiter = new RateLimiterMemory({ - points: this.config.rateLimitPoints, - duration: this.config.rateLimitDuration, - }); - this.countyRateLimiter = new RateLimiterMemory({ - points: this.config.rateLimitPoints, - duration: this.config.rateLimitDuration, - }); - } - - private async withRetry( - fn: () => Promise, - operation: string, - attempts = this.config.retryAttempts - ): Promise { - for (let i = 0; i < attempts; i++) { - try { - return await fn(); - } catch (error) { - const isLastAttempt = i === attempts - 1; - if (isLastAttempt) { - throw new Error(`Failed ${operation} after ${attempts} attempts: ${error}`); - } - await new Promise(resolve => setTimeout(resolve, this.config.retryDelayMs * Math.pow(2, i))); - } - } - throw new Error(`Failed ${operation}`); - } - - private async checkRateLimiter(rateLimiter: RateLimiterMemory, key: string): Promise { - try { - await rateLimiter.consume(key); - } catch (rejRes) { - const res = rejRes as RateLimiterRes; - const retryAfter = res.msBeforeNext / 1000; - throw new Error(`Rate limit exceeded. Retry after ${retryAfter} seconds`); - } - } - - async fetchByAddress(address: string, county?: string): Promise { - const cacheKey = `property:address:${address}`; - - try { - const cached = await this.cache.get(cacheKey); - if (cached) { - return cached; - } - } catch (error) { - console.warn('Cache read error:', error); - } - - const record = await this.withRetry( - async () => { - await this.checkRateLimiter(this.attomRateLimiter, 'attom'); - - const attomResult = await this.fetchFromATTOM({ address }); - if (!attomResult) { - return null; - } - - const deedHistory = this.config.countyScraperEnabled && county - ? await this.fetchCountyDeeds(attomResult.parcelNumber, county) - : []; - - const liens = this.config.countyScraperEnabled && county - ? await this.fetchCountyLiens(attomResult.parcelNumber, county) - : []; - - return { - ...attomResult, - deedHistory: deedHistory.length > 0 ? deedHistory : undefined, - liens: liens.length > 0 ? liens : undefined, - dataSource: (deedHistory.length > 0 || liens.length > 0 ? 'combined' : 'attom') as DataSourceType, - lastUpdated: new Date().toISOString(), - }; - }, - 'fetch property by address' - ); - - if (record) { - try { - await this.cache.set(cacheKey, record, this.config.cacheTTL); - } catch (error) { - console.warn('Cache write error:', error); - } - } - - return record; - } - - async fetchByParcel(parcelNumber: string, county: string): Promise { - const cacheKey = `property:parcel:${parcelNumber}:${county}`; - - try { - const cached = await this.cache.get(cacheKey); - if (cached) { - return cached; - } - } catch (error) { - console.warn('Cache read error:', error); - } - - const record = await this.withRetry( - async () => { - await this.checkRateLimiter(this.attomRateLimiter, 'attom'); - - const attomResult = await this.fetchFromATTOM({ parcelNumber, county }); - if (!attomResult) { - return null; - } - - const deedHistory = this.config.countyScraperEnabled - ? await this.fetchCountyDeeds(parcelNumber, county) - : []; - - const liens = this.config.countyScraperEnabled - ? await this.fetchCountyLiens(parcelNumber, county) - : []; - - return { - ...attomResult, - deedHistory: deedHistory.length > 0 ? deedHistory : undefined, - liens: liens.length > 0 ? liens : undefined, - dataSource: (deedHistory.length > 0 || liens.length > 0 ? 'combined' : 'attom') as DataSourceType, - lastUpdated: new Date().toISOString(), - }; - }, - 'fetch property by parcel' - ); - - if (record) { - try { - await this.cache.set(cacheKey, record, this.config.cacheTTL); - } catch (error) { - console.warn('Cache write error:', error); - } - } - - return record; - } - - private async fetchFromATTOM( - query: { address?: string; parcelNumber?: string; county?: string } - ): Promise | null> { - try { - const url = new URL('https://api.attomdata.com/propertyapi/v4.0.0/property/get'); - - if (query.address) { - url.searchParams.append('address', query.address); - } else if (query.parcelNumber && query.county) { - url.searchParams.append('parcelnumber', query.parcelNumber); - url.searchParams.append('county', query.county); - } else { - throw new Error('Either address or parcelNumber+county must be provided'); - } - - const response = await fetch(url.toString(), { - headers: { - 'Authorization': `Bearer ${this.config.attomApiKey}`, - 'Content-Type': 'application/json', - }, - }); - - if (response.status === 404) { - return null; - } - - if (!response.ok) { - throw new Error(`ATTOM API error: ${response.status} ${response.statusText}`); - } - - const data = await response.json() as ATTOMPropertyResponse; - - return { - id: uuidv4(), - address: { - streetAddress: data.data.address?.streetAddress ?? 'Unknown', - city: data.data.address?.city ?? 'Unknown', - state: data.data.address?.state ?? 'Unknown', - zipCode: data.data.address?.zipCode ?? 'Unknown', - latitude: data.data.address?.latitude, - longitude: data.data.address?.longitude, - }, - parcelNumber: data.data.parcelNumber ?? 'Unknown', - ownerName: data.data.owner?.ownerName1 ?? 'Unknown', - assessment: data.data.assessment - ? { - totalValue: data.data.assessment.totalValue, - landValue: data.data.assessment.landValue, - improvementValue: data.data.assessment.improvementValue, - taxYear: data.data.assessment.taxYear, - taxAmount: data.data.assessment.taxAmount, - } - : undefined, - propertyDetails: data.data.propertyDetails - ? { - propertyType: data.data.propertyDetails.propertyType, - yearBuilt: data.data.propertyDetails.yearBuilt, - squareFeet: data.data.propertyDetails.squareFeet, - } - : undefined, - }; - } catch (error) { - console.error('ATTOM API fetch error:', error); - throw error; - } - } - - private async fetchCountyDeeds(parcelNumber: string, county: string): Promise { - try { - await this.checkRateLimiter(this.countyRateLimiter, `county:${county}`); - - const countyRecords = await prisma.countyDeedRecord.findMany({ - where: { - parcelNumber, - county, - documentType: { - in: ['DEED', 'QUITCLAIM DEED', 'WARRANTY DEED', 'SPECIAL WARRANTY DEED'], - }, - }, - orderBy: { recordingDate: 'desc' }, - take: 10, - }); - - return countyRecords.map((record: { - documentId: string; - recordingDate: Date; - documentType: string; - grantorName: string; - granteeName: string; - propertyAddress: string; - parcelNumber: string; - considerationAmount: number | null; - lienAmount: number | null; - }) => ({ - documentId: record.documentId, - recordingDate: record.recordingDate.toISOString(), - documentType: record.documentType, - grantorName: record.grantorName, - granteeName: record.granteeName, - propertyAddress: record.propertyAddress, - parcelNumber: record.parcelNumber, - considerationAmount: record.considerationAmount ?? undefined, - })); - } catch (error) { - console.error(`County deeds fetch error for ${county}:`, error); - return []; - } - } - - private async fetchCountyLiens(parcelNumber: string, county: string): Promise { - try { - await this.checkRateLimiter(this.countyRateLimiter, `county:${county}`); - - const lienRecords = await prisma.countyDeedRecord.findMany({ - where: { - parcelNumber, - county, - documentType: { - in: ['LIEN', 'MECHANIC LIEN', 'TAX LIEN', 'JUDGMENT LIEN', 'MORTGAGE'], - }, - }, - orderBy: { recordingDate: 'desc' }, - take: 20, - }); - - return lienRecords.map((record: { - documentId: string; - recordingDate: Date; - documentType: string; - grantorName: string; - granteeName: string; - propertyAddress: string; - parcelNumber: string; - considerationAmount: number | null; - lienAmount: number | null; - }) => ({ - documentId: record.documentId, - recordingDate: record.recordingDate.toISOString(), - documentType: record.documentType, - grantorName: record.grantorName, - granteeName: record.granteeName, - propertyAddress: record.propertyAddress, - parcelNumber: record.parcelNumber, - lienAmount: record.lienAmount ?? undefined, - lienType: record.documentType, - })); - } catch (error) { - console.error(`County liens fetch error for ${county}:`, error); - return []; - } - } - - async standardizeAddress(address: string): Promise { - const cacheKey = `usps:address:${address}`; - - try { - const cached = await this.cache.get(cacheKey); - if (cached) { - return cached; - } - } catch (error) { - console.warn('Cache read error:', error); - } - - const standardized = await this.withRetry( - async () => { - await this.checkRateLimiter(this.attomRateLimiter, 'usps'); - - const url = new URL('https://api.usps.com/api/address/standardize'); - url.searchParams.append('address', address); - url.searchParams.append('api-key', this.config.uspsApiKey); - - const response = await fetch(url.toString()); - - if (!response.ok) { - if (response.status === 404) { - return null; - } - throw new Error(`USPS API error: ${response.status} ${response.statusText}`); - } - - const data = await response.json() as any; - - const result: USPSStandardizedAddress = { - streetNumber: data.streetNumber, - streetName: data.streetName, - streetSuffix: data.streetSuffix, - unitType: data.unitType, - unitValue: data.unitValue, - city: data.city, - state: data.state, - zip5: data.zip5, - zip4: data.zip4, - deliveryPointBarCode: data.deliveryPointBarCode, - isDeliverable: data.isDeliverable, - }; - - return result; - }, - 'standardize address' - ); - - if (standardized) { - try { - await this.cache.set(cacheKey, standardized, this.config.cacheTTL); - } catch (error) { - console.warn('Cache write error:', error); - } - } - - return standardized; - } - - async batchFetchProperties( - requests: Array<{ address?: string; parcelNumber?: string; county?: string }> - ): Promise<(PropertyRecord | null)[]> { - const results: (PropertyRecord | null)[] = []; - - for (const request of requests) { - try { - if (request.address) { - const result = await this.fetchByAddress(request.address, request.county); - results.push(result); - } else if (request.parcelNumber && request.county) { - const result = await this.fetchByParcel(request.parcelNumber, request.county); - results.push(result); - } else { - results.push(null); - } - - await new Promise(resolve => setTimeout(resolve, 100)); - } catch (error) { - console.error('Batch fetch error:', error); - results.push(null); - } - } - - return results; - } - - async invalidateCache(address?: string, parcelNumber?: string, county?: string): Promise { - if (address) { - await this.cache.del(`property:address:${address}`); - } - if (parcelNumber && county) { - await this.cache.del(`property:parcel:${parcelNumber}:${county}`); - } - } - - async getCacheStats(): Promise<{ hits: number; misses: number; size: number }> { - try { - const stats = await this.cache.get<{ hits: number; misses: number; size: number }>('cache:stats') || { - hits: 0, - misses: 0, - size: 0, - }; - return stats; - } catch (error) { - console.warn('Cache stats error:', error); - return { hits: 0, misses: 0, size: 0 }; - } - } -} - -export const propertyScannerService = new PropertyScannerService(); diff --git a/services/hometitle/src/scheduler.service.ts b/services/hometitle/src/scheduler.service.ts deleted file mode 100644 index 76d9526..0000000 --- a/services/hometitle/src/scheduler.service.ts +++ /dev/null @@ -1,237 +0,0 @@ -import { prisma } from '@shieldai/db'; -import { detectChanges, shouldTriggerAlert } from './change-detector'; -import { homeTitleAlertPipeline } from './alert.pipeline'; -import { - PropertySnapshot, - SchedulerConfig, - ScheduledScanResult, -} from './types'; -// @ts-expect-error uuid v10 ships with its own types but module resolution is complex -import { v4 as uuidv4 } from 'uuid'; - -const DEFAULT_SCHEDULER_CONFIG: SchedulerConfig = { - scanIntervalMinutes: 60, - premiumScanIntervalMinutes: 30, - maxPropertiesPerScan: 100, - enabled: true, -}; - -export class HomeTitleSchedulerService { - private config: SchedulerConfig; - private timerId: NodeJS.Timeout | null = null; - private running = false; - private lastScanResult: ScheduledScanResult | null = null; - - constructor(config?: Partial) { - this.config = { ...DEFAULT_SCHEDULER_CONFIG, ...config }; - } - - getConfig(): SchedulerConfig { - return { ...this.config }; - } - - updateConfig(partial: Partial): void { - this.config = { ...this.config, ...partial }; - if (partial.scanIntervalMinutes && this.timerId) { - this.stop(); - this.start(); - } - } - - start(): void { - if (!this.config.enabled) return; - - const intervalMs = this.config.scanIntervalMinutes * 60 * 1000; - this.running = true; - - this.timerId = setInterval(async () => { - if (this.running) { - try { - const result = await this.runScan(); - this.lastScanResult = result; - } catch (error) { - console.error('[HomeTitleScheduler] Scan error:', error); - } - } - }, intervalMs); - - console.log( - `[HomeTitleScheduler] Started with ${this.config.scanIntervalMinutes}min interval`, - ); - } - - stop(): void { - this.running = false; - if (this.timerId) { - clearInterval(this.timerId); - this.timerId = null; - } - console.log('[HomeTitleScheduler] Stopped'); - } - - async runScan(): Promise { - const scanId = uuidv4(); - const startedAt = new Date().toISOString(); - const errors: string[] = []; - let propertiesScanned = 0; - let changesDetected = 0; - let alertsCreated = 0; - let notificationsSent = 0; - - try { - const subscriptions = await prisma.subscription.findMany({ - where: { - status: 'active', - tier: { in: ['plus', 'premium'] }, - }, - select: { - id: true, - userId: true, - tier: true, - }, - take: this.config.maxPropertiesPerScan, - }); - - for (const subscription of subscriptions) { - try { - const propertySnapshots = await this.fetchLatestSnapshots( - subscription.userId, - ); - propertiesScanned += propertySnapshots.length; - - for (const snapshot of propertySnapshots) { - const previousSnapshot = await this.fetchPreviousSnapshot( - snapshot.propertyId, - snapshot.id, - snapshot.capturedAt, - ); - - if (!previousSnapshot) continue; - - const result = detectChanges(previousSnapshot, snapshot); - - if (shouldTriggerAlert(result, 'warning')) { - changesDetected++; - - const alert = await homeTitleAlertPipeline.processChangeDetection( - result, - subscription.id, - subscription.userId, - ); - - if (alert) { - alertsCreated++; - if (subscription.tier === 'premium') { - notificationsSent++; - } - } - } - } - } catch (error) { - const errorMsg = `Subscription ${subscription.id}: ${error instanceof Error ? error.message : String(error)}`; - errors.push(errorMsg); - console.error(`[HomeTitleScheduler] Subscription scan error:`, errorMsg); - } - } - } catch (error) { - const errorMsg = `Scan ${scanId}: ${error instanceof Error ? error.message : String(error)}`; - errors.push(errorMsg); - console.error(`[HomeTitleScheduler] Scan error:`, errorMsg); - } - - const completedAt = new Date().toISOString(); - - const scanResult: ScheduledScanResult = { - scanId, - propertiesScanned, - changesDetected, - alertsCreated, - notificationsSent, - errors, - startedAt, - completedAt, - }; - - this.lastScanResult = scanResult; - - return scanResult; - } - - getLastScanResult(): ScheduledScanResult | null { - return this.lastScanResult; - } - - isRunning(): boolean { - return this.running; - } - - private async fetchLatestSnapshots(userId: string): Promise { - const rawSnapshots = await prisma.$queryRaw< - Array> - >` - SELECT "id", "propertyId", "capturedAt", "ownerName", - "deedDate", "taxId", "propertyType", - "taxAmount", "lienCount" - FROM "PropertySnapshot" - WHERE "propertyId" IN ( - SELECT "propertyId" FROM "WatchlistItem" - WHERE "subscriptionId" IN ( - SELECT "id" FROM "Subscription" WHERE "userId" = ${userId} - ) - ) - ORDER BY "capturedAt" DESC - LIMIT ${this.config.maxPropertiesPerScan} - `; - - return rawSnapshots.map(row => ({ - id: String(row.id), - propertyId: String(row.propertyId), - capturedAt: String(row.capturedAt), - ownerName: String(row.ownerName), - address: row.address ? JSON.parse(String(row.address)) : {}, - deedDate: row.deedDate ? String(row.deedDate) : undefined, - taxId: row.taxId ? String(row.taxId) : undefined, - propertyType: String(row.propertyType) as PropertySnapshot['propertyType'], - taxAmount: row.taxAmount ? Number(row.taxAmount) : undefined, - lienCount: row.lienCount ? Number(row.lienCount) : undefined, - })); - } - - private async fetchPreviousSnapshot( - propertyId: string, - currentSnapshotId: string, - currentCapturedAt: string, - ): Promise { - const rawSnapshots = await prisma.$queryRaw< - Array> - >` - SELECT "id", "propertyId", "capturedAt", "ownerName", - "deedDate", "taxId", "propertyType", - "taxAmount", "lienCount" - FROM "PropertySnapshot" - WHERE "propertyId" = ${propertyId} - AND "capturedAt" < ${currentCapturedAt} - AND "id" != ${currentSnapshotId} - ORDER BY "capturedAt" DESC - LIMIT 1 - `; - - if (rawSnapshots.length === 0) return null; - - const row = rawSnapshots[0]; - return { - id: String(row.id), - propertyId: String(row.propertyId), - capturedAt: String(row.capturedAt), - ownerName: String(row.ownerName), - address: row.address ? JSON.parse(String(row.address)) : {}, - deedDate: row.deedDate ? String(row.deedDate) : undefined, - taxId: row.taxId ? String(row.taxId) : undefined, - propertyType: String(row.propertyType) as PropertySnapshot['propertyType'], - taxAmount: row.taxAmount ? Number(row.taxAmount) : undefined, - lienCount: row.lienCount ? Number(row.lienCount) : undefined, - }; - } -} - -export const homeTitleScheduler = new HomeTitleSchedulerService(); diff --git a/services/hometitle/src/types.ts b/services/hometitle/src/types.ts deleted file mode 100644 index 476f823..0000000 --- a/services/hometitle/src/types.ts +++ /dev/null @@ -1,204 +0,0 @@ -export interface PropertyRecord { - id: string; - address: { - streetAddress: string; - city: string; - state: string; - zipCode: string; - latitude?: number; - longitude?: number; - }; - parcelNumber: string; - ownerName: string; - assessment?: { - totalValue: number; - landValue: number; - improvementValue: number; - taxYear: number; - taxAmount: number; - }; - propertyDetails?: { - propertyType: string; - yearBuilt: number; - squareFeet: number; - }; - deedHistory?: CountyDeedRecord[]; - liens?: CountyDeedRecord[]; - dataSource: DataSourceType; - lastUpdated: string; - deedDate?: string; - taxId?: string; - metadata?: Record; -} - -export interface CountyDeedRecord { - documentId: string; - recordingDate: string; - documentType: string; - grantorName: string; - granteeName: string; - propertyAddress: string; - parcelNumber: string; - considerationAmount?: number; - lienAmount?: number; - lienType?: string; -} - -export type DataSourceType = 'attom' | 'county' | 'combined'; - -export interface Address { - streetNumber: string; - streetName: string; - streetType?: string; - unit?: string; - city: string; - state: string; - zip: string; - latitude?: number; - longitude?: number; -} - -export type PropertyType = 'residential' | 'commercial' | 'land' | 'multi-family'; - -export interface PropertySnapshot { - id: string; - propertyId: string; - capturedAt: string; - ownerName: string; - address: Address; - deedDate?: string; - taxId?: string; - propertyType: PropertyType; - taxAmount?: number; - lienCount?: number; -} - -export interface MatchResult { - nameScore: number; - addressScore: number; - overallConfidence: number; - isMatch: boolean; - details: MatchDetails; -} - -export interface MatchDetails { - nameNormalized: string[]; - addressNormalized: string[]; - levenshteinDistance: number; - geocodingDistance?: number; - fields: { - firstName: FieldMatch; - lastName: FieldMatch; - middleName: FieldMatch; - streetNumber: FieldMatch; - streetName: FieldMatch; - streetType: FieldMatch; - unit: FieldMatch; - city: FieldMatch; - state: FieldMatch; - zip: FieldMatch; - }; -} - -export interface FieldMatch { - valueA: string; - valueB: string; - normalizedA: string; - normalizedB: string; - score: number; -} - -export interface ChangeDetectionResult { - propertyId: string; - changeType: ChangeType; - severity: Severity; - confidence: number; - changes: PropertyChange[]; - previousSnapshot: PropertySnapshot; - currentSnapshot: PropertySnapshot; - detectedAt: string; -} - -export type ChangeType = 'tax_change' | 'deed_change' | 'ownership_transfer' | 'lien_filing' | 'metadata_change'; - -export type Severity = 'info' | 'warning' | 'critical'; - -export interface PropertyChange { - field: string; - oldValue: unknown; - newValue: unknown; - changeType: ChangeType; -} - -export interface MatchingConfig { - nameThreshold: number; - addressThreshold: number; - overallThreshold: number; - geocodingRadiusMeters: number; -} - -export interface DetectionConfig { - ownershipNameThreshold: number; - deedDateSensitivity: number; - taxAmountChangePercent: number; - severityOverrides?: Record; -} - -export interface NormalizedTokens { - firstName: string; - lastName: string; - middleName: string; - initials: string[]; -} - -// ============================================ -// Alert Pipeline Types -// ============================================ - -export type AlertSeverityLevel = 'info' | 'warning' | 'critical'; - -export interface PropertyAlert { - id: string; - propertyId: string; - subscriptionId: string; - userId: string; - changeType: ChangeType; - severity: AlertSeverityLevel; - title: string; - message: string; - changeDetectionResult: ChangeDetectionResult; - channel: NotificationChannel[]; - dedupKey: string; - createdAt: string; -} - -export type NotificationChannel = 'email' | 'sms' | 'push'; - -export interface AlertPipelineConfig { - dedupWindowMs: number; - minSeverity: Severity; - premiumTierChannels: NotificationChannel[]; - defaultChannels: NotificationChannel[]; -} - -// ============================================ -// Scheduler Types -// ============================================ - -export interface SchedulerConfig { - scanIntervalMinutes: number; - premiumScanIntervalMinutes: number; - maxPropertiesPerScan: number; - enabled: boolean; -} - -export interface ScheduledScanResult { - scanId: string; - propertiesScanned: number; - changesDetected: number; - alertsCreated: number; - notificationsSent: number; - errors: string[]; - startedAt: string; - completedAt: string; -} diff --git a/services/hometitle/src/watchlist.service.ts b/services/hometitle/src/watchlist.service.ts deleted file mode 100644 index e56de53..0000000 --- a/services/hometitle/src/watchlist.service.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { prisma, WatchlistType } from '@shieldai/db'; -import { createHash } from 'crypto'; - -export function normalizeAddressValue(address: string): string { - return address.trim().toLowerCase().replace(/\s+/g, ' '); -} - -export function hashAddressValue(value: string): string { - return createHash('sha256').update(value).digest('hex'); -} - -const TIER_LIMITS: Record = { - BASIC: 3, - PLUS: 5, - PREMIUM: 50, -}; - -export class PropertyWatchlistService { - async addItem( - subscriptionId: string, - address: string, - parcelId?: string, - ownerName?: string, - ) { - const normalized = normalizeAddressValue(address); - const itemHash = hashAddressValue(normalized); - - const currentCount = await prisma.propertyWatchlistItem.count({ - where: { subscriptionId, isActive: true }, - }); - - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { tier: true }, - }); - - if (!subscription) { - throw new Error(`Subscription not found: ${subscriptionId}`); - } - - const tier = subscription.tier.toUpperCase(); - const maxItems = TIER_LIMITS[tier] ?? 3; - - if (currentCount >= maxItems) { - throw new Error( - `Property watchlist limit reached (${maxItems} items). Your ${tier} plan allows up to ${maxItems} properties.`, - ); - } - - const existing = await prisma.propertyWatchlistItem.findFirst({ - where: { subscriptionId, address: normalized, isActive: true }, - }); - - if (existing) { - if (!existing.isActive) { - return prisma.propertyWatchlistItem.update({ - where: { id: existing.id }, - data: { isActive: true }, - }); - } - return existing; - } - - return prisma.propertyWatchlistItem.create({ - data: { - subscriptionId, - address: normalized, - parcelId: parcelId ?? null, - ownerName: ownerName ?? null, - streetAddress: normalized, - isActive: true, - }, - }); - } - - async getItems(subscriptionId: string) { - return prisma.propertyWatchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - orderBy: { createdAt: 'desc' }, - }); - } - - async removeItem(id: string, subscriptionId: string) { - const item = await prisma.propertyWatchlistItem.findFirst({ - where: { id, subscriptionId, isActive: true }, - }); - - if (!item) { - throw new Error(`Watchlist item not found: ${id}`); - } - - return prisma.propertyWatchlistItem.update({ - where: { id }, - data: { isActive: false }, - }); - } - - async getActiveItemsForScan(subscriptionId: string) { - return prisma.propertyWatchlistItem.findMany({ - where: { subscriptionId, isActive: true }, - include: { - snapshots: { - orderBy: { capturedAt: 'desc' }, - take: 1, - }, - }, - }); - } - - async getItemCount(subscriptionId: string) { - return prisma.propertyWatchlistItem.count({ - where: { subscriptionId, isActive: true }, - }); - } - - async getMaxItemsForTier(subscriptionId: string): Promise { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - select: { tier: true }, - }); - - if (!subscription) { - return 3; - } - - const tier = subscription.tier.toUpperCase(); - return TIER_LIMITS[tier] ?? 3; - } -} - -export const propertyWatchlistService = new PropertyWatchlistService(); diff --git a/services/hometitle/test/alert.pipeline.test.ts b/services/hometitle/test/alert.pipeline.test.ts deleted file mode 100644 index ba4607c..0000000 --- a/services/hometitle/test/alert.pipeline.test.ts +++ /dev/null @@ -1,429 +0,0 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { HomeTitleAlertPipeline } from '../src/alert.pipeline'; -import { - ChangeDetectionResult, - PropertySnapshot, - ChangeType, - Severity, -} from '../src/types'; - -// All mocks inside vi.hoisted() to avoid vitest hoisting issues -const mockedDb = vi.hoisted(() => { - const mocks = { - subscription: { findUnique: vi.fn() }, - alert: { create: vi.fn(), findFirst: vi.fn() }, - normalizedAlert: { create: vi.fn(), updateMany: vi.fn() }, - correlationGroup: { create: vi.fn() }, - user: { findUnique: vi.fn() }, - }; - return mocks; -}); - -vi.mock('@shieldai/db', () => { - const mocks = vi.hoisted ? mockedDb : { - subscription: { findUnique: vi.fn() }, - alert: { create: vi.fn(), findFirst: vi.fn() }, - normalizedAlert: { create: vi.fn(), updateMany: vi.fn() }, - correlationGroup: { create: vi.fn() }, - user: { findUnique: vi.fn() }, - }; - return { - prisma: mocks, - AlertSeverity: { INFO: 'INFO', WARNING: 'WARNING', CRITICAL: 'CRITICAL' }, - AlertChannel: { EMAIL: 'email', PUSH: 'push', SMS: 'sms' }, - }; -}); - -vi.mock('@shieldai/shared-notifications', () => { - const mockSend = vi.fn().mockResolvedValue({ notificationId: 'mock-notif', status: 'sent' }); - class MockNotificationService { - send = mockSend; - static getInstance() { return new MockNotificationService(); } - } - return { - NotificationService: MockNotificationService, - loadNotificationConfig: () => ({ apiKey: 'test-key', baseUrl: 'http://localhost:3000' }), - }; -}); - -function buildChangeResult(overrides: Partial = {}): ChangeDetectionResult { - return { - propertyId: 'prop-001', - changeType: 'ownership_transfer' as ChangeType, - severity: 'critical' as Severity, - confidence: 0.95, - changes: [ - { field: 'ownerName', oldValue: 'John Doe', newValue: 'Jane Smith', changeType: 'ownership_transfer' as ChangeType }, - ], - previousSnapshot: { - id: 'snap-1', - propertyId: 'prop-001', - capturedAt: '2026-01-01T00:00:00Z', - ownerName: 'John Doe', - address: { streetNumber: '123', streetName: 'main', city: 'springfield', state: 'IL', zip: '62701' }, - propertyType: 'residential', - } as PropertySnapshot, - currentSnapshot: { - id: 'snap-2', - propertyId: 'prop-001', - capturedAt: '2026-02-01T00:00:00Z', - ownerName: 'Jane Smith', - address: { streetNumber: '123', streetName: 'main', city: 'springfield', state: 'IL', zip: '62701' }, - propertyType: 'residential', - } as PropertySnapshot, - detectedAt: '2026-05-14T12:00:00Z', - ...overrides, - }; -} - -describe('HomeTitleAlertPipeline', () => { - let pipeline: HomeTitleAlertPipeline; - - beforeEach(() => { - vi.useFakeTimers(); - mockedDb.subscription.findUnique.mockClear(); - mockedDb.alert.findFirst.mockClear(); - mockedDb.alert.create.mockClear(); - mockedDb.normalizedAlert.create.mockClear(); - mockedDb.normalizedAlert.updateMany.mockClear(); - mockedDb.correlationGroup.create.mockClear(); - mockedDb.user.findUnique.mockClear(); - - pipeline = new HomeTitleAlertPipeline(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - describe('processChangeDetection', () => { - it('creates alert for critical severity change', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-001', - subscriptionId: 'sub-001', - userId: 'user-001', - type: 'system_warning', - title: '[CRITICAL] Ownership Transfer detected', - message: 'Change detected', - severity: 'CRITICAL', - channel: ['email', 'push', 'sms'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - - const result = buildChangeResult({ - changeType: 'ownership_transfer', - severity: 'critical', - confidence: 0.95, - }); - - const alert = await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - expect(alert).toBeDefined(); - expect(alert?.changeType).toBe('ownership_transfer'); - expect(alert?.severity).toBe('critical'); - expect(mockedDb.alert.create).toHaveBeenCalled(); - }); - - it('creates alert for warning severity change', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'plus' }); - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-002', - subscriptionId: 'sub-001', - userId: 'user-001', - type: 'system_warning', - title: '[WARNING] Deed Change detected', - message: 'Change detected', - severity: 'WARNING', - channel: ['email', 'push'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - - const result = buildChangeResult({ - changeType: 'deed_change', - severity: 'warning', - confidence: 0.85, - }); - - const alert = await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - expect(alert).toBeDefined(); - expect(alert?.changeType).toBe('deed_change'); - expect(alert?.severity).toBe('warning'); - }); - - it('returns null when subscription not found', async () => { - mockedDb.subscription.findUnique.mockResolvedValue(null); - - const result = buildChangeResult(); - const alert = await pipeline.processChangeDetection(result, 'sub-999', 'user-001'); - - expect(alert).toBeNull(); - }); - - it('returns null for minor severity with default minSeverity', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - - const result = buildChangeResult({ - changeType: 'tax_change', - severity: 'minor', - confidence: 0.85, - }); - - const alert = await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - expect(alert).toBeNull(); - }); - - it('returns null when confidence below threshold', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - - const result = buildChangeResult({ - confidence: 0.5, - }); - - const alert = await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - expect(alert).toBeNull(); - }); - - it('deduplicates alerts within 24h window', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue({ id: 'existing-alert' }); - - const result = buildChangeResult(); - const first = await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - const second = await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - expect(first).toBeDefined(); - expect(second).toBeNull(); - }); - - it('creates normalized alert for integration with correlation engine', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-003', - subscriptionId: 'sub-001', - userId: 'user-001', - type: 'system_warning', - title: '[MAJOR] Ownership Transfer detected', - message: 'Change detected', - severity: 'CRITICAL', - channel: ['email', 'push', 'sms'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - - const result = buildChangeResult(); - await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - expect(mockedDb.normalizedAlert.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ - source: 'DARKWATCH', - userId: 'user-001', - }), - }) - ); - }); - - it('dispatches notifications for premium tier', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-004', - subscriptionId: 'sub-001', - userId: 'user-001', - type: 'system_warning', - title: '[MAJOR] Ownership Transfer detected', - message: 'Change detected on property prop-001.\n\nChanges:\n- ownerName: John Doe → Jane Smith', - severity: 'CRITICAL', - channel: ['email', 'push', 'sms'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - mockedDb.user.findUnique.mockResolvedValue({ - email: 'test@example.com', - name: 'Test User', - }); - - const result = buildChangeResult(); - await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - // Notification service was instantiated (no error thrown) - expect(mockedDb.user.findUnique).toHaveBeenCalled(); - }); - - it('records dedup key in memory', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-005', - subscriptionId: 'sub-001', - userId: 'user-001', - type: 'system_warning', - title: '[MAJOR] Ownership Transfer detected', - message: 'Change', - severity: 'CRITICAL', - channel: ['email'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - - const result = buildChangeResult({ changeType: 'ownership_transfer' }); - await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - // No expired dedups at this point - const cleanupCount = pipeline.cleanupExpiredDedups(); - expect(cleanupCount).toBe(0); - }); - }); - - describe('processBatch', () => { - it('processes multiple change results', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-batch-1', - subscriptionId: 'sub-001', - userId: 'user-001', - type: 'system_warning', - title: '[CRITICAL] Ownership Transfer detected', - message: 'Change 1', - severity: 'CRITICAL', - channel: ['email'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - mockedDb.user.findUnique.mockResolvedValue({ email: 'test@example.com', name: 'Test' }); - mockedDb.correlationGroup.create.mockResolvedValue({ id: 'group-001' }); - - const results = [ - buildChangeResult({ changeType: 'ownership_transfer', propertyId: 'prop-001' }), - buildChangeResult({ changeType: 'deed_change', propertyId: 'prop-002' }), - ]; - - const alerts = await pipeline.processBatch(results, 'sub-001', 'user-001'); - - expect(alerts.length).toBe(2); - expect(mockedDb.alert.create).toHaveBeenCalledTimes(results.length); - }); - - it('creates correlation group for multiple alerts', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-batch-' + Date.now(), - subscriptionId: 'sub-001', - userId: 'user-001', - type: 'system_warning', - title: '[CRITICAL] Ownership Transfer detected', - message: 'Change', - severity: 'CRITICAL', - channel: ['email'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - mockedDb.user.findUnique.mockResolvedValue({ email: 'test@example.com', name: 'Test' }); - mockedDb.correlationGroup.create.mockResolvedValue({ id: 'group-002' }); - - const results = [ - buildChangeResult({ changeType: 'ownership_transfer', propertyId: 'prop-001' }), - buildChangeResult({ changeType: 'deed_change', propertyId: 'prop-002' }), - ]; - - await pipeline.processBatch(results, 'sub-001', 'user-001'); - - expect(mockedDb.correlationGroup.create).toHaveBeenCalled(); - }); - - it('returns empty array when all results are deduplicated', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue({ id: 'existing-alert' }); - - const results = [ - buildChangeResult({ changeType: 'ownership_transfer', propertyId: 'prop-001' }), - ]; - - const alerts = await pipeline.processBatch(results, 'sub-001', 'user-001'); - expect(alerts).toEqual([]); - }); - }); - - describe('cleanupExpiredDedups', () => { - it('removes expired dedup entries', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-cleanup', - subscriptionId: 'sub-001', - userId: 'user-001', - type: 'system_warning', - title: '[CRITICAL] Ownership Transfer detected', - message: 'Change', - severity: 'CRITICAL', - channel: ['email'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - - const result = buildChangeResult(); - await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - // Advance timer past the dedup window (24 hours) - vi.advanceTimersByTime(25 * 60 * 60 * 1000); - - const cleaned = pipeline.cleanupExpiredDedups(); - expect(cleaned).toBe(1); - }); - }); - - describe('severity mapping', () => { - it('maps critical to critical', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-sev-1', - subscriptionId: 'sub-001', - userId: 'user-001', - type: 'system_warning', - title: '[CRITICAL] Ownership Transfer detected', - message: 'Change', - severity: 'CRITICAL', - channel: ['email'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - - const result = buildChangeResult({ severity: 'critical' }); - await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - expect(mockedDb.alert.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ severity: 'critical' }) - }) - ); - }); - - it('maps warning to warning', async () => { - mockedDb.subscription.findUnique.mockResolvedValue({ tier: 'premium' }); - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-sev-2', - subscriptionId: 'sub-001', - userId: 'user-001', - type: 'system_warning', - title: '[WARNING] Deed Change detected', - message: 'Change', - severity: 'WARNING', - channel: ['email'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - - const result = buildChangeResult({ severity: 'warning', changeType: 'deed_change' }); - await pipeline.processChangeDetection(result, 'sub-001', 'user-001'); - - expect(mockedDb.alert.create).toHaveBeenCalledWith( - expect.objectContaining({ - data: expect.objectContaining({ severity: 'warning' }) - }) - ); - }); - }); -}); diff --git a/services/hometitle/test/change-detector.test.ts b/services/hometitle/test/change-detector.test.ts deleted file mode 100644 index 410ff21..0000000 --- a/services/hometitle/test/change-detector.test.ts +++ /dev/null @@ -1,305 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - detectChanges, - shouldTriggerAlert, - determineSeverity, - computeChangeConfidence, -} from '../src/change-detector'; -import { PropertySnapshot, PropertyChange, DetectionConfig } from '../src/types'; - -const baselineSnapshot: PropertySnapshot = { - id: 'snap-1', - propertyId: 'prop-001', - capturedAt: '2026-01-01T00:00:00Z', - ownerName: 'John Doe', - address: { - streetNumber: '123', - streetName: 'main', - streetType: 'st', - city: 'springfield', - state: 'IL', - zip: '62701', - }, - deedDate: '2020-03-15', - taxId: 'tax-123', - propertyType: 'residential', - taxAmount: 2500, - lienCount: 0, -}; - -describe('detectChanges', () => { - it('detects ownership transfer via name change', () => { - const current = { - ...baselineSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - ownerName: 'Jane Smith', - }; - const result = detectChanges(baselineSnapshot, current); - expect(result.changeType).toBe('ownership_transfer'); - expect(result.severity).toBe('critical'); - expect(result.changes.some(c => c.field === 'ownerName')).toBe(true); - }); - - it('detects deed change via deed date update', () => { - const current = { - ...baselineSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - deedDate: '2026-01-15', - }; - const result = detectChanges(baselineSnapshot, current); - expect(result.changes.some(c => c.changeType === 'deed_change')).toBe(true); - expect(result.severity).toBe('warning'); - }); - - it('detects tax change', () => { - const current = { - ...baselineSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - taxAmount: 3200, - }; - const result = detectChanges(baselineSnapshot, current); - expect(result.changes.some(c => c.changeType === 'tax_change')).toBe(true); - expect(result.severity).toBe('info'); - }); - - it('detects lien filing when lien count increases', () => { - const current = { - ...baselineSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - lienCount: 1, - }; - const result = detectChanges(baselineSnapshot, current); - expect(result.changes.some(c => c.changeType === 'lien_filing')).toBe(true); - expect(result.severity).toBe('warning'); - }); - - it('detects multiple changes with highest severity', () => { - const current = { - ...baselineSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - ownerName: 'Jane Smith', - deedDate: '2026-01-15', - taxAmount: 3200, - }; - const result = detectChanges(baselineSnapshot, current); - expect(result.severity).toBe('critical'); - expect(result.changes.length).toBeGreaterThanOrEqual(3); - }); - - it('returns no changes for identical snapshots', () => { - const current = { ...baselineSnapshot, id: 'snap-2', capturedAt: '2026-02-01T00:00:00Z' }; - const result = detectChanges(baselineSnapshot, current); - expect(result.changes.length).toBe(0); - expect(result.severity).toBe('info'); - }); - - it('detects address changes as metadata changes', () => { - const current = { - ...baselineSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - address: { - ...baselineSnapshot.address, - streetNumber: '125', - }, - }; - const result = detectChanges(baselineSnapshot, current); - expect(result.changes.some(c => c.field === 'address.streetNumber')).toBe(true); - }); - - it('detects tax ID change as deed change', () => { - const current = { - ...baselineSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - taxId: 'tax-456', - }; - const result = detectChanges(baselineSnapshot, current); - expect(result.changes.some(c => c.changeType === 'deed_change')).toBe(true); - }); - - it('respects configurable ownership threshold', () => { - const config: DetectionConfig = { - ownershipNameThreshold: 0.5, - deedDateSensitivity: 0.9, - taxAmountChangePercent: 15, - }; - const current = { - ...baselineSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - ownerName: 'Jon Doe', - }; - const result = detectChanges(baselineSnapshot, current, config); - expect(result.changes.some(c => c.field === 'ownerName')).toBe(true); - }); - - it('populates previous and current snapshots in result', () => { - const current = { - ...baselineSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - ownerName: 'Jane Smith', - }; - const result = detectChanges(baselineSnapshot, current); - expect(result.previousSnapshot).toBe(baselineSnapshot); - expect(result.currentSnapshot).toBe(current); - }); - - it('includes detectedAt timestamp', () => { - const current = { - ...baselineSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - ownerName: 'Jane Smith', - }; - const result = detectChanges(baselineSnapshot, current); - expect(result.detectedAt).toBeDefined(); - expect(new Date(result.detectedAt).getTime()).toBeGreaterThan(0); - }); -}); - -describe('shouldTriggerAlert', () => { - it('triggers for critical severity above default threshold', () => { - const result = { - propertyId: 'prop-001', - changeType: 'ownership_transfer' as const, - severity: 'critical' as const, - confidence: 0.95, - changes: [], - previousSnapshot: baselineSnapshot, - currentSnapshot: baselineSnapshot, - detectedAt: new Date().toISOString(), - }; - expect(shouldTriggerAlert(result)).toBe(true); - }); - - it('triggers for warning severity with high confidence', () => { - const result = { - propertyId: 'prop-001', - changeType: 'deed_change' as const, - severity: 'warning' as const, - confidence: 0.85, - changes: [], - previousSnapshot: baselineSnapshot, - currentSnapshot: baselineSnapshot, - detectedAt: new Date().toISOString(), - }; - expect(shouldTriggerAlert(result)).toBe(true); - }); - - it('does not trigger for info severity with default threshold', () => { - const result = { - propertyId: 'prop-001', - changeType: 'tax_change' as const, - severity: 'info' as const, - confidence: 0.85, - changes: [], - previousSnapshot: baselineSnapshot, - currentSnapshot: baselineSnapshot, - detectedAt: new Date().toISOString(), - }; - expect(shouldTriggerAlert(result)).toBe(false); - }); - - it('does not trigger when confidence below 0.7', () => { - const result = { - propertyId: 'prop-001', - changeType: 'deed_change' as const, - severity: 'warning' as const, - confidence: 0.5, - changes: [], - previousSnapshot: baselineSnapshot, - currentSnapshot: baselineSnapshot, - detectedAt: new Date().toISOString(), - }; - expect(shouldTriggerAlert(result)).toBe(false); - }); - - it('triggers info when minSeverity set to info', () => { - const result = { - propertyId: 'prop-001', - changeType: 'tax_change' as const, - severity: 'info' as const, - confidence: 0.85, - changes: [], - previousSnapshot: baselineSnapshot, - currentSnapshot: baselineSnapshot, - detectedAt: new Date().toISOString(), - }; - expect(shouldTriggerAlert(result, 'info')).toBe(true); - }); -}); - -describe('determineSeverity', () => { - it('returns major when ownership transfer present', () => { - const changes: PropertyChange[] = [ - { field: 'ownerName', oldValue: 'John', newValue: 'Jane', changeType: 'ownership_transfer' }, - ]; - expect(determineSeverity(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 })).toBe('critical'); - }); - - it('returns warning when only deed change', () => { - const changes: PropertyChange[] = [ - { field: 'deedDate', oldValue: '2020-01-01', newValue: '2026-01-01', changeType: 'deed_change' }, - ]; - expect(determineSeverity(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 })).toBe('warning'); - }); - - it('returns info when only metadata changes', () => { - const changes: PropertyChange[] = [ - { field: 'propertyType', oldValue: 'residential', newValue: 'commercial', changeType: 'metadata_change' }, - ]; - expect(determineSeverity(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 })).toBe('info'); - }); - - it('respects severity overrides', () => { - const changes: PropertyChange[] = [ - { field: 'taxAmount', oldValue: 1000, newValue: 2000, changeType: 'tax_change' }, - ]; - const config: DetectionConfig = { - ownershipNameThreshold: 0.7, - deedDateSensitivity: 0.9, - taxAmountChangePercent: 15, - severityOverrides: { tax_change: 'warning' }, - }; - expect(determineSeverity(changes, config)).toBe('warning'); - }); -}); - -describe('computeChangeConfidence', () => { - it('returns 0 for empty changes', () => { - expect(computeChangeConfidence([], { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 })).toBe(0); - }); - - it('returns high confidence for ownership transfer', () => { - const changes: PropertyChange[] = [ - { field: 'ownerName', oldValue: 'John', newValue: 'Jane', changeType: 'ownership_transfer' }, - ]; - const conf = computeChangeConfidence(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 }); - expect(conf).toBeCloseTo(0.95, 2); - }); - - it('returns high confidence for lien filing', () => { - const changes: PropertyChange[] = [ - { field: 'lienCount', oldValue: 0, newValue: 1, changeType: 'lien_filing' }, - ]; - const conf = computeChangeConfidence(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 }); - expect(conf).toBeCloseTo(0.9, 2); - }); - - it('averages confidence across multiple changes', () => { - const changes: PropertyChange[] = [ - { field: 'ownerName', oldValue: 'John', newValue: 'Jane', changeType: 'ownership_transfer' }, - { field: 'taxAmount', oldValue: 1000, newValue: 2000, changeType: 'tax_change' }, - ]; - const conf = computeChangeConfidence(changes, { ownershipNameThreshold: 0.7, deedDateSensitivity: 0.9, taxAmountChangePercent: 15 }); - expect(conf).toBeGreaterThan(0.7); - expect(conf).toBeLessThan(1.0); - }); -}); diff --git a/services/hometitle/test/integration.test.ts b/services/hometitle/test/integration.test.ts deleted file mode 100644 index 840e9b9..0000000 --- a/services/hometitle/test/integration.test.ts +++ /dev/null @@ -1,555 +0,0 @@ -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { - propertyWatchlistService, - normalizeAddressValue, - hashAddressValue, -} from '../src/watchlist.service'; -import { HomeTitleAlertPipeline } from '../src/alert.pipeline'; -import { detectChanges } from '../src/change-detector'; -import { PropertySnapshot } from '../src/types'; - -const mockedDb = vi.hoisted(() => { - const mocks = { - subscription: { - findUnique: vi.fn(), - count: vi.fn(), - }, - propertyWatchlistItem: { - count: vi.fn(), - findFirst: vi.fn(), - create: vi.fn(), - findMany: vi.fn(), - update: vi.fn(), - }, - alert: { - create: vi.fn(), - findFirst: vi.fn(), - }, - normalizedAlert: { - create: vi.fn(), - updateMany: vi.fn(), - }, - correlationGroup: { - create: vi.fn(), - }, - user: { - findUnique: vi.fn(), - }, - }; - return mocks; -}); - -vi.mock('@shieldai/db', () => ({ - prisma: { - subscription: mockedDb.subscription, - propertyWatchlistItem: mockedDb.propertyWatchlistItem, - alert: mockedDb.alert, - normalizedAlert: mockedDb.normalizedAlert, - correlationGroup: mockedDb.correlationGroup, - user: mockedDb.user, - }, - AlertSeverity: { INFO: 'INFO', WARNING: 'WARNING', CRITICAL: 'CRITICAL' }, - AlertChannel: { EMAIL: 'email', PUSH: 'push', SMS: 'sms' }, -})); - -vi.mock('@shieldai/shared-notifications', () => { - const mockSend = vi.fn().mockResolvedValue({ notificationId: 'mock-notif', status: 'sent' }); - class MockNS { - send = mockSend; - static getInstance() { return new MockNS(); } - } - return { - NotificationService: MockNS, - loadNotificationConfig: () => ({ apiKey: 'test', baseUrl: 'http://localhost' }), - }; -}); - -const PREMIUM_SUB = { id: 'sub-premium', tier: 'premium' as const }; -const PLUS_SUB = { id: 'sub-plus', tier: 'plus' as const }; -const BASIC_SUB = { id: 'sub-basic', tier: 'basic' as const }; - -function makeSnapshot(overrides: Partial = {}): PropertySnapshot { - return { - id: 'snap-1', - propertyId: 'prop-001', - capturedAt: '2026-01-01T00:00:00Z', - ownerName: 'John Doe', - address: { - streetNumber: '123', - streetName: 'main', - streetType: 'st', - city: 'springfield', - state: 'IL', - zip: '62701', - }, - propertyType: 'residential', - ...overrides, - }; -} - -describe('PropertyWatchlistService', () => { - beforeEach(() => { - vi.useFakeTimers(); - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - describe('addItem', () => { - it('creates a new watchlist item', async () => { - mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB); - mockedDb.propertyWatchlistItem.count.mockResolvedValue(0); - mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(null); - mockedDb.propertyWatchlistItem.create.mockResolvedValue({ - id: 'pw-1', - subscriptionId: 'sub-premium', - address: '123 main st', - parcelId: null, - ownerName: null, - streetAddress: '123 main st', - city: '', - state: '', - zipCode: '', - latitude: null, - longitude: null, - isActive: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const item = await propertyWatchlistService.addItem( - 'sub-premium', - '123 Main St', - 'parcel-001', - 'John Doe', - ); - - expect(item.address).toBe('123 main st'); - expect(mockedDb.propertyWatchlistItem.create).toHaveBeenCalled(); - }); - - it('enforces BASIC tier limit of 3', async () => { - mockedDb.subscription.findUnique.mockResolvedValue(BASIC_SUB); - mockedDb.propertyWatchlistItem.count.mockResolvedValue(3); - - await expect( - propertyWatchlistService.addItem('sub-basic', '456 Oak Ave', 'parcel-002') - ).rejects.toThrow(/limit reached/); - }); - - it('enforces PLUS tier limit of 5', async () => { - mockedDb.subscription.findUnique.mockResolvedValue(PLUS_SUB); - mockedDb.propertyWatchlistItem.count.mockResolvedValue(5); - - await expect( - propertyWatchlistService.addItem('sub-plus', '789 Elm Blvd', 'parcel-003') - ).rejects.toThrow(/limit reached/); - }); - - it('allows up to 50 for PREMIUM tier', async () => { - mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB); - mockedDb.propertyWatchlistItem.count.mockResolvedValue(49); - - mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(null); - mockedDb.propertyWatchlistItem.create.mockResolvedValue({ - id: 'pw-50', - subscriptionId: 'sub-premium', - address: '50th property', - parcelId: 'parcel-050', - ownerName: null, - streetAddress: '50th property', - city: '', - state: '', - zipCode: '', - latitude: null, - longitude: null, - isActive: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const item = await propertyWatchlistService.addItem( - 'sub-premium', - '50th Property', - 'parcel-050', - ); - - expect(item).toBeDefined(); - expect(item.address).toBe('50th property'); - }); - - it('deduplicates by normalized address', async () => { - const existingItem = { - id: 'pw-existing', - subscriptionId: 'sub-premium', - address: '123 main st', - isActive: true, - }; - mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB); - mockedDb.propertyWatchlistItem.count.mockResolvedValue(1); - mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(existingItem); - - const result = await propertyWatchlistService.addItem( - 'sub-premium', - '123 Main St', - 'parcel-001', - ); - - expect(result.id).toBe('pw-existing'); - expect(mockedDb.propertyWatchlistItem.create).not.toHaveBeenCalled(); - }); - - it('reactivates a deactivated item', async () => { - const deactivatedItem = { - id: 'pw-deactivated', - subscriptionId: 'sub-premium', - address: '123 main st', - isActive: false, - }; - mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB); - mockedDb.propertyWatchlistItem.count.mockResolvedValue(1); - mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(deactivatedItem); - mockedDb.propertyWatchlistItem.update.mockResolvedValue({ - ...deactivatedItem, - isActive: true, - }); - - const result = await propertyWatchlistService.addItem( - 'sub-premium', - '123 Main St', - ); - - expect(result.isActive).toBe(true); - expect(mockedDb.propertyWatchlistItem.update).toHaveBeenCalled(); - }); - - it('throws on invalid subscription', async () => { - mockedDb.subscription.findUnique.mockResolvedValue(null); - - await expect( - propertyWatchlistService.addItem('sub-invalid', '123 Main St') - ).rejects.toThrow(/not found/); - }); - }); - - describe('getItems', () => { - it('returns active items for subscription', async () => { - const items = [ - { - id: 'pw-1', - subscriptionId: 'sub-premium', - address: '123 main st', - isActive: true, - createdAt: new Date('2026-01-01'), - updatedAt: new Date('2026-01-01'), - }, - { - id: 'pw-2', - subscriptionId: 'sub-premium', - address: '456 oak ave', - isActive: true, - createdAt: new Date('2026-02-01'), - updatedAt: new Date('2026-02-01'), - }, - ]; - mockedDb.propertyWatchlistItem.findMany.mockResolvedValue(items); - - const result = await propertyWatchlistService.getItems('sub-premium'); - - expect(result).toHaveLength(2); - expect(mockedDb.propertyWatchlistItem.findMany).toHaveBeenCalledWith({ - where: { subscriptionId: 'sub-premium', isActive: true }, - orderBy: { createdAt: 'desc' }, - }); - }); - }); - - describe('removeItem', () => { - it('deactivates an item', async () => { - mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue({ - id: 'pw-1', - subscriptionId: 'sub-premium', - address: '123 main st', - isActive: true, - }); - mockedDb.propertyWatchlistItem.update.mockResolvedValue({ - id: 'pw-1', - subscriptionId: 'sub-premium', - address: '123 main st', - isActive: false, - }); - - const result = await propertyWatchlistService.removeItem('pw-1', 'sub-premium'); - - expect(result.isActive).toBe(false); - }); - - it('throws on missing item', async () => { - mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(null); - - await expect( - propertyWatchlistService.removeItem('pw-missing', 'sub-premium') - ).rejects.toThrow(/not found/); - }); - }); - - describe('getActiveItemsForScan', () => { - it('returns items with latest snapshot', async () => { - mockedDb.propertyWatchlistItem.findMany.mockResolvedValue([ - { - id: 'pw-1', - subscriptionId: 'sub-premium', - address: '123 main st', - isActive: true, - snapshots: [{ id: 'snap-1', capturedAt: new Date('2026-01-01') }], - }, - ]); - - const result = await propertyWatchlistService.getActiveItemsForScan('sub-premium'); - - expect(result).toHaveLength(1); - expect(result[0].snapshots).toHaveLength(1); - }); - }); - - describe('max items for tier', () => { - it('returns correct limits per tier', async () => { - mockedDb.subscription.findUnique.mockResolvedValue(BASIC_SUB); - expect(await propertyWatchlistService.getMaxItemsForTier('sub-basic')).toBe(3); - - mockedDb.subscription.findUnique.mockResolvedValue(PLUS_SUB); - expect(await propertyWatchlistService.getMaxItemsForTier('sub-plus')).toBe(5); - - mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB); - expect(await propertyWatchlistService.getMaxItemsForTier('sub-premium')).toBe(50); - }); - - it('returns 3 for unknown subscription', async () => { - mockedDb.subscription.findUnique.mockResolvedValue(null); - expect(await propertyWatchlistService.getMaxItemsForTier('sub-unknown')).toBe(3); - }); - }); - - describe('normalizeAddressValue', () => { - it('lowercases and trims', () => { - expect(normalizeAddressValue(' 123 Main St ')).toBe('123 main st'); - }); - - it('collapses multiple spaces', () => { - expect(normalizeAddressValue('123 Main St')).toBe('123 main st'); - }); - }); - - describe('hashAddressValue', () => { - it('produces consistent sha256 hash', () => { - const hash1 = hashAddressValue('123 main st'); - const hash2 = hashAddressValue('123 main st'); - expect(hash1).toBe(hash2); - expect(hash1).toHaveLength(64); - }); - - it('different addresses produce different hashes', () => { - const hash1 = hashAddressValue('123 main st'); - const hash2 = hashAddressValue('456 oak ave'); - expect(hash1).not.toBe(hash2); - }); - }); -}); - -describe('Integration: Full Pipeline', () => { - beforeEach(() => { - vi.useFakeTimers(); - vi.clearAllMocks(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('happy path: add property -> detect change -> create alert', async () => { - // Setup: add property to watchlist - mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB); - mockedDb.propertyWatchlistItem.count.mockResolvedValue(0); - mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(null); - mockedDb.propertyWatchlistItem.create.mockResolvedValue({ - id: 'pw-1', - subscriptionId: 'sub-premium', - address: '123 main st', - parcelId: 'parcel-001', - ownerName: 'John Doe', - streetAddress: '123 main st', - city: '', - state: '', - zipCode: '', - isActive: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const item = await propertyWatchlistService.addItem( - 'sub-premium', - '123 Main St', - 'parcel-001', - 'John Doe', - ); - expect(item.address).toBe('123 main st'); - - // Detect change: ownership transfer - const previous: PropertySnapshot = makeSnapshot({ - ownerName: 'John Doe', - capturedAt: '2026-01-01T00:00:00Z', - }); - const current: PropertySnapshot = makeSnapshot({ - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - ownerName: 'Jane Smith', - }); - - const changeResult = detectChanges(previous, current); - expect(changeResult.changeType).toBe('ownership_transfer'); - expect(changeResult.severity).toBe('critical'); - expect(changeResult.confidence).toBeGreaterThan(0.9); - - // Pipeline processes the change - mockedDb.alert.findFirst.mockResolvedValue(null); - mockedDb.alert.create.mockResolvedValue({ - id: 'alert-001', - subscriptionId: 'sub-premium', - userId: 'user-001', - type: 'system_warning', - title: '[CRITICAL] Ownership Transfer detected', - message: 'Change detected', - severity: 'CRITICAL' as any, - channel: ['email', 'push', 'sms'], - createdAt: new Date('2026-05-14T12:00:00Z'), - }); - - const pipeline = new HomeTitleAlertPipeline(); - const alert = await pipeline.processChangeDetection( - changeResult, - 'sub-premium', - 'user-001', - ); - - expect(alert).toBeDefined(); - expect(alert?.changeType).toBe('ownership_transfer'); - expect(alert?.severity).toBe('critical'); - expect(mockedDb.alert.create).toHaveBeenCalled(); - expect(mockedDb.normalizedAlert.create).toHaveBeenCalled(); - }); - - it('tier gating: premium gets more watchlist items than plus', async () => { - // Plus tier: 5 items max - mockedDb.subscription.findUnique.mockResolvedValue(PLUS_SUB); - mockedDb.propertyWatchlistItem.count.mockResolvedValue(5); - - await expect( - propertyWatchlistService.addItem('sub-plus', '50th property') - ).rejects.toThrow(/limit reached/); - - // Premium tier: 50 items max - mockedDb.subscription.findUnique.mockResolvedValue(PREMIUM_SUB); - mockedDb.propertyWatchlistItem.count.mockResolvedValue(49); - mockedDb.propertyWatchlistItem.findFirst.mockResolvedValue(null); - mockedDb.propertyWatchlistItem.create.mockResolvedValue({ - id: 'pw-50', - subscriptionId: 'sub-premium', - address: '50th property', - parcelId: 'parcel-050', - ownerName: null, - streetAddress: '50th property', - city: '', - state: '', - zipCode: '', - isActive: true, - createdAt: new Date(), - updatedAt: new Date(), - }); - - const item = await propertyWatchlistService.addItem( - 'sub-premium', - '50th Property', - 'parcel-050', - ); - expect(item).toBeDefined(); - }); - - it('fuzzy matching: similar names are detected as matches', async () => { - const { matchRecords } = await import('../src/matcher.service'); - - const addr = { - streetNumber: '123', - streetName: 'main', - streetType: 'st', - city: 'springfield', - state: 'IL', - zip: '62701', - }; - - // Slight typo in name - const result = matchRecords( - 'John Doe', - addr, - 'Jhon Doe', - addr, - ); - - expect(result.isMatch).toBe(true); - expect(result.nameScore).toBeGreaterThan(0.7); - }); - - it('fuzzy matching: completely different names don\'t match', async () => { - const { matchRecords } = await import('../src/matcher.service'); - - const addr = { - streetNumber: '123', - streetName: 'main', - streetType: 'st', - city: 'springfield', - state: 'IL', - zip: '62701', - }; - - const result = matchRecords( - 'John Doe', - addr, - 'Robert Williams', - addr, - ); - - expect(result.isMatch).toBe(false); - expect(result.nameScore).toBeLessThan(0.5); - }); - - it('change detection: tax change is minor severity', async () => { - const previous: PropertySnapshot = makeSnapshot({ - taxAmount: 2500, - }); - const current: PropertySnapshot = makeSnapshot({ - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - taxAmount: 3500, - }); - - const result = detectChanges(previous, current); - expect(result.changes.some(c => c.changeType === 'tax_change')).toBe(true); - expect(result.severity).toBe('info'); - }); - - it('change detection: lien filing is moderate severity', async () => { - const previous: PropertySnapshot = makeSnapshot({ - lienCount: 0, - }); - const current: PropertySnapshot = makeSnapshot({ - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - lienCount: 2, - }); - - const result = detectChanges(previous, current); - expect(result.changes.some(c => c.changeType === 'lien_filing')).toBe(true); - expect(result.severity).toBe('warning'); - }); -}); diff --git a/services/hometitle/test/matcher.test.ts b/services/hometitle/test/matcher.test.ts deleted file mode 100644 index 0ec93d8..0000000 --- a/services/hometitle/test/matcher.test.ts +++ /dev/null @@ -1,272 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - matchRecords, - parseName, - normalizeString, - normalizeStreetType, - levenshteinDistance, - similarityScore, - getConfigForPropertyType, -} from '../src/matcher.service'; -import { Address } from '../src/types'; - -const baselineAddress: Address = { - streetNumber: '123', - streetName: 'main', - streetType: 'st', - unit: 'apt 4b', - city: 'springfield', - state: 'IL', - zip: '62701', - latitude: 39.7817, - longitude: -89.6501, -}; - -describe('levenshteinDistance', () => { - it('returns 0 for identical strings', () => { - expect(levenshteinDistance('hello', 'hello')).toBe(0); - }); - - it('computes distance for different strings', () => { - expect(levenshteinDistance('kitten', 'sitting')).toBe(3); - }); - - it('handles empty strings', () => { - expect(levenshteinDistance('', 'hello')).toBe(5); - expect(levenshteinDistance('hello', '')).toBe(5); - }); - - it('handles single character differences', () => { - expect(levenshteinDistance('cat', 'bat')).toBe(1); - }); -}); - -describe('similarityScore', () => { - it('returns 1.0 for zero distance', () => { - expect(similarityScore(0, 5)).toBe(1.0); - }); - - it('returns 0.0 when distance equals max length', () => { - expect(similarityScore(5, 5)).toBe(0.0); - }); - - it('returns 1.0 for empty strings', () => { - expect(similarityScore(0, 0)).toBe(1.0); - }); -}); - -describe('normalizeString', () => { - it('lowercases and trims', () => { - expect(normalizeString(' Hello World ')).toBe('hello world'); - }); - - it('removes special characters', () => { - expect(normalizeString('O\'Brien-Jr!')).toBe('obrien jr'); - }); - - it('collapses multiple spaces', () => { - expect(normalizeString('John Doe')).toBe('john doe'); - }); -}); - -describe('parseName', () => { - it('parses first and last name', () => { - const tokens = parseName('John Doe'); - expect(tokens.firstName).toBe('john'); - expect(tokens.lastName).toBe('doe'); - expect(tokens.middleName).toBe(''); - }); - - it('parses name with middle name', () => { - const tokens = parseName('John Robert Doe'); - expect(tokens.firstName).toBe('john'); - expect(tokens.lastName).toBe('doe'); - expect(tokens.middleName).toBe('robert'); - }); - - it('strips prefixes', () => { - const tokens = parseName('Dr. John Doe'); - expect(tokens.firstName).toBe('john'); - expect(tokens.lastName).toBe('doe'); - }); - - it('strips suffixes', () => { - const tokens = parseName('John Doe Jr'); - expect(tokens.firstName).toBe('john'); - expect(tokens.lastName).toBe('doe'); - }); - - it('handles single name', () => { - const tokens = parseName('Madonna'); - expect(tokens.lastName).toBe('madonna'); - expect(tokens.firstName).toBe(''); - }); - - it('extracts initials from middle names', () => { - const tokens = parseName('John M Doe'); - expect(tokens.initials).toContain('m'); - }); - - it('handles empty name', () => { - const tokens = parseName(''); - expect(tokens.firstName).toBe(''); - expect(tokens.lastName).toBe(''); - expect(tokens.middleName).toBe(''); - }); -}); - -describe('normalizeStreetType', () => { - it('expands abbreviations', () => { - expect(normalizeStreetType('st')).toBe('street'); - expect(normalizeStreetType('ave')).toBe('avenue'); - expect(normalizeStreetType('blvd')).toBe('boulevard'); - expect(normalizeStreetType('ct')).toBe('court'); - expect(normalizeStreetType('ln')).toBe('lane'); - expect(normalizeStreetType('dr')).toBe('drive'); - }); - - it('normalizes full names', () => { - expect(normalizeStreetType('Street')).toBe('street'); - expect(normalizeStreetType('Avenue')).toBe('avenue'); - }); - - it('passes through unknown types', () => { - expect(normalizeStreetType('way')).toBe('way'); - }); -}); - -describe('matchRecords', () => { - it('matches identical records with high confidence', () => { - const result = matchRecords( - 'John Doe', - { ...baselineAddress }, - 'John Doe', - { ...baselineAddress }, - ); - expect(result.nameScore).toBeCloseTo(1.0, 2); - expect(result.addressScore).toBeGreaterThan(0.95); - expect(result.isMatch).toBe(true); - }); - - it('matches names with different prefixes', () => { - const result = matchRecords( - 'Dr. John Doe', - { ...baselineAddress }, - 'John Doe', - { ...baselineAddress }, - ); - expect(result.nameScore).toBeGreaterThan(0.8); - expect(result.isMatch).toBe(true); - }); - - it('matches names with different suffixes', () => { - const result = matchRecords( - 'John Doe Jr', - { ...baselineAddress }, - 'John Doe', - { ...baselineAddress }, - ); - expect(result.nameScore).toBeGreaterThan(0.8); - }); - - it('matches names with typos via Levenshtein', () => { - const result = matchRecords( - 'Jhon Doe', - { ...baselineAddress }, - 'John Doe', - { ...baselineAddress }, - ); - expect(result.nameScore).toBeGreaterThan(0.7); - expect(result.details.levenshteinDistance).toBeGreaterThan(0); - }); - - it('handles middle initial matching', () => { - const result = matchRecords( - 'John M Doe', - { ...baselineAddress }, - 'John Michael Doe', - { ...baselineAddress }, - ); - expect(result.nameScore).toBeGreaterThan(0.7); - }); - - it('matches addresses with different street type formats', () => { - const addrA: Address = { ...baselineAddress, streetType: 'st' }; - const addrB: Address = { ...baselineAddress, streetType: 'street' }; - const result = matchRecords('John Doe', addrA, 'John Doe', addrB); - expect(result.addressScore).toBeGreaterThan(0.9); - }); - - it('uses geocoding proximity when coordinates available', () => { - const addrA: Address = { - ...baselineAddress, - latitude: 39.7817, - longitude: -89.6501, - }; - const addrB: Address = { - ...baselineAddress, - latitude: 39.782, - longitude: -89.6505, - }; - const result = matchRecords('John Doe', addrA, 'John Doe', addrB); - expect(result.details.geocodingDistance).toBeDefined(); - expect(result.details.geocodingDistance!).toBeLessThan(100); - }); - - it('returns false for completely different records', () => { - const result = matchRecords( - 'John Doe', - baselineAddress, - 'Jane Smith', - { - streetNumber: '999', - streetName: 'oak', - streetType: 'ave', - city: 'chicago', - state: 'IL', - zip: '60601', - }, - ); - expect(result.isMatch).toBe(false); - }); - - it('provides detailed field-level match info', () => { - const result = matchRecords( - 'John Doe', - baselineAddress, - 'John Doe', - baselineAddress, - ); - expect(result.details.fields.firstName.score).toBe(1.0); - expect(result.details.fields.lastName.score).toBe(1.0); - expect(result.details.fields.streetNumber.score).toBe(1.0); - }); - - it('reports normalized address strings', () => { - const result = matchRecords( - 'John Doe', - baselineAddress, - 'John Doe', - baselineAddress, - ); - expect(result.details.addressNormalized[0]).toBe(result.details.addressNormalized[1]); - }); -}); - -describe('getConfigForPropertyType', () => { - it('returns residential config with higher thresholds', () => { - const config = getConfigForPropertyType('residential'); - expect(config.nameThreshold).toBe(0.85); - expect(config.addressThreshold).toBe(0.9); - }); - - it('returns commercial config with lower name threshold', () => { - const config = getConfigForPropertyType('commercial'); - expect(config.nameThreshold).toBe(0.8); - }); - - it('returns land config with lower address threshold', () => { - const config = getConfigForPropertyType('land'); - expect(config.addressThreshold).toBe(0.85); - }); -}); diff --git a/services/hometitle/test/scanner.test.ts b/services/hometitle/test/scanner.test.ts deleted file mode 100644 index e50978e..0000000 --- a/services/hometitle/test/scanner.test.ts +++ /dev/null @@ -1,648 +0,0 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { PropertyScannerService } from '../src/scanner.service'; -import { prisma } from '@shieldai/db'; - -vi.mock('@shieldai/db', () => ({ - prisma: { - countyDeedRecord: { - findMany: vi.fn(), - }, - }, -})); - -describe('PropertyScannerService', () => { - let scanner: PropertyScannerService; - - beforeEach(() => { - vi.clearAllMocks(); - scanner = new PropertyScannerService({ - attomApiKey: 'test-api-key', - uspsApiKey: 'usps-test-key', - countyScraperEnabled: true, - cacheTTL: 60, - rateLimitPoints: 100, - rateLimitDuration: 60, - retryAttempts: 2, - retryDelayMs: 100, - }); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('fetchByAddress', () => { - it('should fetch property data from ATTOM API by address', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-123', - parcelNumber: '123-456-789', - address: { - streetAddress: '123 Main St', - city: 'San Francisco', - state: 'CA', - zipCode: '94102', - latitude: 37.7749, - longitude: -122.4194, - }, - owner: { - ownerName1: 'John Doe', - ownerName2: 'Jane Doe', - }, - assessment: { - totalValue: 1000000, - landValue: 400000, - improvementValue: 600000, - taxYear: 2024, - taxAmount: 12000, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 1990, - squareFeet: 2500, - bedrooms: 4, - bathrooms: 3, - }, - salesHistory: [], - }, - }), - }); - - vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValueOnce([]); - - const result = await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102'); - - expect(result).not.toBeNull(); - expect(result?.parcelNumber).toBe('123-456-789'); - expect(result?.ownerName).toBe('John Doe'); - expect(result?.address.city).toBe('San Francisco'); - expect(result?.dataSource).toBe('attom'); - expect(result?.assessment?.totalValue).toBe(1000000); - }); - - it('should return null when property not found', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: false, - status: 404, - statusText: 'Not Found', - }); - - const result = await scanner.fetchByAddress('999 Nonexistent St'); - - expect(result).toBeNull(); - }); - - it('should combine ATTOM data with county deed records', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-123', - parcelNumber: '123-456-789', - address: { - streetAddress: '123 Main St', - city: 'San Francisco', - state: 'CA', - zipCode: '94102', - latitude: 37.7749, - longitude: -122.4194, - }, - owner: { - ownerName1: 'John Doe', - }, - assessment: { - totalValue: 1000000, - landValue: 400000, - improvementValue: 600000, - taxYear: 2024, - taxAmount: 12000, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 1990, - squareFeet: 2500, - }, - salesHistory: [], - }, - }), - }); - - vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValueOnce([ - { - id: 'deed-1', - documentId: 'DOC-2024-001', - recordingDate: new Date('2024-01-15'), - documentType: 'WARRANTY DEED', - grantorName: 'Previous Owner LLC', - granteeName: 'John Doe', - propertyAddress: '123 Main St', - parcelNumber: '123-456-789', - considerationAmount: 950000, - }, - ]); - - const result = await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102', 'San Francisco'); - - expect(result).not.toBeNull(); - expect(result?.dataSource).toBe('combined'); - expect(result?.deedHistory?.length).toBe(1); - expect(result?.deedHistory?.[0].documentType).toBe('WARRANTY DEED'); - }); - - it('should handle ATTOM API errors with retry', async () => { - global.fetch = vi.fn() - .mockRejectedValueOnce(new Error('Network error')) - .mockRejectedValueOnce(new Error('Network error')) - .mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-123', - parcelNumber: '123-456-789', - address: { - streetAddress: '123 Main St', - city: 'San Francisco', - state: 'CA', - zipCode: '94102', - latitude: 37.7749, - longitude: -122.4194, - }, - owner: { - ownerName1: 'John Doe', - }, - assessment: { - totalValue: 1000000, - landValue: 400000, - improvementValue: 600000, - taxYear: 2024, - taxAmount: 12000, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 1990, - squareFeet: 2500, - }, - salesHistory: [], - }, - }), - }); - - vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValueOnce([]); - - const result = await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102'); - - expect(result).not.toBeNull(); - expect(result?.ownerName).toBe('John Doe'); - expect(global.fetch).toHaveBeenCalledTimes(3); - }); - }); - - describe('fetchByParcel', () => { - it('should fetch property data by parcel number', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-456', - parcelNumber: '987-654-321', - address: { - streetAddress: '456 Oak Ave', - city: 'Los Angeles', - state: 'CA', - zipCode: '90001', - latitude: 34.0522, - longitude: -118.2437, - }, - owner: { - ownerName1: 'Alice Smith', - }, - assessment: { - totalValue: 750000, - landValue: 300000, - improvementValue: 450000, - taxYear: 2024, - taxAmount: 9000, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 2000, - squareFeet: 1800, - }, - salesHistory: [], - }, - }), - }); - - vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValueOnce([]); - - const result = await scanner.fetchByParcel('987-654-321', 'Los Angeles'); - - expect(result).not.toBeNull(); - expect(result?.parcelNumber).toBe('987-654-321'); - expect(result?.ownerName).toBe('Alice Smith'); - expect(result?.address.city).toBe('Los Angeles'); - }); - - it('should fetch county liens for parcel', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-456', - parcelNumber: '987-654-321', - address: { - streetAddress: '456 Oak Ave', - city: 'Los Angeles', - state: 'CA', - zipCode: '90001', - latitude: 34.0522, - longitude: -118.2437, - }, - owner: { - ownerName1: 'Alice Smith', - }, - assessment: { - totalValue: 750000, - landValue: 300000, - improvementValue: 450000, - taxYear: 2024, - taxAmount: 9000, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 2000, - squareFeet: 1800, - }, - salesHistory: [], - }, - }), - }); - - vi.mocked(prisma.countyDeedRecord.findMany) - .mockResolvedValueOnce([]) - .mockResolvedValueOnce([ - { - id: 'lien-1', - documentId: 'LIEN-2024-001', - recordingDate: new Date('2024-02-01'), - documentType: 'MECHANIC LIEN', - grantorName: 'ABC Construction', - granteeName: 'Alice Smith', - propertyAddress: '456 Oak Ave', - parcelNumber: '987-654-321', - considerationAmount: 15000, - }, - ]); - - const result = await scanner.fetchByParcel('987-654-321', 'Los Angeles'); - - expect(result).not.toBeNull(); - expect(result?.liens?.length).toBe(1); - expect(result?.liens?.[0].lienType).toBe('MECHANIC LIEN'); - expect(result?.liens?.[0].lienAmount).toBe(15000); - }); - }); - - describe('standardizeAddress', () => { - it('should standardize address using USPS API', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - streetNumber: '123', - streetName: 'Main', - streetSuffix: 'St', - unitType: 'Apt', - unitValue: '4B', - city: 'San Francisco', - state: 'CA', - zip5: '94102', - zip4: '1234', - deliveryPointBarCode: '941021234123', - isDeliverable: true, - }), - }); - - const result = await scanner.standardizeAddress('123 main street apt 4b san francisco ca 94102'); - - expect(result).not.toBeNull(); - expect(result?.streetNumber).toBe('123'); - expect(result?.streetName).toBe('Main'); - expect(result?.city).toBe('San Francisco'); - expect(result?.isDeliverable).toBe(true); - }); - - it('should return null for undeliverable address', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - streetNumber: '999', - streetName: 'Nonexistent', - streetSuffix: 'St', - city: 'Nowhere', - state: 'XX', - zip5: '00000', - isDeliverable: false, - }), - }); - - const result = await scanner.standardizeAddress('999 nonexistent st nowhere xx 00000'); - - expect(result).not.toBeNull(); - expect(result?.isDeliverable).toBe(false); - }); - }); - - describe('batchFetchProperties', () => { - it('should fetch multiple properties in batch', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-1', - parcelNumber: '111-222-333', - address: { - streetAddress: '111 First St', - city: 'San Francisco', - state: 'CA', - zipCode: '94102', - latitude: 37.7749, - longitude: -122.4194, - }, - owner: { - ownerName1: 'Owner One', - }, - assessment: { - totalValue: 500000, - landValue: 200000, - improvementValue: 300000, - taxYear: 2024, - taxAmount: 6000, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 1985, - squareFeet: 1500, - }, - salesHistory: [], - }, - }), - }).mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-2', - parcelNumber: '444-555-666', - address: { - streetAddress: '222 Second Ave', - city: 'Los Angeles', - state: 'CA', - zipCode: '90001', - latitude: 34.0522, - longitude: -118.2437, - }, - owner: { - ownerName1: 'Owner Two', - }, - assessment: { - totalValue: 800000, - landValue: 350000, - improvementValue: 450000, - taxYear: 2024, - taxAmount: 9600, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 1995, - squareFeet: 2000, - }, - salesHistory: [], - }, - }), - }); - - vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValue([]); - - const requests = [ - { address: '111 First St, San Francisco, CA 94102' }, - { address: '222 Second Ave, Los Angeles, CA 90001' }, - ]; - - const results = await scanner.batchFetchProperties(requests); - - expect(results.length).toBe(2); - expect(results[0]).not.toBeNull(); - expect(results[1]).not.toBeNull(); - expect(results[0]?.ownerName).toBe('Owner One'); - expect(results[1]?.ownerName).toBe('Owner Two'); - }); - - it('should handle batch with mixed valid and invalid requests', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-1', - parcelNumber: '111-222-333', - address: { - streetAddress: '111 First St', - city: 'San Francisco', - state: 'CA', - zipCode: '94102', - latitude: 37.7749, - longitude: -122.4194, - }, - owner: { - ownerName1: 'Owner One', - }, - assessment: { - totalValue: 500000, - landValue: 200000, - improvementValue: 300000, - taxYear: 2024, - taxAmount: 6000, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 1985, - squareFeet: 1500, - }, - salesHistory: [], - }, - }), - }).mockResolvedValueOnce({ - ok: false, - status: 404, - }); - - vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValue([]); - - const requests = [ - { address: '111 First St, San Francisco, CA 94102' }, - { address: '999 Invalid Address' }, - ]; - - const results = await scanner.batchFetchProperties(requests); - - expect(results.length).toBe(2); - expect(results[0]).not.toBeNull(); - expect(results[1]).toBeNull(); - }); - }); - - describe('cache behavior', () => { - it('should use cached results for repeated requests', async () => { - global.fetch = vi.fn().mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-123', - parcelNumber: '123-456-789', - address: { - streetAddress: '123 Main St', - city: 'San Francisco', - state: 'CA', - zipCode: '94102', - latitude: 37.7749, - longitude: -122.4194, - }, - owner: { - ownerName1: 'John Doe', - }, - assessment: { - totalValue: 1000000, - landValue: 400000, - improvementValue: 600000, - taxYear: 2024, - taxAmount: 12000, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 1990, - squareFeet: 2500, - }, - salesHistory: [], - }, - }), - }); - - vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValueOnce([]); - - await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102'); - await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102'); - - expect(global.fetch).toHaveBeenCalledTimes(1); - }); - - it('should invalidate cache when requested', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-123', - parcelNumber: '123-456-789', - address: { - streetAddress: '123 Main St', - city: 'San Francisco', - state: 'CA', - zipCode: '94102', - latitude: 37.7749, - longitude: -122.4194, - }, - owner: { - ownerName1: 'John Doe', - }, - assessment: { - totalValue: 1000000, - landValue: 400000, - improvementValue: 600000, - taxYear: 2024, - taxAmount: 12000, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 1990, - squareFeet: 2500, - }, - salesHistory: [], - }, - }), - }); - - vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValue([]); - - await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102'); - await scanner.invalidateCache('123 Main St, San Francisco, CA 94102'); - await scanner.fetchByAddress('123 Main St, San Francisco, CA 94102'); - - expect(global.fetch).toHaveBeenCalledTimes(2); - }); - }); - - describe('rate limiting', () => { - it('should handle rate limit errors gracefully', async () => { - global.fetch = vi.fn().mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ - data: { - propertyId: 'prop-123', - parcelNumber: '123-456-789', - address: { - streetAddress: '123 Main St', - city: 'San Francisco', - state: 'CA', - zipCode: '94102', - latitude: 37.7749, - longitude: -122.4194, - }, - owner: { - ownerName1: 'John Doe', - }, - assessment: { - totalValue: 1000000, - landValue: 400000, - improvementValue: 600000, - taxYear: 2024, - taxAmount: 12000, - }, - propertyDetails: { - propertyType: 'residential', - yearBuilt: 1990, - squareFeet: 2500, - }, - salesHistory: [], - }, - }), - }); - - vi.mocked(prisma.countyDeedRecord.findMany).mockResolvedValue([]); - - const scannerWithStrictLimit = new PropertyScannerService({ - attomApiKey: 'test-api-key', - countyScraperEnabled: false, - rateLimitPoints: 1, - rateLimitDuration: 60, - }); - - await scannerWithStrictLimit.fetchByAddress('123 Main St, San Francisco, CA 94102'); - - await expect( - scannerWithStrictLimit.fetchByAddress('456 Oak Ave, San Francisco, CA 94102') - ).rejects.toThrow('Rate limit exceeded'); - }); - }); -}); diff --git a/services/hometitle/test/scheduler.service.test.ts b/services/hometitle/test/scheduler.service.test.ts deleted file mode 100644 index 3a9a324..0000000 --- a/services/hometitle/test/scheduler.service.test.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; -import { HomeTitleSchedulerService } from '../src/scheduler.service'; -import { PropertySnapshot } from '../src/types'; - -// All mocks inside vi.hoisted() to avoid vitest hoisting issues -const mocked = vi.hoisted(() => { - const mockPrisma = { - subscription: { findMany: vi.fn() }, - $queryRaw: vi.fn(), - }; - const mockProcessChangeDetection = vi.fn(); - const mockDetectChanges = vi.fn(); - const mockShouldTriggerAlert = vi.fn(); - - return { - mockPrisma, - mockProcessChangeDetection, - mockDetectChanges, - mockShouldTriggerAlert, - }; -}); - -vi.mock('@shieldai/db', () => ({ - prisma: mocked.mockPrisma, -})); - -vi.mock('../src/alert.pipeline', () => ({ - homeTitleAlertPipeline: { - processChangeDetection: mocked.mockProcessChangeDetection, - }, - HomeTitleAlertPipeline: class {}, -})); - -vi.mock('../src/change-detector', () => ({ - detectChanges: mocked.mockDetectChanges, - shouldTriggerAlert: mocked.mockShouldTriggerAlert, -})); - -vi.mock('uuid', () => ({ - v4: () => 'scan-uuid-' + Date.now(), -})); - -const mockSubscription = { - id: 'sub-001', - userId: 'user-001', - tier: 'premium' as const, -}; - -function mockLatestSnapshots(snapshots: PropertySnapshot[]) { - mocked.mockPrisma.$queryRaw.mockResolvedValue( - snapshots.map(s => ({ - id: s.id, - propertyId: s.propertyId, - capturedAt: s.capturedAt, - ownerName: s.ownerName, - address: JSON.stringify(s.address), - deedDate: s.deedDate ?? null, - taxId: s.taxId ?? null, - propertyType: s.propertyType, - taxAmount: s.taxAmount ?? null, - lienCount: s.lienCount ?? null, - })) - ); -} - -function mockPreviousSnapshot(snapshot: PropertySnapshot | null) { - if (!snapshot) { - mocked.mockPrisma.$queryRaw.mockResolvedValue([]); - } else { - mocked.mockPrisma.$queryRaw.mockResolvedValue([ - { - id: snapshot.id, - propertyId: snapshot.propertyId, - capturedAt: snapshot.capturedAt, - ownerName: snapshot.ownerName, - address: JSON.stringify(snapshot.address), - deedDate: snapshot.deedDate ?? null, - taxId: snapshot.taxId ?? null, - propertyType: snapshot.propertyType, - taxAmount: snapshot.taxAmount ?? null, - lienCount: snapshot.lienCount ?? null, - }, - ]); - } -} - -describe('HomeTitleSchedulerService', () => { - let scheduler: HomeTitleSchedulerService; - - beforeEach(() => { - vi.useFakeTimers(); - vi.clearAllMocks(); - mocked.mockProcessChangeDetection.mockReset(); - mocked.mockDetectChanges.mockReset(); - mocked.mockShouldTriggerAlert.mockReset(); - - scheduler = new HomeTitleSchedulerService({ - scanIntervalMinutes: 60, - maxPropertiesPerScan: 100, - enabled: true, - }); - }); - - afterEach(() => { - scheduler.stop(); - vi.useRealTimers(); - }); - - describe('constructor and config', () => { - it('uses default config when none provided', () => { - const defaultScheduler = new HomeTitleSchedulerService(); - const config = defaultScheduler.getConfig(); - expect(config.scanIntervalMinutes).toBe(60); - expect(config.maxPropertiesPerScan).toBe(100); - expect(config.enabled).toBe(true); - defaultScheduler.stop(); - }); - - it('accepts custom config', () => { - scheduler = new HomeTitleSchedulerService({ scanIntervalMinutes: 30 }); - const config = scheduler.getConfig(); - expect(config.scanIntervalMinutes).toBe(30); - }); - - it('updates config dynamically', () => { - scheduler.updateConfig({ scanIntervalMinutes: 15 }); - const config = scheduler.getConfig(); - expect(config.scanIntervalMinutes).toBe(15); - }); - }); - - describe('start/stop', () => { - it('starts the scheduler', () => { - scheduler.start(); - expect(scheduler.isRunning()).toBe(true); - }); - - it('stops the scheduler', () => { - scheduler.start(); - scheduler.stop(); - expect(scheduler.isRunning()).toBe(false); - }); - - it('does not start when disabled', () => { - scheduler = new HomeTitleSchedulerService({ enabled: false }); - scheduler.start(); - expect(scheduler.isRunning()).toBe(false); - }); - }); - - describe('runScan', () => { - it('returns empty results when no subscriptions', async () => { - mocked.mockPrisma.subscription.findMany.mockResolvedValue([]); - - const result = await scheduler.runScan(); - - expect(result.propertiesScanned).toBe(0); - expect(result.changesDetected).toBe(0); - expect(result.alertsCreated).toBe(0); - expect(result.notificationsSent).toBe(0); - expect(result.errors).toEqual([]); - }); - - it('detects changes and creates alerts', async () => { - const previousSnapshot: PropertySnapshot = { - id: 'snap-1', - propertyId: 'prop-001', - capturedAt: '2026-01-01T00:00:00Z', - ownerName: 'John Doe', - address: { streetNumber: '123', streetName: 'main', city: 'springfield', state: 'IL', zip: '62701' }, - propertyType: 'residential', - }; - const currentSnapshot: PropertySnapshot = { - ...previousSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - ownerName: 'Jane Smith', - }; - - mocked.mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]); - mockLatestSnapshots([currentSnapshot]); - mockPreviousSnapshot(previousSnapshot); - mocked.mockDetectChanges.mockReturnValue({ - propertyId: 'prop-001', - changeType: 'ownership_transfer', - severity: 'major', - confidence: 0.95, - changes: [], - previousSnapshot, - currentSnapshot, - detectedAt: new Date().toISOString(), - }); - mocked.mockShouldTriggerAlert.mockReturnValue(true); - mocked.mockProcessChangeDetection.mockResolvedValue({ - id: 'alert-001', - propertyId: 'prop-001', - subscriptionId: 'sub-001', - userId: 'user-001', - changeType: 'ownership_transfer', - severity: 'critical', - title: '[MAJOR] Ownership Transfer detected', - message: 'Change detected', - changeDetectionResult: {} as any, - channel: ['email', 'push', 'sms'], - dedupKey: 'hometitle:user-001:prop-001:ownership_transfer', - createdAt: new Date().toISOString(), - }); - - const result = await scheduler.runScan(); - - expect(result.changesDetected).toBe(1); - expect(result.alertsCreated).toBe(1); - expect(result.notificationsSent).toBe(1); - }); - - it('skips snapshots without previous', async () => { - mocked.mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]); - mockLatestSnapshots([{ - id: 'snap-1', - propertyId: 'prop-001', - capturedAt: '2026-01-01T00:00:00Z', - ownerName: 'John Doe', - address: { streetNumber: '123', streetName: 'main', city: 'springfield', state: 'IL', zip: '62701' }, - propertyType: 'residential', - }]); - mockPreviousSnapshot(null); - - const result = await scheduler.runScan(); - - expect(result.changesDetected).toBe(0); - }); - - it('handles subscription scan errors gracefully', async () => { - mocked.mockPrisma.subscription.findMany.mockResolvedValue([mockSubscription]); - mockLatestSnapshots([]); - mockPreviousSnapshot(null); - mocked.mockDetectChanges.mockReturnValue({ - propertyId: 'prop-001', - changeType: 'metadata_change', - severity: 'minor', - confidence: 0.5, - changes: [], - previousSnapshot: {} as any, - currentSnapshot: {} as any, - detectedAt: new Date().toISOString(), - }); - mocked.mockShouldTriggerAlert.mockReturnValue(false); - - const result = await scheduler.runScan(); - - expect(result.errors).toEqual([]); - expect(result.propertiesScanned).toBe(0); - }); - - it('tracks scan metadata', async () => { - mocked.mockPrisma.subscription.findMany.mockResolvedValue([]); - - const result = await scheduler.runScan(); - - expect(result.scanId).toBeDefined(); - expect(result.startedAt).toBeDefined(); - expect(result.completedAt).toBeDefined(); - // completedAt should be after startedAt - expect(new Date(result.completedAt).getTime()).toBeGreaterThanOrEqual( - new Date(result.startedAt).getTime() - ); - }); - - it('does not send notifications for non-premium tier', async () => { - const previousSnapshot: PropertySnapshot = { - id: 'snap-1', - propertyId: 'prop-001', - capturedAt: '2026-01-01T00:00:00Z', - ownerName: 'John Doe', - address: { streetNumber: '123', streetName: 'main', city: 'springfield', state: 'IL', zip: '62701' }, - propertyType: 'residential', - }; - const currentSnapshot: PropertySnapshot = { - ...previousSnapshot, - id: 'snap-2', - capturedAt: '2026-02-01T00:00:00Z', - ownerName: 'Jane Smith', - }; - - const nonPremiumSub = { ...mockSubscription, tier: 'plus' as const }; - mocked.mockPrisma.subscription.findMany.mockResolvedValue([nonPremiumSub]); - mockLatestSnapshots([currentSnapshot]); - mockPreviousSnapshot(previousSnapshot); - mocked.mockDetectChanges.mockReturnValue({ - propertyId: 'prop-001', - changeType: 'ownership_transfer', - severity: 'major', - confidence: 0.95, - changes: [], - previousSnapshot, - currentSnapshot, - detectedAt: new Date().toISOString(), - }); - mocked.mockShouldTriggerAlert.mockReturnValue(true); - mocked.mockProcessChangeDetection.mockResolvedValue({ - id: 'alert-002', - propertyId: 'prop-001', - subscriptionId: 'sub-001', - userId: 'user-001', - changeType: 'ownership_transfer', - severity: 'critical', - title: '[MAJOR] Ownership Transfer detected', - message: 'Change detected', - changeDetectionResult: {} as any, - channel: ['email', 'push'], - dedupKey: 'hometitle:user-001:prop-001:ownership_transfer', - createdAt: new Date().toISOString(), - }); - - const result = await scheduler.runScan(); - - expect(result.changesDetected).toBe(1); - expect(result.alertsCreated).toBe(1); - expect(result.notificationsSent).toBe(0); - }); - }); - - describe('getLastScanResult', () => { - it('returns null before first scan', () => { - expect(scheduler.getLastScanResult()).toBeNull(); - }); - - it('returns last scan result after scan', async () => { - mocked.mockPrisma.subscription.findMany.mockResolvedValue([]); - scheduler.start(); - await vi.advanceTimersByTimeAsync(60 * 60 * 1000); - expect(scheduler.getLastScanResult()).not.toBeNull(); - }); - }); -}); diff --git a/services/hometitle/tsconfig.json b/services/hometitle/tsconfig.json deleted file mode 100644 index e6f3c02..0000000 --- a/services/hometitle/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "skipLibCheck": true, - "module": "ES2022", - "moduleResolution": "Bundler" - }, - "include": ["src/**/*.ts"] -} diff --git a/services/hometitle/vitest.config.ts b/services/hometitle/vitest.config.ts deleted file mode 100644 index fe01a0a..0000000 --- a/services/hometitle/vitest.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['src/**/*.test.ts', 'test/**/*.test.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - reportsDirectory: './coverage', - include: ['src/**/*.ts'], - exclude: [ - 'src/**/*.d.ts', - '**/node_modules/**', - '**/test/**', - ], - thresholds: { - statements: 80, - branches: 80, - functions: 80, - lines: 80, - }, - }, - }, -}); diff --git a/services/removebrokers/package.json b/services/removebrokers/package.json deleted file mode 100644 index 766d627..0000000 --- a/services/removebrokers/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "@shieldai/removebrokers", - "version": "0.1.0", - "main": "./dist/index.js", - "types": "./dist/index.js", - "scripts": { - "build": "tsc", - "test": "vitest run", - "test:coverage": "vitest run --coverage", - "lint": "eslint src/" - }, - "dependencies": { - "@shieldai/correlation": "workspace:*", - "@shieldai/db": "workspace:*", - "@shieldai/types": "workspace:*", - "@shieldai/shared-notifications": "workspace:*", - "node-cache": "^5.1.2" - }, - "devDependencies": { - "vitest": "^4.1.5", - "@vitest/coverage-v8": "^4.1.5" - }, - "exports": { - ".": "./src/index.ts" - } -} diff --git a/services/removebrokers/src/BrokerAlertPipeline.ts b/services/removebrokers/src/BrokerAlertPipeline.ts deleted file mode 100644 index dc2ec45..0000000 --- a/services/removebrokers/src/BrokerAlertPipeline.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { AlertSource, AlertCategory, Severity, EntityType, NormalizedAlertInput } from "@shieldai/types"; - -export interface BrokerAlertInput { - userId: string; - brokerName: string; - brokerId: string; - category: AlertCategory; - severity: Severity; - title: string; - description: string; - entities: Array<{ type: EntityType; value: string }>; - metadata?: Record; -} - -export class BrokerAlertPipeline { - async sendListingFoundAlert(input: BrokerAlertInput) { - const alert = { - source: AlertSource.INFO_BROKER, - category: AlertCategory.INFO_BROKER_LISTING, - severity: input.severity, - userId: input.userId, - title: input.title, - description: input.description, - entities: input.entities, - sourceAlertId: `broker_listing_${input.brokerId}_${Date.now()}`, - payload: { - brokerId: input.brokerId, - brokerName: input.brokerName, - ...input.metadata, - }, - timestamp: new Date(), - }; - - return this.normalizeAndSend(alert); - } - - async sendRemovalStatusAlert(input: BrokerAlertInput) { - const alert = { - source: AlertSource.INFO_BROKER, - category: AlertCategory.INFO_BROKER_REMOVAL, - severity: input.severity, - userId: input.userId, - title: input.title, - description: input.description, - entities: input.entities, - sourceAlertId: `broker_removal_${input.brokerId}_${Date.now()}`, - payload: { - brokerId: input.brokerId, - brokerName: input.brokerName, - ...input.metadata, - }, - timestamp: new Date(), - }; - - return this.normalizeAndSend(alert); - } - - private async normalizeAndSend(alert: NormalizedAlertInput) { - try { - const { correlationService } = await import("@shieldai/correlation"); - return correlationService.ingestGenericAlert(alert); - } catch { - console.error("[BrokerAlert] Failed to send alert:", alert.sourceAlertId); - return alert; - } - } -} - -export const brokerAlertPipeline = new BrokerAlertPipeline(); diff --git a/services/removebrokers/src/RemoveBrokersScheduler.ts b/services/removebrokers/src/RemoveBrokersScheduler.ts deleted file mode 100644 index 779dbc1..0000000 --- a/services/removebrokers/src/RemoveBrokersScheduler.ts +++ /dev/null @@ -1,145 +0,0 @@ -import prisma from "@shieldai/db"; -import { RemovalStatus } from "@shieldai/types"; -import { removeBrokersService } from "./RemoveBrokersService"; - -const DEFAULT_SCAN_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours -const VERIFICATION_DELAY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days after submission - -export class RemoveBrokersScheduler { - private intervalId: NodeJS.Timeout | null = null; - private intervalMs: number; - - constructor(intervalMs: number = DEFAULT_SCAN_INTERVAL_MS) { - this.intervalMs = intervalMs; - } - - isRunning(): boolean { - return this.intervalId !== null; - } - - start() { - if (this.isRunning()) { - return; - } - - this.intervalId = setInterval(async () => { - try { - await this.runCycle(); - } catch (error) { - console.error("[RemoveBrokers] Scheduler cycle error:", error); - } - }, this.intervalMs); - - console.log("[RemoveBrokers] Scheduler started"); - } - - stop() { - if (this.intervalId) { - clearInterval(this.intervalId); - this.intervalId = null; - console.log("[RemoveBrokers] Scheduler stopped"); - } - } - - async runCycle() { - console.log("[RemoveBrokers] Running scheduler cycle..."); - - const results = { - processed: 0, - verified: 0, - errors: 0, - }; - - try { - const processResults = await removeBrokersService.processPendingRequests(); - results.processed = processResults.length; - } catch (error) { - console.error("[RemoveBrokers] Process pending error:", error); - results.errors++; - } - - try { - const verifyResults = await this.verifyCompletedRemovals(); - results.verified = verifyResults.length; - } catch (error) { - console.error("[RemoveBrokers] Verify completions error:", error); - results.errors++; - } - - console.log( - `[RemoveBrokers] Cycle complete: ${JSON.stringify(results)}`, - ); - - return results; - } - - async runScan(subscriptionId: string) { - const subscription = await prisma.subscription.findUnique({ - where: { id: subscriptionId }, - }); - - if (!subscription) { - throw new Error("Subscription not found"); - } - - const user = await prisma.user.findUnique({ - where: { id: subscription.userId }, - select: { name: true, email: true }, - }); - - if (!user?.name) { - throw new Error("User name required for scanning"); - } - - const personalInfo = { - fullName: user.name, - email: user.email, - }; - - const results = await removeBrokersService.scanForListings( - subscriptionId, - personalInfo, - ); - - return { - subscriptionId, - brokersScanned: results.length, - listingsFound: results.filter((r) => r.found).length, - results, - }; - } - - private async verifyCompletedRemovals() { - const submitted = await prisma.removalRequest.findMany({ - where: { - status: RemovalStatus.SUBMITTED, - submittedAt: { - lte: new Date(Date.now() - VERIFICATION_DELAY_MS), - }, - }, - }); - - const results = []; - - for (const request of submitted) { - try { - const verification = await removeBrokersService.verifyRemoval( - request.id, - ); - results.push({ - requestId: request.id, - ...verification, - }); - } catch (error) { - console.error( - `[RemoveBrokers] Verification error for ${request.id}:`, - error, - ); - } - } - - return results; - } -} - -export const removeBrokersScheduler = new RemoveBrokersScheduler(); diff --git a/services/removebrokers/src/RemoveBrokersService.ts b/services/removebrokers/src/RemoveBrokersService.ts deleted file mode 100644 index 61ce990..0000000 --- a/services/removebrokers/src/RemoveBrokersService.ts +++ /dev/null @@ -1,509 +0,0 @@ -import prisma from "@shieldai/db"; -import { RemovalStatus, RemovalMethod } from "@shieldai/types"; -import { getBrokerById, getActiveBrokers } from "./brokerRegistry"; -import type { PersonalInfo, RemovalJob, BrokerEntry } from "./types"; -import { MAX_REMOVAL_ATTEMPTS, RETRY_DELAY_MS } from "./types"; -import type { RemovalRequest as PrismaRemovalRequest, InfoBroker } from "@shieldai/db"; - -function toPersonalInfo(raw: unknown): PersonalInfo | null { - if (typeof raw !== "object" || raw === null) return null; - const obj = raw as Record; - if (typeof obj.fullName !== "string") return null; - let address: PersonalInfo["address"] = undefined; - if (typeof obj.address === "object" && obj.address !== null) { - const addr = obj.address as Record; - address = { - street: typeof addr.street === "string" ? addr.street : undefined, - city: typeof addr.city === "string" ? addr.city : undefined, - state: typeof addr.state === "string" ? addr.state : undefined, - zip: typeof addr.zip === "string" ? addr.zip : undefined, - }; - } - return { - fullName: obj.fullName, - email: typeof obj.email === "string" ? obj.email : undefined, - phone: typeof obj.phone === "string" ? obj.phone : undefined, - address, - dob: typeof obj.dob === "string" ? obj.dob : undefined, - }; -} - -type RemovalRequestWithBroker = PrismaRemovalRequest & { - broker: InfoBroker; -}; - -export class RemoveBrokersService { - async scanForListings(subscriptionId: string, personalInfo: PersonalInfo) { - const brokers = getActiveBrokers(); - const results = []; - - for (const broker of brokers) { - const existingListing = await prisma.brokerListing.findFirst({ - where: { - subscriptionId, - brokerId: broker.id, - isRemoved: false, - }, - }); - - if (existingListing) { - results.push({ - brokerId: broker.id, - brokerName: broker.name, - found: true, - listingId: existingListing.id, - url: existingListing.url, - }); - continue; - } - - const found = await this.checkBrokerListing(broker, personalInfo); - if (found) { - const listing = await prisma.brokerListing.create({ - data: { - subscriptionId, - brokerId: broker.id, - url: found.url, - dataFound: found.dataFound, - isRemoved: false, - }, - }); - - results.push({ - brokerId: broker.id, - brokerName: broker.name, - found: true, - listingId: listing.id, - url: found.url, - }); - } else { - results.push({ - brokerId: broker.id, - brokerName: broker.name, - found: false, - }); - } - } - - return results; - } - - async createRemovalRequest( - subscriptionId: string, - brokerId: string, - personalInfo: PersonalInfo, - notes?: string, - ) { - const broker = getBrokerById(brokerId); - if (!broker) { - throw new Error(`Broker not found: ${brokerId}`); - } - - const existing = await prisma.removalRequest.findFirst({ - where: { - subscriptionId, - brokerId, - status: { in: [RemovalStatus.PENDING, RemovalStatus.SUBMITTED, RemovalStatus.IN_PROGRESS] }, - }, - }); - - if (existing) { - throw new Error(`Active removal request already exists for ${broker.name}`); - } - - const request = await prisma.removalRequest.create({ - data: { - subscriptionId, - brokerId, - status: RemovalStatus.PENDING, - personalInfo: JSON.parse(JSON.stringify(personalInfo)), - method: broker.removalMethod, - notes, - }, - }); - - return request; - } - - async submitRemoval(job: RemovalJob): Promise { - const broker = getBrokerById(job.brokerId); - if (!broker) { - throw new Error(`Broker not found: ${job.brokerId}`); - } - - switch (job.method) { - case RemovalMethod.AUTOMATED: - return await this.submitAutomatedRemoval(job, broker); - case RemovalMethod.MANUAL_FORM: - return await this.submitManualFormRemoval(job, broker); - case RemovalMethod.EMAIL: - return await this.submitEmailRemoval(job, broker); - default: - return false; - } - } - - async processPendingRequests() { - const pending = await prisma.removalRequest.findMany({ - where: { - status: RemovalStatus.PENDING, - OR: [ - { nextRetryAt: null }, - { nextRetryAt: { lte: new Date() } }, - ], - }, - }); - - const results = []; - - for (const request of pending) { - try { - await prisma.removalRequest.update({ - where: { id: request.id }, - data: { status: RemovalStatus.IN_PROGRESS }, - }); - - const job: RemovalJob = { - requestId: request.id, - brokerId: request.brokerId, - brokerName: getBrokerById(request.brokerId)?.name || request.brokerId, - personalInfo: toPersonalInfo(request.personalInfo)!, - method: request.method, - attempt: request.attempts + 1, - }; - - const success = await this.submitRemoval(job); - - if (success) { - await prisma.removalRequest.update({ - where: { id: request.id }, - data: { - status: RemovalStatus.SUBMITTED, - attempts: request.attempts + 1, - submittedAt: new Date(), - }, - }); - results.push({ requestId: request.id, status: "submitted" }); - } else if (request.attempts + 1 >= MAX_REMOVAL_ATTEMPTS) { - await prisma.removalRequest.update({ - where: { id: request.id }, - data: { - status: RemovalStatus.FAILED, - attempts: request.attempts + 1, - error: "Max attempts reached", - }, - }); - results.push({ requestId: request.id, status: "failed" }); - } else { - await prisma.removalRequest.update({ - where: { id: request.id }, - data: { - attempts: request.attempts + 1, - nextRetryAt: new Date(Date.now() + RETRY_DELAY_MS), - }, - }); - results.push({ requestId: request.id, status: "retry_scheduled" }); - } - } catch (error) { - const message = error instanceof Error ? error.message : "Unknown error"; - await prisma.removalRequest.update({ - where: { id: request.id }, - data: { - status: RemovalStatus.PENDING, - error: message, - nextRetryAt: new Date(Date.now() + RETRY_DELAY_MS), - }, - }); - results.push({ requestId: request.id, status: "error", error: message }); - } - } - - return results; - } - - async verifyRemoval(requestId: string) { - const request = await prisma.removalRequest.findUnique({ - where: { id: requestId }, - include: { broker: true }, - }); - - if (!request) { - throw new Error(`Removal request not found: ${requestId}`); - } - - const personalInfo = toPersonalInfo(request.personalInfo); - if (!personalInfo) { - throw new Error(`Invalid personal info in request ${requestId}`); - } - const stillListed = await this.checkBrokerListing( - { ...request.broker, removalUrl: request.broker.removalUrl || undefined }, - personalInfo, - ); - - if (!stillListed) { - await prisma.removalRequest.update({ - where: { id: requestId }, - data: { - status: RemovalStatus.COMPLETED, - completedAt: new Date(), - }, - }); - - await prisma.brokerListing.updateMany({ - where: { - removalRequestId: requestId, - isRemoved: false, - }, - data: { - isRemoved: true, - removedAt: new Date(), - }, - }); - - return { completed: true }; - } - - return { completed: false, stillListed: true }; - } - - async getRemovalStatus(subscriptionId: string) { - const requests = await prisma.removalRequest.findMany({ - where: { subscriptionId }, - include: { broker: true }, - orderBy: { updatedAt: "desc" }, - }); - - return requests.map((r: RemovalRequestWithBroker) => ({ - id: r.id, - brokerId: r.brokerId, - brokerName: r.broker.name, - status: r.status, - method: r.method, - attempts: r.attempts, - submittedAt: r.submittedAt, - completedAt: r.completedAt, - error: r.error, - createdAt: r.createdAt, - updatedAt: r.updatedAt, - })); - } - - async getAvailableBrokers(): Promise { - return getActiveBrokers(); - } - - // ---- Private methods ---- - - private async checkBrokerListing( - broker: BrokerEntry, - personalInfo: PersonalInfo, - ) { - const searchUrl = this.buildSearchUrl(broker, personalInfo); - try { - const response = await fetch(searchUrl, { - headers: { - "User-Agent": "ShieldAI-RemoveBrokers/1.0", - }, - signal: AbortSignal.timeout(10000), - }); - - if (!response.ok) { - return null; - } - - const html = await response.text(); - const listingUrl = this.extractListingUrl(html, searchUrl); - - if (!listingUrl) { - return null; - } - - const dataFound = this.extractPersonalData(html, personalInfo); - - return { - url: listingUrl, - dataFound, - }; - } catch { - return null; - } - } - - private buildSearchUrl(broker: BrokerEntry, info: PersonalInfo): string { - const nameParts = info.fullName.split(" "); - const firstName = nameParts[0] || ""; - const lastName = nameParts.slice(1).join(" ") || ""; - - const urlMap: Record string> = { - whitepages: (f, l) => - `https://www.whitepages.com/people/${f.toLowerCase()}-${l.toLowerCase()}`, - spokeo: (f, l) => - `https://www.spokeo.com/search?q=${encodeURIComponent(info.fullName)}`, - truepeoplesearch: (f, l) => - `https://www.truepeoplesearch.com/name/${encodeURIComponent(info.fullName)}`, - peoplefinders: (f, l) => - `https://www.peoplefinders.com/results?name=${encodeURIComponent(info.fullName)}`, - thatsmth: (f, l) => - `https://thatsmth.com/name/${encodeURIComponent(info.fullName)}`, - fastpeoplesearch: (f, l) => - `https://www.fastpeoplesearch.com/name/${encodeURIComponent(info.fullName)}`, - }; - - const builder = urlMap[broker.id]; - if (builder) { - return builder(firstName, lastName, info.address?.state || ""); - } - - return `https://${broker.domain}/search?q=${encodeURIComponent(info.fullName)}`; - } - - private extractListingUrl(html: string, searchUrl: string): string | null { - const profilePatterns = [ - /href="([^"]*\/people\/[^"]+)"/, - /href="([^"]*\/profile\/[^"]+)"/, - /href="([^"]*\/results\/[^"]+)"/, - ]; - - for (const pattern of profilePatterns) { - const match = html.match(pattern); - if (match) { - let url = match[1]; - if (url.startsWith("/")) { - const urlObj = new URL(searchUrl); - url = `${urlObj.protocol}//${urlObj.host}${url}`; - } - return url; - } - } - - return null; - } - - private extractPersonalData(html: string, _info: PersonalInfo): Record { - const data: Record = {}; - - const phonePattern = /\b(\d{3}[-.]?\d{3}[-.]?\d{4})\b/; - const phoneMatch = html.match(phonePattern); - if (phoneMatch) { - data.phoneNumber = phoneMatch[1]; - } - - const addressPattern = /(\d+\s+[A-Za-z\s]+,\s*[A-Z]{2}\s*\d{5})/; - const addressMatch = html.match(addressPattern); - if (addressMatch) { - data.address = addressMatch[1]; - } - - const emailPattern = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/; - const emailMatch = html.match(emailPattern); - if (emailMatch) { - data.email = emailMatch[1]; - } - - return data; - } - - private async submitAutomatedRemoval(job: RemovalJob, broker: BrokerEntry): Promise { - if (!broker.removalUrl) { - return false; - } - - try { - const response = await fetch(broker.removalUrl, { - method: "POST", - headers: { - "Content-Type": "application/x-www-form-urlencoded", - "User-Agent": "ShieldAI-RemoveBrokers/1.0", - }, - body: new URLSearchParams({ - full_name: job.personalInfo.fullName, - email: job.personalInfo.email || "", - phone: job.personalInfo.phone || "", - address: job.personalInfo.address - ? `${job.personalInfo.address.street || ""}, ${job.personalInfo.address.city || ""}, ${job.personalInfo.address.state || ""} ${job.personalInfo.address.zip || ""}`.trim() - : "", - dob: job.personalInfo.dob || "", - }), - signal: AbortSignal.timeout(30000), - }); - - return response.ok || response.status === 302; - } catch { - return false; - } - } - - private async submitManualFormRemoval(job: RemovalJob, broker: BrokerEntry): Promise { - if (!broker.removalUrl) { - return false; - } - - try { - const response = await fetch(broker.removalUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - "User-Agent": "ShieldAI-RemoveBrokers/1.0", - }, - body: JSON.stringify({ - name: job.personalInfo.fullName, - email: job.personalInfo.email, - phone: job.personalInfo.phone, - address: job.personalInfo.address, - reason: "privacy_removal", - }), - signal: AbortSignal.timeout(30000), - }); - - return response.ok || response.status === 302; - } catch { - return false; - } - } - - private async submitEmailRemoval(job: RemovalJob, broker: BrokerEntry): Promise { - const emailBody = this.generateEmailRemovalRequest(job, broker); - - try { - const emailServiceUrl = process.env.REMOVAL_EMAIL_SERVICE_URL; - if (!emailServiceUrl) { - return false; - } - - const response = await fetch(emailServiceUrl, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - to: `privacy@${broker.domain}`, - subject: `Data Removal Request - ${job.personalInfo.fullName}`, - body: emailBody, - }), - signal: AbortSignal.timeout(15000), - }); - - return response.ok; - } catch { - return false; - } - } - - private generateEmailRemovalRequest(job: RemovalJob, broker: BrokerEntry): string { - return `Dear ${broker.name} Privacy Team, - -I am requesting the removal of my personal information from your website (${broker.domain}). - -My Details: -- Full Name: ${job.personalInfo.fullName} -${job.personalInfo.email ? `- Email: ${job.personalInfo.email}` : ""} -${job.personalInfo.phone ? `- Phone: ${job.personalInfo.phone}` : ""} -${job.personalInfo.address ? `- Address: ${job.personalInfo.address.street || ""}, ${job.personalInfo.address.city || ""}, ${job.personalInfo.address.state || ""} ${job.personalInfo.address.zip || ""}`.trim() : ""} -${job.personalInfo.dob ? `- Date of Birth: ${job.personalInfo.dob}` : ""} - -I do not consent to the publication of my personal information on your site. Please process this removal request within 30 days as required by applicable privacy laws. - -Thank you, -${job.personalInfo.fullName}`; - } -} - -export const removeBrokersService = new RemoveBrokersService(); diff --git a/services/removebrokers/src/brokerRegistry.ts b/services/removebrokers/src/brokerRegistry.ts deleted file mode 100644 index cd45bed..0000000 --- a/services/removebrokers/src/brokerRegistry.ts +++ /dev/null @@ -1,261 +0,0 @@ -import { RemovalMethod, BrokerCategory } from "@shieldai/types"; -import type { BrokerEntry } from "./types"; - -export const BROKER_REGISTRY: BrokerEntry[] = [ - { - id: "whitepages", - name: "Whitepages", - domain: "whitepages.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://www.whitepages.com/optout", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 7, - isActive: true, - }, - { - id: "spokeo", - name: "Spokeo", - domain: "spokeo.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://www.spokeo.com/privacy/removal-request", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 14, - isActive: true, - }, - { - id: "truepeoplesearch", - name: "TruePeopleSearch", - domain: "truepeoplesearch.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.AUTOMATED, - removalUrl: "https://www.truepeoplesearch.com/remove-your-info", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 3, - isActive: true, - }, - { - id: "peoplefinders", - name: "PeopleFinders", - domain: "peoplefinders.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://www.peoplefinders.com/privacy-policy", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 14, - isActive: true, - }, - { - id: "thatsmth", - name: "That's Them", - domain: "thatsmth.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.AUTOMATED, - removalUrl: "https://thatsmth.com/opt-out", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 7, - isActive: true, - }, - { - id: "fastpeoplesearch", - name: "FastPeopleSearch", - domain: "fastpeoplesearch.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.AUTOMATED, - removalUrl: "https://www.fastpeoplesearch.com/opt-out", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 5, - isActive: true, - }, - { - id: "backgroundcheck", - name: "BackgroundCheck", - domain: "backgroundcheck.com", - category: BrokerCategory.BACKGROUND_CHECK, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://www.backgroundcheck.com/removal", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 14, - isActive: true, - }, - { - id: "freepeopledirectory", - name: "Free People Directory", - domain: "freepeopledirectory.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.AUTOMATED, - removalUrl: "https://freepeopledirectory.com/optout", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 7, - isActive: true, - }, - { - id: "radaris", - name: "Radaris", - domain: "radaris.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.EMAIL, - removalUrl: undefined, - requiresAccount: false, - requiresVerification: true, - estimatedDays: 30, - isActive: true, - }, - { - id: "zynda", - name: "Zynda", - domain: "zynda.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://zynda.com/opt-out", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 14, - isActive: true, - }, - { - id: "addressinator", - name: "Addressinator", - domain: "addressinator.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://addressinator.com/opt-out", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 14, - isActive: true, - }, - { - id: "familytreenow", - name: "FamilyTree Now", - domain: "familytreenow.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.EMAIL, - removalUrl: undefined, - requiresAccount: false, - requiresVerification: true, - estimatedDays: 30, - isActive: true, - }, - { - id: "accuratebackground", - name: "Accurate Background", - domain: "accuratebackground.com", - category: BrokerCategory.BACKGROUND_CHECK, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://www.accuratebackground.com/optout", - requiresAccount: true, - requiresVerification: true, - estimatedDays: 30, - isActive: true, - }, - { - id: "instantcheckmate", - name: "Instant Checkmate", - domain: "instantcheckmate.com", - category: BrokerCategory.BACKGROUND_CHECK, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://www.instantcheckmate.com/opt-out", - requiresAccount: true, - requiresVerification: true, - estimatedDays: 30, - isActive: true, - }, - { - id: "pthree", - name: "P3 (People Finders)", - domain: "pthree.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://www.pthree.com/opt-out", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 14, - isActive: true, - }, - { - id: "sortedbee", - name: "Sorted Bee", - domain: "sortedbee.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://www.sortedbee.com/opt-out", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 14, - isActive: true, - }, - { - id: "ussearch", - name: "US Search", - domain: "ussearch.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.AUTOMATED, - removalUrl: "https://www.ussearch.com/opt-out", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 7, - isActive: true, - }, - { - id: "tellme", - name: "Tell me Online Info", - domain: "tellmeonlineinfo.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.MANUAL_FORM, - removalUrl: "https://tellmeonlineinfo.com/opt-out", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 14, - isActive: true, - }, - { - id: "synpeople", - name: "Synpeople", - domain: "synpeople.com", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.AUTOMATED, - removalUrl: "https://www.synpeople.com/opt-out", - requiresAccount: false, - requiresVerification: true, - estimatedDays: 7, - isActive: true, - }, - { - id: "atomdata", - name: "Atom Data", - domain: "atomdata.xyz", - category: BrokerCategory.PEOPLE_SEARCH, - removalMethod: RemovalMethod.EMAIL, - removalUrl: undefined, - requiresAccount: false, - requiresVerification: false, - estimatedDays: 14, - isActive: true, - }, -]; - -export function getBrokerById(id: string): BrokerEntry | undefined { - return BROKER_REGISTRY.find((b) => b.id === id); -} - -export function getActiveBrokers(): BrokerEntry[] { - return BROKER_REGISTRY.filter((b) => b.isActive); -} - -export function getBrokersByCategory(category: string): BrokerEntry[] { - return BROKER_REGISTRY.filter((b) => b.category === category); -} - -export function getBrokersByMethod(method: RemovalMethod): BrokerEntry[] { - return BROKER_REGISTRY.filter((b) => b.removalMethod === method); -} diff --git a/services/removebrokers/src/index.ts b/services/removebrokers/src/index.ts deleted file mode 100644 index a706f73..0000000 --- a/services/removebrokers/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { removeBrokersService } from "./RemoveBrokersService"; -export { RemoveBrokersService } from "./RemoveBrokersService"; -export { removeBrokersScheduler } from "./RemoveBrokersScheduler"; -export { RemoveBrokersScheduler } from "./RemoveBrokersScheduler"; -export { brokerAlertPipeline } from "./BrokerAlertPipeline"; -export { BrokerAlertPipeline } from "./BrokerAlertPipeline"; -export { BROKER_REGISTRY, getBrokerById, getActiveBrokers } from "./brokerRegistry"; -export type { PersonalInfo, RemovalJob, BrokerEntry } from "./types"; diff --git a/services/removebrokers/src/types.ts b/services/removebrokers/src/types.ts deleted file mode 100644 index c204ecc..0000000 --- a/services/removebrokers/src/types.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { RemovalMethod, RemovalStatus, BrokerCategory } from "@shieldai/types"; - -export interface PersonalInfo { - fullName: string; - email?: string; - phone?: string; - address?: { - street?: string; - city?: string; - state?: string; - zip?: string; - }; - dob?: string; -} - -export interface BrokerEntry { - id: string; - name: string; - domain: string; - category: BrokerCategory; - removalMethod: RemovalMethod; - removalUrl?: string; - requiresAccount: boolean; - requiresVerification: boolean; - estimatedDays: number; - isActive: boolean; -} - -export interface RemovalJob { - requestId: string; - brokerId: string; - brokerName: string; - personalInfo: PersonalInfo; - method: RemovalMethod; - attempt: number; -} - -export const MAX_REMOVAL_ATTEMPTS = 3; -export const RETRY_DELAY_MS = 24 * 60 * 60 * 1000; // 24 hours diff --git a/services/removebrokers/test/brokerRegistry.test.ts b/services/removebrokers/test/brokerRegistry.test.ts deleted file mode 100644 index a97e393..0000000 --- a/services/removebrokers/test/brokerRegistry.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { BROKER_REGISTRY, getBrokerById, getActiveBrokers } from "../src/brokerRegistry"; -import { RemovalMethod } from "@shieldai/types"; - -describe("BrokerRegistry", () => { - it("should have brokers registered", () => { - expect(BROKER_REGISTRY.length).toBeGreaterThan(0); - }); - - it("should find broker by id", () => { - const broker = getBrokerById("whitepages"); - expect(broker).toBeDefined(); - expect(broker?.name).toBe("Whitepages"); - expect(broker?.domain).toBe("whitepages.com"); - }); - - it("should return undefined for unknown broker", () => { - const broker = getBrokerById("nonexistent"); - expect(broker).toBeUndefined(); - }); - - it("should return only active brokers", () => { - const active = getActiveBrokers(); - expect(active.length).toBeGreaterThan(0); - for (const broker of active) { - expect(broker.isActive).toBe(true); - } - }); - - it("should have varied removal methods", () => { - const methods = new Set(BROKER_REGISTRY.map((b) => b.removalMethod)); - expect(methods.has(RemovalMethod.AUTOMATED)).toBe(true); - expect(methods.has(RemovalMethod.MANUAL_FORM)).toBe(true); - expect(methods.has(RemovalMethod.EMAIL)).toBe(true); - }); -}); diff --git a/services/removebrokers/tsconfig.json b/services/removebrokers/tsconfig.json deleted file mode 100644 index e6f3c02..0000000 --- a/services/removebrokers/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src", - "skipLibCheck": true, - "module": "ES2022", - "moduleResolution": "Bundler" - }, - "include": ["src/**/*.ts"] -} diff --git a/services/removebrokers/vitest.config.ts b/services/removebrokers/vitest.config.ts deleted file mode 100644 index cd170fe..0000000 --- a/services/removebrokers/vitest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['test/**/*.test.ts'], - coverage: { - provider: 'v8', - include: ['src/**/*.ts'], - exclude: ['src/index.ts'], - }, - }, -}); diff --git a/services/spamshield/Dockerfile b/services/spamshield/Dockerfile deleted file mode 100644 index ce35ec7..0000000 --- a/services/spamshield/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -FROM node:20-alpine AS builder - -WORKDIR /app - -COPY package.json pnpm-lock.yaml turbo.json pnpm-workspace.yaml ./ -COPY packages/api/package.json ./packages/api/ -COPY packages/db/package.json ./packages/db/ -COPY packages/types/package.json ./packages/types/ -COPY packages/core/package.json ./packages/core/ 2>/dev/null || true -COPY packages/jobs/package.json ./packages/jobs/ -COPY packages/shared-notifications/package.json ./packages/shared-notifications/ -COPY services/darkwatch/package.json ./services/darkwatch/ -COPY services/spamshield/package.json ./services/spamshield/ -COPY services/voiceprint/package.json ./services/voiceprint/ - -RUN npm i -g pnpm@9 && pnpm install --frozen-lockfile - -COPY tsconfig.json ./ -COPY packages/types/tsconfig.json ./packages/types/ -COPY packages/db/tsconfig.json ./packages/db/ -COPY services/spamshield/tsconfig.json ./services/spamshield/ -COPY services/spamshield/ ./services/spamshield/ -COPY packages/types/ ./packages/types/ -COPY packages/db/ ./packages/db/ - -RUN pnpm build --filter=@shieldai/types --filter=@shieldai/db --filter=@shieldai/spamshield - -FROM node:20-alpine AS runner - -WORKDIR /app - -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 shieldai - -COPY --from=builder --chown=shieldai:nodejs /app/services/spamshield/dist ./dist -COPY --from=builder --chown=shieldai:nodejs /app/node_modules ./node_modules -COPY --from=builder --chown=shieldai:nodejs /app/services/spamshield/package.json ./package.json -COPY --from=builder --chown=shieldai:nodejs /app/packages/db ./packages/db - -USER shieldai - -EXPOSE 3002 - -CMD ["node", "dist/index.js"] diff --git a/services/spamshield/package.json b/services/spamshield/package.json deleted file mode 100644 index 590966b..0000000 --- a/services/spamshield/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@shieldai/spamshield", - "version": "0.1.0", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "scripts": { - "build": "tsc", - "dev": "tsx watch src/index.ts", - "lint": "eslint src/", - "test": "vitest run", - "test:coverage": "vitest run --coverage", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@shieldsai/shared-analytics": "workspace:*", - "@shieldai/db": "workspace:*", - "@shieldai/types": "workspace:*", - "@shieldai/correlation": "workspace:*", - "@prisma/client": "^6.2.0", - "libphonenumber-js": "^1.10.50", - "ws": "^8.16.0" - }, - "devDependencies": { - "typescript": "^5.3.3", - "tsx": "^4.19.0", - "eslint": "^8.56.0", - "@types/ws": "^8.5.10", - "vitest": "^4.1.5", - "@vitest/coverage-v8": "^4.1.5" - } -} diff --git a/services/spamshield/src/carriers/carrier-factory.ts b/services/spamshield/src/carriers/carrier-factory.ts deleted file mode 100644 index 01f7983..0000000 --- a/services/spamshield/src/carriers/carrier-factory.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { CarrierApi } from './carrier-types'; -import { TwilioCarrier } from './twilio-carrier'; -import { PlivoCarrier } from './plivo-carrier'; - -export type CarrierType = 'twilio' | 'plivo' | 'sip'; - -export interface CarrierFactoryConfig { - twilio?: { - apiKey: string; - apiSecret: string; - accountSid: string; - apiBaseUrl?: string; - decisionTimeout?: number; - }; - plivo?: { - authId: string; - authToken: string; - apiBaseUrl?: string; - decisionTimeout?: number; - }; - defaultDecisionTimeout?: number; -} - -export class CarrierFactory { - private readonly config: CarrierFactoryConfig; - private readonly carriers: Map = new Map(); - - constructor(config: CarrierFactoryConfig) { - this.config = { - defaultDecisionTimeout: 200, - ...config, - }; - } - - createCarrier(type: CarrierType): CarrierApi { - const cached = this.carriers.get(type); - if (cached) { - return cached; - } - - const carrier = this.instantiateCarrier(type); - this.carriers.set(type, carrier); - return carrier; - } - - async validateCarrier(type: CarrierType): Promise { - const carrier = this.createCarrier(type); - return carrier.isHealthy(); - } - - async getCarrierMetrics(type: CarrierType): Promise<{ - type: CarrierType; - healthy: boolean; - latency: number; - }> { - const carrier = this.createCarrier(type); - const startTime = Date.now(); - const healthy = await carrier.isHealthy(); - const latency = Date.now() - startTime; - - return { type, healthy, latency }; - } - - private instantiateCarrier(type: CarrierType): CarrierApi { - switch (type) { - case 'twilio': - if (!this.config.twilio) { - throw new Error('Twilio configuration not provided'); - } - return new TwilioCarrier({ - ...this.config.twilio, - decisionTimeout: this.config.twilio.decisionTimeout ?? this.config.defaultDecisionTimeout, - }); - - case 'plivo': - if (!this.config.plivo) { - throw new Error('Plivo configuration not provided'); - } - return new PlivoCarrier({ - ...this.config.plivo, - decisionTimeout: this.config.plivo.decisionTimeout ?? this.config.defaultDecisionTimeout, - }); - - case 'sip': - // SIP carrier would be implemented separately - throw new Error('SIP carrier not yet implemented'); - - default: - throw new Error(`Unknown carrier type: ${type}`); - } - } - - async getAllCarriers(): Promise> { - const results: Array<{ type: CarrierType; healthy: boolean }> = []; - - for (const [type, carrier] of this.carriers.entries()) { - const healthy = await carrier.isHealthy(); - results.push({ - type, - healthy, - }); - } - - return results; - } - - clearCache(): void { - this.carriers.clear(); - } -} diff --git a/services/spamshield/src/carriers/carrier-types.ts b/services/spamshield/src/carriers/carrier-types.ts deleted file mode 100644 index 53eaa2c..0000000 --- a/services/spamshield/src/carriers/carrier-types.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Carrier API types and interfaces - -export interface CarrierCall { - callSid: string; - from: string; - to: string; - status: 'initiated' | 'ringing' | 'in-progress' | 'completed' | 'failed'; - startTime: Date; - duration?: number; - metadata?: Record; -} - -export interface CarrierSms { - messageSid: string; - from: string; - to: string; - body: string; - direction: 'inbound' | 'outbound'; - status: 'queued' | 'sent' | 'delivered' | 'failed'; - timestamp: Date; - metadata?: Record; -} - -export interface CarrierDecision { - action: 'block' | 'flag' | 'allow'; - confidence: number; - reasons: string[]; - executedAt: Date; -} - -export interface CarrierApi { - // Call operations - getCall(callSid: string): Promise; - blockCall(callSid: string): Promise; - flagCall(callSid: string): Promise; - allowCall(callSid: string): Promise; - - // SMS operations - getSms(messageSid: string): Promise; - blockSms(messageSid: string): Promise; - flagSms(messageSid: string): Promise; - allowSms(messageSid: string): Promise; - - // Health check - isHealthy(): Promise; -} diff --git a/services/spamshield/src/carriers/index.ts b/services/spamshield/src/carriers/index.ts deleted file mode 100644 index 2a7ffcb..0000000 --- a/services/spamshield/src/carriers/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './carrier-types'; -export * from './twilio-carrier'; -export * from './plivo-carrier'; -export * from './carrier-factory'; diff --git a/services/spamshield/src/carriers/plivo-carrier.ts b/services/spamshield/src/carriers/plivo-carrier.ts deleted file mode 100644 index 3365e79..0000000 --- a/services/spamshield/src/carriers/plivo-carrier.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { CarrierApi, CarrierCall, CarrierSms } from './carrier-types'; - -interface PlivoConfig { - authId: string; - authToken: string; - apiBaseUrl?: string; - decisionTimeout?: number; -} - -export class PlivoCarrier implements CarrierApi { - private readonly config: PlivoConfig; - private readonly apiBaseUrl: string; - - constructor(config: PlivoConfig) { - this.config = { - ...config, - apiBaseUrl: config.apiBaseUrl ?? 'https://api.plivo.com', - decisionTimeout: config.decisionTimeout ?? 200, - }; - this.apiBaseUrl = this.config.apiBaseUrl; - } - - async getCall(callSid: string): Promise { - const response = await fetch( - `${this.apiBaseUrl}/v1/Account/${this.config.authId}/Call/${callSid}/`, - { - method: 'GET', - headers: { - 'Authorization': `Basic ${Buffer.from(`${this.config.authId}:${this.config.authToken}`).toString('base64')}`, - 'Accept': 'application/json', - }, - timeout: this.config.decisionTimeout, - } - ); - - if (!response.ok) { - throw new Error(`Plivo API error: ${response.status}`); - } - - const data = await response.json() as PlivoCallResponse; - return this.mapToCarrierCall(data); - } - - async blockCall(callSid: string): Promise { - await this.executeCarrierAction('block', callSid); - } - - async flagCall(callSid: string): Promise { - await this.executeCarrierAction('flag', callSid); - } - - async allowCall(callSid: string): Promise { - await this.executeCarrierAction('allow', callSid); - } - - async getSms(messageSid: string): Promise { - const response = await fetch( - `${this.apiBaseUrl}/v1/Account/${this.config.authId}/Message/${messageSid}/`, - { - method: 'GET', - headers: { - 'Authorization': `Basic ${Buffer.from(`${this.config.authId}:${this.config.authToken}`).toString('base64')}`, - 'Accept': 'application/json', - }, - timeout: this.config.decisionTimeout, - } - ); - - if (!response.ok) { - throw new Error(`Plivo API error: ${response.status}`); - } - - const data = await response.json() as PlivoSmsResponse; - return this.mapToCarrierSms(data); - } - - async blockSms(messageSid: string): Promise { - await this.executeCarrierAction('block', messageSid, 'sms'); - } - - async flagSms(messageSid: string): Promise { - await this.executeCarrierAction('flag', messageSid, 'sms'); - } - - async allowSms(messageSid: string): Promise { - await this.executeCarrierAction('allow', messageSid, 'sms'); - } - - async isHealthy(): Promise { - try { - const response = await fetch( - `${this.apiBaseUrl}/v1/Account/${this.config.authId}/`, - { - method: 'GET', - headers: { - 'Authorization': `Basic ${Buffer.from(`${this.config.authId}:${this.config.authToken}`).toString('base64')}`, - }, - timeout: 5000, - } - ); - return response.ok; - } catch { - return false; - } - } - - private async executeCarrierAction( - action: 'block' | 'flag' | 'allow', - sid: string, - type: 'call' | 'sms' = 'call' - ): Promise { - const endpoint = type === 'call' - ? `${this.apiBaseUrl}/v1/Account/${this.config.authId}/Call/${sid}/` - : `${this.apiBaseUrl}/v1/Account/${this.config.authId}/Message/${sid}/`; - - // Plivo uses a custom header for action control - const actionHeader = - action === 'block' ? 'spam-block' : - action === 'flag' ? 'spam-flag' : 'allow'; - - await fetch(endpoint, { - method: 'POST', - headers: { - 'Authorization': `Basic ${Buffer.from(`${this.config.authId}:${this.config.authToken}`).toString('base64')}`, - 'Content-Type': 'application/json', - 'X-ShieldAI-Action': actionHeader, - }, - body: JSON.stringify({ action }), - timeout: this.config.decisionTimeout, - }); - } - - private mapToCarrierCall(data: PlivoCallResponse): CarrierCall { - return { - callSid: data.callUuid || data.resourceUri, - from: data.from, - to: data.to, - status: this.mapCallStatus(data.status), - startTime: new Date(data.startTime || data.callStartTime), - duration: data.duration ? parseInt(data.duration) : undefined, - metadata: { - plivoPrice: data.price, - plivoDirection: data.direction, - plivoAnswerTime: data.answerTime, - }, - }; - } - - private mapToCarrierSms(data: PlivoSmsResponse): CarrierSms { - return { - messageSid: data.messageUuid || data.resourceUri, - from: data.from, - to: data.to, - body: data.text, - direction: this.mapSmsDirection(data.direction), - status: this.mapSmsStatus(data.status), - timestamp: new Date(data.sendTime || data.time), - metadata: { - plivoNumParts: data.numParts, - plivoType: data.type, - plivoError: data.error, - }, - }; - } - - private mapCallStatus(status: string): CarrierCall['status'] { - const statusMap: Record = { - 'in-progress': 'in-progress', - 'completed': 'completed', - 'failed': 'failed', - 'ringing': 'ringing', - 'busy': 'failed', - 'no-answer': 'failed', - }; - return statusMap[status] ?? 'failed'; - } - - private mapSmsDirection(direction: string): CarrierSms['direction'] { - return direction === 'inbound' ? 'inbound' : 'outbound'; - } - - private mapSmsStatus(status: string): CarrierSms['status'] { - const statusMap: Record = { - 'queued': 'queued', - 'sent': 'sent', - 'delivered': 'delivered', - 'failed': 'failed', - 'undelivered': 'failed', - }; - return statusMap[status] ?? 'failed'; - } -} - -interface PlivoCallResponse { - callUuid?: string; - resourceUri: string; - from: string; - to: string; - status: string; - startTime?: string; - callStartTime?: string; - duration?: string; - price?: string; - direction?: string; - answerTime?: string; -} - -interface PlivoSmsResponse { - messageUuid?: string; - resourceUri: string; - from: string; - to: string; - text: string; - direction: string; - status: string; - sendTime?: string; - time?: string; - numParts?: string; - type?: string; - error?: string; -} diff --git a/services/spamshield/src/carriers/twilio-carrier.ts b/services/spamshield/src/carriers/twilio-carrier.ts deleted file mode 100644 index 48fa511..0000000 --- a/services/spamshield/src/carriers/twilio-carrier.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { CarrierApi, CarrierCall, CarrierSms, CarrierDecision } from './carrier-types'; - -interface TwilioConfig { - apiKey: string; - apiSecret: string; - accountSid: string; - apiBaseUrl?: string; - decisionTimeout?: number; -} - -export class TwilioCarrier implements CarrierApi { - private readonly config: TwilioConfig; - private readonly apiBaseUrl: string; - - constructor(config: TwilioConfig) { - this.config = { - ...config, - apiBaseUrl: config.apiBaseUrl ?? 'https://api.twilio.com', - decisionTimeout: config.decisionTimeout ?? 200, - }; - this.apiBaseUrl = this.config.apiBaseUrl; - } - - async getCall(callSid: string): Promise { - const response = await fetch( - `${this.apiBaseUrl}/2010-04-01/Accounts/${this.config.accountSid}/Calls/${callSid}.json`, - { - method: 'GET', - headers: { - 'Authorization': `Basic ${Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString('base64')}`, - 'Accept': 'application/json', - }, - timeout: this.config.decisionTimeout, - } - ); - - if (!response.ok) { - throw new Error(`Twilio API error: ${response.status}`); - } - - const data = await response.json() as TwilioCallResponse; - return this.mapToCarrierCall(data); - } - - async blockCall(callSid: string): Promise { - await this.executeCarrierAction('block', callSid); - } - - async flagCall(callSid: string): Promise { - await this.executeCarrierAction('flag', callSid); - } - - async allowCall(callSid: string): Promise { - await this.executeCarrierAction('allow', callSid); - } - - async getSms(messageSid: string): Promise { - const response = await fetch( - `${this.apiBaseUrl}/2010-04-01/Accounts/${this.config.accountSid}/Messages/${messageSid}.json`, - { - method: 'GET', - headers: { - 'Authorization': `Basic ${Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString('base64')}`, - 'Accept': 'application/json', - }, - timeout: this.config.decisionTimeout, - } - ); - - if (!response.ok) { - throw new Error(`Twilio API error: ${response.status}`); - } - - const data = await response.json() as TwilioSmsResponse; - return this.mapToCarrierSms(data); - } - - async blockSms(messageSid: string): Promise { - await this.executeCarrierAction('block', messageSid, 'sms'); - } - - async flagSms(messageSid: string): Promise { - await this.executeCarrierAction('flag', messageSid, 'sms'); - } - - async allowSms(messageSid: string): Promise { - await this.executeCarrierAction('allow', messageSid, 'sms'); - } - - async isHealthy(): Promise { - try { - const response = await fetch( - `${this.apiBaseUrl}/2010-04-01/Accounts/${this.config.accountSid}.json`, - { - method: 'GET', - headers: { - 'Authorization': `Basic ${Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString('base64')}`, - }, - timeout: 5000, - } - ); - return response.ok; - } catch { - return false; - } - } - - private async executeCarrierAction( - action: 'block' | 'flag' | 'allow', - sid: string, - type: 'call' | 'sms' = 'call' - ): Promise { - const endpoint = type === 'call' - ? `${this.apiBaseUrl}/2010-04-01/Accounts/${this.config.accountSid}/Calls/${sid}.json` - : `${this.apiBaseUrl}/2010-04-01/Accounts/${this.config.accountSid}/Messages/${sid}.json`; - - // Twilio uses Status parameter to control call/SMS state - const statusUpdate: string = - action === 'block' ? 'completed' : - action === 'flag' ? 'ringing' : 'in-progress'; - - await fetch(endpoint, { - method: 'POST', - headers: { - 'Authorization': `Basic ${Buffer.from(`${this.config.apiKey}:${this.config.apiSecret}`).toString('base64')}`, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: `Status=${statusUpdate}`, - timeout: this.config.decisionTimeout, - }); - } - - private mapToCarrierCall(data: TwilioCallResponse): CarrierCall { - return { - callSid: data.sid, - from: data.from, - to: data.to, - status: this.mapCallStatus(data.status), - startTime: new Date(data.startTime), - duration: data.duration ? parseInt(data.duration) : undefined, - metadata: { - twilioPrice: data.price, - twilioDirection: data.direction, - twilioApiVersion: data.apiVersion, - }, - }; - } - - private mapToCarrierSms(data: TwilioSmsResponse): CarrierSms { - return { - messageSid: data.sid, - from: data.from, - to: data.to, - body: data.body, - direction: this.mapSmsDirection(data.direction), - status: this.mapSmsStatus(data.status), - timestamp: new Date(data.dateSent || data.dateCreated), - metadata: { - twilioNumSegments: data.numSegments, - twilioNumMedia: data.numMedia, - twilioError: data.errorMessage, - }, - }; - } - - private mapCallStatus(status: string): CarrierCall['status'] { - const statusMap: Record = { - 'initiated': 'initiated', - 'ringing': 'ringing', - 'in-progress': 'in-progress', - 'completed': 'completed', - 'failed': 'failed', - 'busy': 'failed', - 'no-answer': 'failed', - }; - return statusMap[status] ?? 'failed'; - } - - private mapSmsDirection(direction: string): CarrierSms['direction'] { - return direction === 'inbound' ? 'inbound' : 'outbound'; - } - - private mapSmsStatus(status: string): CarrierSms['status'] { - const statusMap: Record = { - 'queued': 'queued', - 'sent': 'sent', - 'delivered': 'delivered', - 'failed': 'failed', - 'undelivered': 'failed', - }; - return statusMap[status] ?? 'failed'; - } -} - -interface TwilioCallResponse { - sid: string; - from: string; - to: string; - status: string; - startTime: string; - duration?: string; - price?: string; - direction?: string; - apiVersion?: string; -} - -interface TwilioSmsResponse { - sid: string; - from: string; - to: string; - body: string; - direction: string; - status: string; - dateSent?: string; - dateCreated: string; - numSegments?: string; - numMedia?: string; - errorMessage?: string; -} diff --git a/services/spamshield/src/circuit-breaker/circuit-breaker.ts b/services/spamshield/src/circuit-breaker/circuit-breaker.ts deleted file mode 100644 index 60604d8..0000000 --- a/services/spamshield/src/circuit-breaker/circuit-breaker.ts +++ /dev/null @@ -1,173 +0,0 @@ -export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN'; - -export interface CircuitBreakerMetrics { - state: CircuitState; - failureCount: number; - successCount: number; - lastFailureTime: Date | null; - lastSuccessTime: Date | null; - stateChangedAt: Date | null; - totalExecutions: number; - totalFailures: number; - totalSuccesses: number; -} - -export interface CircuitBreakerOptions { - failureThreshold?: number; - successThreshold?: number; - timeout?: number; - onStateChange?: (state: CircuitState, previousState: CircuitState) => void; -} - -const DEFAULT_FAILURE_THRESHOLD = 5; -const DEFAULT_SUCCESS_THRESHOLD = 3; -const DEFAULT_TIMEOUT_MS = 60000; - -export class CircuitBreakerError extends Error { - public readonly state: CircuitState; - - constructor(message: string, state: CircuitState) { - super(message); - this.name = 'CircuitBreakerError'; - this.state = state; - } -} - -export class CircuitBreaker { - private state: CircuitState = 'CLOSED'; - private failureCount = 0; - private successCount = 0; - private lastFailureTime: Date | null = null; - private lastSuccessTime: Date | null = null; - private stateChangedAt: Date | null = null; - private totalExecutions = 0; - private totalFailures = 0; - private totalSuccesses = 0; - - private readonly failureThreshold: number; - private readonly successThreshold: number; - private readonly timeout: number; - private readonly onStateChange?: (state: CircuitState, previousState: CircuitState) => void; - - constructor(options?: CircuitBreakerOptions) { - this.failureThreshold = options?.failureThreshold ?? DEFAULT_FAILURE_THRESHOLD; - this.successThreshold = options?.successThreshold ?? DEFAULT_SUCCESS_THRESHOLD; - this.timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS; - this.onStateChange = options?.onStateChange; - this.stateChangedAt = new Date(); - } - - public getState(): CircuitState { - if (this.state === 'OPEN') { - const elapsed = Date.now() - this.lastFailureTime!.getTime(); - if (elapsed >= this.timeout) { - this.transitionTo('HALF_OPEN'); - } - } - return this.state; - } - - public async execute( - fn: () => Promise, - fallback?: () => T | Promise - ): Promise { - this.totalExecutions++; - const currentState = this.getState(); - - try { - let result: T; - - if (currentState === 'OPEN') { - throw new CircuitBreakerError( - `Circuit is OPEN. Failures: ${this.failureCount}/${this.failureThreshold}`, - this.state - ); - } - - result = await fn(); - this.recordSuccess(); - return result; - } catch (error) { - this.recordFailure(); - - if (fallback) { - try { - return fallback(); - } catch (fallbackError) { - throw error; - } - } - - throw error; - } - } - - public getMetrics(): CircuitBreakerMetrics { - return { - state: this.getState(), - failureCount: this.failureCount, - successCount: this.successCount, - lastFailureTime: this.lastFailureTime, - lastSuccessTime: this.lastSuccessTime, - stateChangedAt: this.stateChangedAt, - totalExecutions: this.totalExecutions, - totalFailures: this.totalFailures, - totalSuccesses: this.totalSuccesses, - }; - } - - public reset(): void { - const previousState = this.state; - this.state = 'CLOSED'; - this.failureCount = 0; - this.successCount = 0; - this.lastFailureTime = null; - this.lastSuccessTime = null; - this.stateChangedAt = new Date(); - if (previousState !== 'CLOSED') { - this.emitStateChange('CLOSED', previousState); - } - } - - private recordSuccess(): void { - this.lastSuccessTime = new Date(); - this.totalSuccesses++; - - if (this.state === 'HALF_OPEN') { - this.successCount++; - if (this.successCount >= this.successThreshold) { - this.transitionTo('CLOSED'); - this.failureCount = 0; - this.successCount = 0; - } - } - } - - private recordFailure(): void { - this.lastFailureTime = new Date(); - this.totalFailures++; - this.failureCount++; - - if (this.state === 'HALF_OPEN') { - this.transitionTo('OPEN'); - } else if (this.state === 'CLOSED' && this.failureCount >= this.failureThreshold) { - this.transitionTo('OPEN'); - } - } - - private transitionTo(newState: CircuitState): void { - const previousState = this.state; - this.state = newState; - this.stateChangedAt = new Date(); - if (newState === 'CLOSED') { - this.successCount = 0; - } - this.emitStateChange(newState, previousState); - } - - private emitStateChange(newState: CircuitState, previousState: CircuitState): void { - if (this.onStateChange && newState !== previousState) { - this.onStateChange(newState, previousState); - } - } -} diff --git a/services/spamshield/src/circuit-breaker/index.ts b/services/spamshield/src/circuit-breaker/index.ts deleted file mode 100644 index 6465e15..0000000 --- a/services/spamshield/src/circuit-breaker/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { CircuitBreaker, CircuitBreakerError } from './circuit-breaker'; -export type { CircuitState, CircuitBreakerMetrics, CircuitBreakerOptions } from './circuit-breaker'; diff --git a/services/spamshield/src/classifier/sms-classifier.ts b/services/spamshield/src/classifier/sms-classifier.ts deleted file mode 100644 index b708279..0000000 --- a/services/spamshield/src/classifier/sms-classifier.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { SpamShieldService } from '../services/spamshield.service'; -import { - HIGH_RISK_LINK_SCORE, - SHORT_AGGRESSIVE_SCORE, - EXCESSIVE_NUMBERS_SCORE, - URGENT_NEGATIVE_SCORE, - REPUTATION_SCORE_WEIGHT, - SMS_SPAM_THRESHOLD, -} from '../constants/sms-classifier.constants'; - -export interface SmsClassificationResult { - isSpam: boolean; - score: number; - features: { - language: string; - length: number; - hasLinks: boolean; - hasNumbers: boolean; - sentiment: 'positive' | 'neutral' | 'negative'; - }; -} - -export interface SmsClassificationContext { - text: string; - senderPhoneNumber?: string; -} - -export interface SmsClassifier { - classify(textOrContext: string | SmsClassificationContext): Promise; - getMetrics(): { - totalClassified: number; - spamDetected: number; - accuracy: number; - }; -} - -/** - * BERT-based SMS Content Classifier - * Uses language analysis, pattern matching, and ML heuristics - */ -export class BertSmsClassifier implements SmsClassifier { - private spamShield: SpamShieldService; - private metrics: { - totalClassified: number; - spamDetected: number; - } = { totalClassified: 0, spamDetected: 0 }; - - constructor(spamShield: SpamShieldService) { - this.spamShield = spamShield; - } - - async classify(textOrContext: string | SmsClassificationContext): Promise { - const text = typeof textOrContext === 'string' ? textOrContext : textOrContext.text; - const senderPhoneNumber = typeof textOrContext === 'string' ? undefined : textOrContext.senderPhoneNumber; - - // Feature 1: Language Analysis - const language = this.analyzeLanguage(text); - - // Feature 2: Length Analysis - const length = text.length; - const lengthScore = this.calculateLengthScore(length); - - // Feature 3: Link Detection - const hasLinks = this.detectLinks(text); - - // Feature 4: Number Detection - const hasNumbers = /\d/.test(text); - - // Feature 5: Sentiment Analysis - const sentiment = this.analyzeSentiment(text); - - // Calculate spam probability - let spamScore = 0; - - // High-risk patterns - if (hasLinks && length > 100) { - spamScore += HIGH_RISK_LINK_SCORE; - } - - // Short aggressive messages - if (length < 20 && hasNumbers) { - spamScore += SHORT_AGGRESSIVE_SCORE; - } - - // Excessive numbers - if (/\d{3,}/.test(text)) { - spamScore += EXCESSIVE_NUMBERS_SCORE; - } - - // Negative/urgent language - if (sentiment === 'negative' && language === 'unknown') { - spamScore += URGENT_NEGATIVE_SCORE; - } - - // Combine with reputation score if available - if (senderPhoneNumber) { - const reputation = await this.spamShield.checkReputation(senderPhoneNumber); - if (reputation.isSpam) { - spamScore += REPUTATION_SCORE_WEIGHT; - } - } - - const isSpam = spamScore > SMS_SPAM_THRESHOLD; - - // Update metrics - this.metrics.totalClassified++; - if (isSpam) { - this.metrics.spamDetected++; - } - - return { - isSpam, - score: spamScore, - features: { - language, - length, - hasLinks, - hasNumbers, - sentiment, - }, - }; - } - - private analyzeLanguage(text: string): string { - // Simple language detection based on character patterns - const englishIndicators = /(?:the|be|to|of|and|a|in|that|it|for|on|with|as|at|this|is|you|his|her|they|we|you|their|who|what|when|where|why|how|can|will|would|should|could|may|might|must|shall|do|does|did|done|have|has|had|hav(?:e|e))gi/; - - if (englishIndicators.test(text)) { - return 'english'; - } - - if (text.length > 50 && /[а-я]/.test(text)) { - return 'russian'; - } - - if (text.length > 50 && /[가-힣]/.test(text)) { - return 'korean'; - } - - if (text.length > 50 && /[؀-ۿ]/.test(text)) { - return 'arabic'; - } - - return 'unknown'; - } - - private calculateLengthScore(length: number): number { - // Optimal SMS length is 160 chars - if (length <= 160) { - return 0; - } - - // Extra characters beyond 160 increase spam probability - const overflow = length - 160; - return Math.min(overflow / 160, 0.3); - } - - private detectLinks(text: string): boolean { - const linkPatterns = [ - /https?:\/\/[a-zA-Z0-9.-]+/g, - /www\.[a-zA-Z0-9.-]+/g, - /bit\.ly\//g, - /t\.co\//g, - /goo\.gl\//g, - ]; - - for (const pattern of linkPatterns) { - if (pattern.test(text)) { - return true; - } - } - - return false; - } - - private analyzeSentiment(text: string): 'positive' | 'neutral' | 'negative' { - const positiveWords = /(?:happy|good|great|awesome|love|win|free|money|prize|congratulations)/i; - const negativeWords = /(?:angry|sad|stop|delete|urgent|immediate|call|verify|account|suspicious|blocked)/i; - const neutralWords = /(?:hello|hi|hey|thanks|thanks|please|help|info)/i; - - if (positiveWords.test(text)) { - return 'positive'; - } - if (negativeWords.test(text)) { - return 'negative'; - } - if (neutralWords.test(text)) { - return 'neutral'; - } - - return 'neutral'; - } - - getMetrics(): { - totalClassified: number; - spamDetected: number; - accuracy: number; - } { - const accuracy = this.metrics.totalClassified > 0 - ? (this.metrics.spamDetected / this.metrics.totalClassified) - : 0; - - return { - totalClassified: this.metrics.totalClassified, - spamDetected: this.metrics.spamDetected, - accuracy, - }; - } -} diff --git a/services/spamshield/src/config/spamshield.config.ts b/services/spamshield/src/config/spamshield.config.ts deleted file mode 100644 index be740a1..0000000 --- a/services/spamshield/src/config/spamshield.config.ts +++ /dev/null @@ -1,82 +0,0 @@ -export type SubscriptionTier = 'BASIC' | 'PLUS' | 'PREMIUM'; - -export interface TierRateLimits { - perMinute: number; - perDay: number; -} - -export type SubscriptionTierRateLimits = Record; - -export const spamRateLimits: SubscriptionTierRateLimits = { - BASIC: { perMinute: 100, perDay: 1000 }, - PLUS: { perMinute: 500, perDay: 5000 }, - PREMIUM: { perMinute: 2000, perDay: 20000 }, -} as const; - -export const spamFeatureFlagDefaults = { - enableHiyaIntegration: true, - enableTruecallerIntegration: true, - enableSMSClassification: true, - enableCallAnalysis: true, - enableFeedbackLoop: true, -} as const; - -type FeatureFlagKey = keyof typeof spamFeatureFlagDefaults; - -export function checkFeatureFlag(flag: FeatureFlagKey): boolean { - const envKey = `FLAG_${flag.toUpperCase()}`; - const envValue = process.env[envKey]; - - if (envValue !== undefined) { - return envValue === 'true' || envValue === '1'; - } - - return spamFeatureFlagDefaults[flag]; -} - -export const spamFeatureFlags = { - get enableHiyaIntegration() { - return checkFeatureFlag('enableHiyaIntegration'); - }, - get enableTruecallerIntegration() { - return checkFeatureFlag('enableTruecallerIntegration'); - }, - get enableSMSClassification() { - return checkFeatureFlag('enableSMSClassification'); - }, - get enableCallAnalysis() { - return checkFeatureFlag('enableCallAnalysis'); - }, - get enableFeedbackLoop() { - return checkFeatureFlag('enableFeedbackLoop'); - }, -} as const; - -export const spamConfig = { - maxPhoneNumberLength: 20, - minPhoneNumberLength: 10, - defaultConfidenceThreshold: 0.7, - maxMetadataSize: 1024 * 4, - circuitBreakerThreshold: 5, - circuitBreakerTimeout: 60000, -} as const; - -export const metadataLimits = { - maxMetadataSizeBytes: 4096, - maxMetadataKeys: 20, - maxMetadataValueSizeBytes: 512, -} as const; - -/** Reputation and Spam Score Constants */ -export const defaultReputationConfidence = 0.7; -export const defaultSpamScore = 0.0; -export const highReputationThreshold = 0.8; -export const lowReputationThreshold = 0.3; - -/** Feature Weights for Reputation Scoring */ -export const featureWeights = { - reputationWeight: 0.4, - ruleWeight: 0.3, - behavioralWeight: 0.2, - userHistoryWeight: 0.1, -} as const; diff --git a/services/spamshield/src/constants/decision-engine.constants.ts b/services/spamshield/src/constants/decision-engine.constants.ts deleted file mode 100644 index f64c4b7..0000000 --- a/services/spamshield/src/constants/decision-engine.constants.ts +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Decision Engine Constants - * - * Scoring weights, thresholds, and behavioral factors used in spam detection. - * These values should be reviewed periodically and updated based on model performance. - */ - -/** Scoring Weights */ -export const DEFAULT_REPUTATION_WEIGHT = 0.4; -export const DEFAULT_RULE_WEIGHT = 0.3; -export const DEFAULT_BEHAVIORAL_WEIGHT = 0.2; -export const DEFAULT_USER_HISTORY_WEIGHT = 0.1; - -/** Decision Thresholds */ -export const DEFAULT_BLOCK_THRESHOLD = 0.85; -export const DEFAULT_FLAG_THRESHOLD = 0.60; - -/** Behavioral Analysis Scores */ -export const SHORT_CALL_SCORE = 0.3; // Call duration < 5 seconds -export const SHORT_SMS_SCORE = 0.1; // SMS call type -export const SHORT_CONTENT_SCORE = 0.2; // SMS body < 10 characters -export const URGENT_KEYWORD_SCORE = 0.3; // Contains urgent/act now/limited keywords - -/** Default Fallback Values */ -export const DEFAULT_EVALUATION_TIMEOUT = 200; // milliseconds -export const DEFAULT_FALLBACK_DECISION = 'ALLOW'; -export const DEFAULT_FALLBACK_ON_TIMEOUT = true; diff --git a/services/spamshield/src/constants/sms-classifier.constants.ts b/services/spamshield/src/constants/sms-classifier.constants.ts deleted file mode 100644 index 3154b77..0000000 --- a/services/spamshield/src/constants/sms-classifier.constants.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * SMS Classifier Constants - * - * Scoring weights and thresholds for SMS spam classification. - * These values control the contribution of different features to the final spam score. - */ - -/** Feature Scoring Weights */ -export const HIGH_RISK_LINK_SCORE = 0.3; // Links + long message (>100 chars) -export const SHORT_AGGRESSIVE_SCORE = 0.2; // Short message (<20 chars) with numbers -export const EXCESSIVE_NUMBERS_SCORE = 0.15; // Messages with 3+ digit numbers -export const URGENT_NEGATIVE_SCORE = 0.2; // Negative sentiment + unknown language -export const REPUTATION_SCORE_WEIGHT = 0.25; // Reputation-based spam indicator - -/** Classification Thresholds */ -export const SMS_SPAM_THRESHOLD = 0.5; // Final score threshold for spam classification - -/** Length Analysis */ -export const OPTIMAL_SMS_LENGTH = 160; // Standard SMS character limit -export const MAX_LENGTH_BONUS = 0.3; // Maximum score from length overflow diff --git a/services/spamshield/src/engine/decision-engine.ts b/services/spamshield/src/engine/decision-engine.ts deleted file mode 100644 index 4bd1f5b..0000000 --- a/services/spamshield/src/engine/decision-engine.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { SpamShieldService, ReputationResult } from '../services/spamshield.service'; -import { RuleEngine, RuleMatch } from './rule-engine'; -import { - DEFAULT_REPUTATION_WEIGHT, - DEFAULT_RULE_WEIGHT, - DEFAULT_BEHAVIORAL_WEIGHT, - DEFAULT_USER_HISTORY_WEIGHT, - DEFAULT_BLOCK_THRESHOLD, - DEFAULT_FLAG_THRESHOLD, - DEFAULT_EVALUATION_TIMEOUT, - DEFAULT_FALLBACK_DECISION, - DEFAULT_FALLBACK_ON_TIMEOUT, - SHORT_CALL_SCORE, - SHORT_SMS_SCORE, - SHORT_CONTENT_SCORE, - URGENT_KEYWORD_SCORE, -} from '../constants/decision-engine.constants'; - -export interface CallMetadata { - callId: string; - startTime: Date; - duration?: number; - direction: 'inbound' | 'outbound'; - callType?: 'voice' | 'video' | 'sms'; - carrierInfo?: Record; -} - -export interface SmsContent { - messageId: string; - body: string; - timestamp: Date; - direction: 'inbound' | 'outbound'; -} - -export interface UserSpamHistory { - phoneNumberHash: string; - spamCount: number; - hamCount: number; - lastSpamReportedAt?: Date; - userPreference?: 'block' | 'flag' | 'allow'; -} - -export interface DecisionContext { - phoneNumber: string; - phoneNumberHash?: string; - callMetadata?: CallMetadata; - smsContent?: SmsContent; - cachedReputation: ReputationResult; - ruleMatches: RuleMatch[]; - userHistory?: UserSpamHistory; - requestId?: string; -} - -export interface DecisionResult { - decision: 'BLOCK' | 'FLAG' | 'ALLOW'; - confidence: number; - reasons: string[]; - fallbackDecision: 'BLOCK' | 'FLAG' | 'ALLOW'; - scoring: { - reputationScore: number; - ruleScore: number; - behavioralScore: number; - userHistoryScore: number; - totalScore: number; - }; - executedAt: Date; - requestId?: string; -} - -export interface DecisionEngineConfig { - // Scoring weights - reputationWeight?: number; - ruleWeight?: number; - behavioralWeight?: number; - userHistoryWeight?: number; - - // Thresholds - blockThreshold?: number; - flagThreshold?: number; - - // Timeouts - evaluationTimeout?: number; - - // Fallback behavior - fallbackOnTimeout?: boolean; - fallbackDecision?: 'BLOCK' | 'FLAG' | 'ALLOW'; -} - -// Configuration defaults exported from constants module - -export class DecisionEngine { - private readonly config: Required; - private readonly reputationService: SpamShieldService; - private readonly ruleEngine: RuleEngine; - - constructor( - reputationService: SpamShieldService, - ruleEngine: RuleEngine, - config?: DecisionEngineConfig - ) { - this.config = { - reputationWeight: config?.reputationWeight ?? DEFAULT_REPUTATION_WEIGHT, - ruleWeight: config?.ruleWeight ?? DEFAULT_RULE_WEIGHT, - behavioralWeight: config?.behavioralWeight ?? DEFAULT_BEHAVIORAL_WEIGHT, - userHistoryWeight: config?.userHistoryWeight ?? DEFAULT_USER_HISTORY_WEIGHT, - blockThreshold: config?.blockThreshold ?? DEFAULT_BLOCK_THRESHOLD, - flagThreshold: config?.flagThreshold ?? DEFAULT_FLAG_THRESHOLD, - evaluationTimeout: config?.evaluationTimeout ?? DEFAULT_EVALUATION_TIMEOUT, - fallbackOnTimeout: config?.fallbackOnTimeout ?? DEFAULT_FALLBACK_ON_TIMEOUT, - fallbackDecision: config?.fallbackDecision ?? DEFAULT_FALLBACK_DECISION, - }; - this.reputationService = reputationService; - this.ruleEngine = ruleEngine; - } - - async evaluate(context: DecisionContext): Promise { - const startTime = Date.now(); - const reqId = context.requestId ?? 'unknown'; - const fallback: DecisionResult = { - decision: this.config.fallbackDecision, - confidence: 0.5, - reasons: ['Fallback decision due to evaluation timeout'], - fallbackDecision: this.config.fallbackDecision, - scoring: { - reputationScore: 0.5, - ruleScore: 0.5, - behavioralScore: 0.5, - userHistoryScore: 0.5, - totalScore: 0.5, - }, - executedAt: new Date(), - requestId: reqId, - }; - - const evaluation = (async () => { - const [reputationScore, ruleScore, behavioralScore, userHistoryScore] = await Promise.all([ - this.calculateReputationScore(context.cachedReputation), - this.calculateRuleScore(context.ruleMatches), - this.calculateBehavioralScore(context), - this.calculateUserHistoryScore(context.userHistory), - ]); - - const totalScore = - reputationScore * this.config.reputationWeight + - ruleScore * this.config.ruleWeight + - behavioralScore * this.config.behavioralWeight + - userHistoryScore * this.config.userHistoryWeight; - - const decision = this.applyThresholds(totalScore); - const reasons = this.collectReasons( - reputationScore, ruleScore, behavioralScore, userHistoryScore, context.ruleMatches - ); - - return { - decision, - confidence: totalScore, - reasons, - fallbackDecision: this.config.fallbackDecision, - scoring: { - reputationScore, - ruleScore, - behavioralScore, - userHistoryScore, - totalScore, - }, - executedAt: new Date(), - requestId: reqId, - }; - })(); - - try { - const result = await Promise.race([ - evaluation, - new Promise((resolve) => { - setTimeout(() => { - console.log(`[DecisionEngine] [${reqId}] Evaluation timeout after ${this.config.evaluationTimeout}ms`); - resolve(fallback); - }, this.config.evaluationTimeout); - }), - ]); - - return result; - } catch (error) { - console.error(`[DecisionEngine] [${reqId}] Evaluation error:`, error); - - if (this.config.fallbackOnTimeout) { - return { ...fallback, reasons: ['Fallback decision due to evaluation error'] }; - } - - throw error; - } - } - - private async calculateReputationScore(reputation: ReputationResult): Promise { - return reputation.score; - } - - private async calculateRuleScore(ruleMatches: RuleMatch[]): Promise { - if (ruleMatches.length === 0) { - return 0; - } - - const totalScore = ruleMatches.reduce((sum, match) => sum + match.score, 0); - return Math.min(totalScore, 1.0); - } - - private async calculateBehavioralScore(context: DecisionContext): Promise { - let score = 0; - - if (context.callMetadata) { - const { callMetadata } = context; - - if (callMetadata.duration && callMetadata.duration < 5) { - score += SHORT_CALL_SCORE; - } - - if (callMetadata.callType === 'sms') { - score += SHORT_SMS_SCORE; - } - } - - if (context.smsContent) { - const { smsContent } = context; - - if (smsContent.body.length < 10) { - score += SHORT_CONTENT_SCORE; - } - - if (/\b(URGENT|ACT NOW|LIMITED)\b/i.test(smsContent.body)) { - score += URGENT_KEYWORD_SCORE; - } - } - - return Math.min(score, 1.0); - } - - private async calculateUserHistoryScore(userHistory?: UserSpamHistory): Promise { - if (!userHistory) { - return 0.5; - } - - const totalReports = userHistory.spamCount + userHistory.hamCount; - if (totalReports === 0) { - return 0.5; - } - - const spamRatio = userHistory.spamCount / totalReports; - - if (userHistory.userPreference) { - switch (userHistory.userPreference) { - case 'block': - return 1.0; - case 'flag': - return 0.6; - case 'allow': - return 0.2; - } - } - - return spamRatio; - } - - private applyThresholds(score: number): 'BLOCK' | 'FLAG' | 'ALLOW' { - if (score >= this.config.blockThreshold) { - return 'BLOCK'; - } - if (score >= this.config.flagThreshold) { - return 'FLAG'; - } - return 'ALLOW'; - } - - private collectReasons( - reputationScore: number, - ruleScore: number, - behavioralScore: number, - userHistoryScore: number, - ruleMatches: RuleMatch[] - ): string[] { - const reasons: string[] = []; - - if (reputationScore > 0.8) { - reasons.push(`High reputation spam score: ${reputationScore.toFixed(2)}`); - } - - if (ruleMatches.length > 0) { - reasons.push(`Matched ${ruleMatches.length} spam rule(s)`); - ruleMatches.forEach(match => { - reasons.push(` - ${match.ruleName} (${match.score.toFixed(2)})`); - }); - } - - if (behavioralScore > 0.5) { - reasons.push(`Suspicious behavioral pattern detected`); - } - - if (userHistoryScore > 0.7) { - reasons.push(`User history indicates high spam probability`); - } - - if (reasons.length === 0) { - reasons.push('No spam indicators detected'); - } - - return reasons; - } - - getConfig(): Required { - return { ...this.config }; - } - - updateConfig(config: Partial): void { - this.config.reputationWeight = config.reputationWeight ?? this.config.reputationWeight; - this.config.ruleWeight = config.ruleWeight ?? this.config.ruleWeight; - this.config.behavioralWeight = config.behavioralWeight ?? this.config.behavioralWeight; - this.config.userHistoryWeight = config.userHistoryWeight ?? this.config.userHistoryWeight; - this.config.blockThreshold = config.blockThreshold ?? this.config.blockThreshold; - this.config.flagThreshold = config.flagThreshold ?? this.config.flagThreshold; - this.config.evaluationTimeout = config.evaluationTimeout ?? this.config.evaluationTimeout; - this.config.fallbackOnTimeout = config.fallbackOnTimeout ?? this.config.fallbackOnTimeout; - this.config.fallbackDecision = config.fallbackDecision ?? this.config.fallbackDecision; - } -} diff --git a/services/spamshield/src/engine/index.ts b/services/spamshield/src/engine/index.ts deleted file mode 100644 index 7faa431..0000000 --- a/services/spamshield/src/engine/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './decision-engine'; -export * from './rule-engine'; diff --git a/services/spamshield/src/engine/rule-engine.ts b/services/spamshield/src/engine/rule-engine.ts deleted file mode 100644 index e28c0ba..0000000 --- a/services/spamshield/src/engine/rule-engine.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { PrismaClient, SpamRule } from '@prisma/client'; -import { generateRequestId } from '@shieldai/types'; -import { validateRegexPattern, RegexValidationError } from '../utils/regex-validation'; - -export interface CompiledRule { - rule: SpamRule; - compiledPattern: RegExp; - compiledCaseInsensitive?: RegExp; -} - -export interface RuleMatch { - ruleId: string; - ruleName: string; - pattern: string; - score: number; - priority: 'high' | 'medium' | 'low'; - matchedAt: Date; -} - -export interface RuleEngineConfig { - loadIntervalMs?: number; - enableCache?: boolean; - cacheTtlMs?: number; -} - -const DEFAULT_CONFIG: Required = { - loadIntervalMs: 60000, - enableCache: true, - cacheTtlMs: 300000, -}; - -export class RuleEngine { - private readonly config: Required; - private numberPatternRules: CompiledRule[] = []; - private behavioralRules: CompiledRule[] = []; - private contentRules: CompiledRule[] = []; - private allRules: CompiledRule[] = []; - private lastLoadTime: Date | null = null; - private readonly prisma: PrismaClient; - - constructor(prisma?: PrismaClient, config?: RuleEngineConfig) { - this.prisma = prisma ?? new PrismaClient() as PrismaClient; - this.config = { ...DEFAULT_CONFIG, ...config }; - } - - async loadActiveRules(): Promise { - const now = new Date(); - - if (this.config.enableCache && this.lastLoadTime) { - const elapsed = now.getTime() - this.lastLoadTime.getTime(); - if (elapsed < this.config.loadIntervalMs) { - return; - } - } - - const rules = await this.prisma.spamRule.findMany({ - where: { isActive: true }, - orderBy: { priority: 'desc' }, - }); - - const compiledRules: CompiledRule[] = []; - for (const rule of rules) { - try { - validateRegexPattern(rule.pattern); - const compiledPattern = new RegExp(rule.pattern); - const compiledCaseInsensitive = new RegExp(rule.pattern, 'i'); - compiledRules.push({ - rule, - compiledPattern, - compiledCaseInsensitive, - }); - } catch (error) { - if (error instanceof RegexValidationError) { - console.warn(`[RuleEngine] [req:${generateRequestId()}] Rule "${rule.name}" (${rule.id}) ReDoS risk: ${error.reason}, skipping`); - } else { - console.error(`[RuleEngine] [req:${generateRequestId()}] Unexpected error validating rule "${rule.name}" (${rule.id}):`, error); - } - } - } - - this.allRules = compiledRules; - this.numberPatternRules = compiledRules.filter(r => (r.rule as any).category === 'number_pattern'); - this.behavioralRules = compiledRules.filter(r => (r.rule as any).category === 'behavioral'); - this.contentRules = compiledRules.filter(r => (r.rule as any).category === 'content'); - this.lastLoadTime = now; - } - - async evaluate(phoneNumber: string): Promise { - if (this.allRules.length === 0) { - await this.loadActiveRules(); - } - - const matches: RuleMatch[] = []; - - for (const compiled of this.allRules) { - try { - if (compiled.compiledPattern.test(phoneNumber)) { - matches.push({ - ruleId: compiled.rule.id, - ruleName: compiled.rule.name, - pattern: compiled.rule.pattern, - score: (compiled.rule as any).score, - priority: (compiled.rule as any).priority as 'high' | 'medium' | 'low', - matchedAt: new Date(), - }); - } - } catch (error) { - console.error(`[RuleEngine] [req:${generateRequestId()}] Evaluation error for rule ${compiled.rule.id}:`, error); - } - } - - return matches.sort((a, b) => b.score - a.score); - } - - async evaluateSms(smsBody: string): Promise { - if (this.contentRules.length === 0) { - await this.loadActiveRules(); - } - - const matches: RuleMatch[] = []; - - for (const compiled of this.contentRules) { - try { - if (compiled.compiledCaseInsensitive!.test(smsBody)) { - matches.push({ - ruleId: compiled.rule.id, - ruleName: compiled.rule.name, - pattern: compiled.rule.pattern, - score: (compiled.rule as any).score, - priority: (compiled.rule as any).priority as 'high' | 'medium' | 'low', - matchedAt: new Date(), - }); - } - } catch (error) { - console.error(`[RuleEngine] [req:${generateRequestId()}] SMS evaluation error for rule ${compiled.rule.id}:`, error); - } - } - - return matches.sort((a, b) => b.score - a.score); - } - - getNumberPatternRules(): SpamRule[] { - return this.numberPatternRules.map(r => r.rule); - } - - getBehavioralRules(): SpamRule[] { - return this.behavioralRules.map(r => r.rule); - } - - getContentRules(): SpamRule[] { - return this.contentRules.map(r => r.rule); - } - - getAllRules(): SpamRule[] { - return this.allRules.map(r => r.rule); - } - - async refreshRules(): Promise { - this.lastLoadTime = null; - await this.loadActiveRules(); - } - - clearCache(): void { - this.allRules = []; - this.numberPatternRules = []; - this.behavioralRules = []; - this.contentRules = []; - this.lastLoadTime = null; - } - - getConfig(): Required { - return { ...this.config }; - } -} diff --git a/services/spamshield/src/feature-flags.ts b/services/spamshield/src/feature-flags.ts deleted file mode 100644 index 5c72e6a..0000000 --- a/services/spamshield/src/feature-flags.ts +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Feature Flag Management System - * Centralized feature flag handling with type safety and runtime updates - */ - -import type { z } from 'zod'; - -/** - * Type for feature flag values - */ -export type FeatureFlagValue = boolean | string | number; - -/** - * Interface for a feature flag definition - */ -export interface FeatureFlag { - key: string; - defaultValue: T; - description?: string; - allowedValues?: T[]; // For enum-like flags - category?: string; -} - -/** - * Feature flag registry - stores all defined flags - */ -export interface FeatureFlagRegistry { - [key: string]: FeatureFlag; -} - -/** - * Feature flag resolver - handles flag resolution logic - */ -export class FeatureFlagResolver { - private flags: FeatureFlagRegistry; - private resolvedCache: Map = new Map(); - - constructor(flags: FeatureFlagRegistry) { - this.flags = flags; - } - - /** - * Resolve a feature flag value - * Priority: Environment > Cache > Default - */ - resolve(key: string, defaultValue: T): T { - // Check cache first - if (this.resolvedCache.has(key)) { - return this.resolvedCache.get(key)! as T; - } - - // Check environment variable (allows runtime updates) - const envValue = process.env[`FLAG_${key.toUpperCase()}`]; - if (envValue !== undefined) { - // Try to parse as JSON first, then as boolean, then as string - let parsed: FeatureFlagValue; - try { - parsed = JSON.parse(envValue); - } catch { - parsed = envValue.toLowerCase() === 'true' ? true : - envValue.toLowerCase() === 'false' ? false : - envValue; - } - - // Validate against allowed values if defined - const flag = this.flags[key]; - if (flag && flag.allowedValues && !flag.allowedValues.includes(parsed)) { - console.warn(`Invalid value for flag ${key}: ${parsed}. Using default.`); - parsed = defaultValue as FeatureFlagValue; - } - - this.resolvedCache.set(key, parsed); - return parsed as T; - } - - // Use cached value if available - if (this.resolvedCache.has(key)) { - return this.resolvedCache.get(key)! as T; - } - - // Return default - this.resolvedCache.set(key, defaultValue as FeatureFlagValue); - return defaultValue as T; - } - - /** - * Check if a flag is enabled (boolean check) - */ - isEnabled(key: string, defaultValue: T): T { - return this.resolve(key, defaultValue) as T; - } - - /** - * Get flag definition - */ - getDefinition(key: string): FeatureFlag | undefined { - return this.flags[key]; - } - - /** - * List all registered flags - */ - getAllFlags(): FeatureFlagRegistry { - return { ...this.flags }; - } - - /** - * Clear the resolution cache (useful for testing) - */ - clearCache(): void { - this.resolvedCache.clear(); - } -} - -/** - * Feature flag configuration with pre-defined flags - */ -export const featureFlags: FeatureFlagRegistry = { - // SpamShield Feature Flags - 'spamshield.enable.number.reputation': { - key: 'spamshield_enable_number_reputation', - defaultValue: true, - description: 'Enable number reputation checking (Hiya API integration)', - category: 'spamshield', - }, - 'spamshield.enable.content.classification': { - key: 'spamshield_enable_content_classification', - defaultValue: true, - description: 'Enable SMS content classification (BERT model)', - category: 'spamshield', - }, - 'spamshield.enable.behavioral.analysis': { - key: 'spamshield_enable_behavioral_analysis', - defaultValue: true, - description: 'Enable call behavioral analysis', - category: 'spamshield', - }, - 'spamshield.enable.community.intelligence': { - key: 'spamshield_enable_community_intelligence', - defaultValue: true, - description: 'Enable community intelligence sharing', - category: 'spamshield', - }, - 'spamshield.enable.real.time.blocking': { - key: 'spamshield_enable_real_time_blocking', - defaultValue: true, - description: 'Enable real-time spam blocking', - category: 'spamshield', - }, - 'spamshield.enable.multiple.sources': { - key: 'spamshield_enable_multiple_sources', - defaultValue: false, - description: 'Enable multiple reputation source aggregation (Truecaller, etc.)', - category: 'spamshield', - }, - 'spamshield.enable.ml.classifier': { - key: 'spamshield_enable_ml_classifier', - defaultValue: false, - description: 'Enable ML-based spam classification', - category: 'spamshield', - }, - - // VoicePrint Feature Flags - 'voiceprint.enable.ml.service': { - key: 'voiceprint_enable_ml_service', - defaultValue: false, - description: 'Enable ML service integration for voice analysis', - category: 'voiceprint', - }, - 'voiceprint.enable.faiss.index': { - key: 'voiceprint_enable_faiss_index', - defaultValue: true, - description: 'Enable FAISS index for voice matching', - category: 'voiceprint', - }, - 'voiceprint.enable.batch.analysis': { - key: 'voiceprint_enable_batch_analysis', - defaultValue: true, - description: 'Enable batch voice analysis', - category: 'voiceprint', - }, - 'voiceprint.enable.realtime.analysis': { - key: 'voiceprint_enable_realtime_analysis', - defaultValue: false, - description: 'Enable real-time voice analysis', - category: 'voiceprint', - }, - 'voiceprint.enable.mock.model': { - key: 'voiceprint_enable_mock_model', - defaultValue: true, - description: 'Enable mock model for development', - category: 'voiceprint', - }, - - // General Platform Flags - 'platform.enable.audit.logs': { - key: 'platform_enable_audit_logs', - defaultValue: true, - description: 'Enable comprehensive audit logging', - category: 'platform', - }, - 'platform.enable.kpi.tracking': { - key: 'platform_enable_kpi_tracking', - defaultValue: true, - description: 'Enable KPI snapshot tracking', - category: 'platform', - }, -}; - -/** - * Create a resolver instance with the default flags - */ -export const featureFlagResolver = new FeatureFlagResolver(featureFlags); - -/** - * Convenience function for quick flag checks - */ -export function isFeatureEnabled(key: string, defaultValue: T): T { - return featureFlagResolver.isEnabled(key, defaultValue); -} - -/** - * Check if a flag is enabled with type safety - */ -export function checkFlag(key: string, defaultValue: T): T { - return featureFlagResolver.resolve(key, defaultValue); -} diff --git a/services/spamshield/src/index.ts b/services/spamshield/src/index.ts deleted file mode 100644 index 95638a2..0000000 --- a/services/spamshield/src/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './services/spamshield.service'; -export * from './services/mixpanel.service'; -export * from './circuit-breaker'; -export * from './config/spamshield.config'; -export * from './utils/phone-validation'; -export * from './carriers'; -export * from './engine'; -export * from './websocket'; -export * from './classifier/sms-classifier'; diff --git a/services/spamshield/src/middleware/spam-rate-limit.middleware.ts b/services/spamshield/src/middleware/spam-rate-limit.middleware.ts deleted file mode 100644 index 9d67d24..0000000 --- a/services/spamshield/src/middleware/spam-rate-limit.middleware.ts +++ /dev/null @@ -1,177 +0,0 @@ -import { RedisService } from '@shieldai/shared-notifications'; -import { TierRateLimits, SubscriptionTier, spamRateLimits } from '../config/spamshield.config'; - -export interface RateLimitStatus { - exceeded: boolean; - limit: number; - remaining: number; - resetAt: Date; - retryAfterSeconds: number; -} - -export interface RateLimitOptions { - windowMs?: number; - dailyWindowMs?: number; -} - -export class SpamRateLimitMiddleware { - private redisService: RedisService; - private options: RateLimitOptions; - - constructor(redisService: RedisService, options?: RateLimitOptions) { - this.redisService = redisService; - this.options = { - windowMs: options?.windowMs || 60000, - dailyWindowMs: options?.dailyWindowMs || 86400000, - }; - } - - private getMinuteKey(userId: string, tier: SubscriptionTier): string { - const windowMs = this.options.windowMs ?? 60000; - const minuteTimestamp = Math.floor(Date.now() / windowMs); - return `ratelimit:spam:${userId}:${tier}:min:${minuteTimestamp}`; - } - - private getDayKey(userId: string, tier: SubscriptionTier): string { - const date = new Date().toISOString().split('T')[0]; - return `ratelimit:spam:${userId}:${tier}:day:${date}`; - } - - private getResetTime(windowMs: number): Date { - const now = Date.now(); - const resetTimestamp = Math.ceil(now / windowMs) * windowMs; - return new Date(resetTimestamp); - } - - async checkLimit( - userId: string, - tier: SubscriptionTier, - ): Promise { - const tierLimits = spamRateLimits[tier]; - const minuteKey = this.getMinuteKey(userId, tier); - const dayKey = this.getDayKey(userId, tier); - - try { - const [minuteCount, dayCount] = await Promise.all([ - this.redisService.getCounter(minuteKey), - this.redisService.getCounter(dayKey), - ]); - - const minuteExceeded = minuteCount >= tierLimits.perMinute; - const dayExceeded = dayCount >= tierLimits.perDay; - const exceeded = minuteExceeded || dayExceeded; - - const effectiveLimit = exceeded - ? Math.min(tierLimits.perMinute, tierLimits.perDay) - : Math.min(tierLimits.perMinute, tierLimits.perDay); - - const effectiveCount = exceeded - ? Math.min(minuteCount, dayCount) - : Math.min(minuteCount, dayCount); - - const windowMs = this.options.windowMs ?? 60000; - return { - exceeded, - limit: effectiveLimit, - remaining: Math.max(0, effectiveLimit - effectiveCount), - resetAt: this.getResetTime(windowMs), - retryAfterSeconds: Math.ceil( - (this.getResetTime(windowMs).getTime() - Date.now()) / 1000, - ), - }; - } catch (error) { - console.error('[SpamRateLimit] Redis error:', error); - const windowMs = this.options.windowMs ?? 60000; - return { - exceeded: false, - limit: tierLimits.perMinute, - remaining: tierLimits.perMinute, - resetAt: this.getResetTime(windowMs), - retryAfterSeconds: windowMs / 1000, - }; - } - } - - async incrementCounter( - userId: string, - tier: SubscriptionTier, - ): Promise<{ minuteCount: number; dayCount: number }> { - const minuteKey = this.getMinuteKey(userId, tier); - const dayKey = this.getDayKey(userId, tier); - - try { - const windowMs = this.options.windowMs ?? 60000; - const dailyWindowMs = this.options.dailyWindowMs ?? 86400000; - const [minuteCount, dayCount] = await Promise.all([ - this.redisService.increment(minuteKey, Math.ceil(windowMs / 1000)), - this.redisService.increment(dayKey, Math.ceil(dailyWindowMs / 1000)), - ]); - - return { minuteCount, dayCount }; - } catch (error) { - console.error('[SpamRateLimit] Increment error:', error); - return { minuteCount: 0, dayCount: 0 }; - } - } - - async checkAndIncrement( - userId: string, - tier: SubscriptionTier, - ): Promise<{ allowed: boolean; status: RateLimitStatus }> { - const status = await this.checkLimit(userId, tier); - - if (!status.exceeded) { - await this.incrementCounter(userId, tier); - const updatedStatus = await this.checkLimit(userId, tier); - return { allowed: true, status: updatedStatus }; - } - - return { allowed: false, status }; - } - - async getUsage(userId: string, tier: SubscriptionTier): Promise<{ - minuteUsed: number; - minuteLimit: number; - minuteRemaining: number; - dayUsed: number; - dayLimit: number; - dayRemaining: number; - }> { - const tierLimits = spamRateLimits[tier]; - const minuteKey = this.getMinuteKey(userId, tier); - const dayKey = this.getDayKey(userId, tier); - - try { - const [minuteCount, dayCount] = await Promise.all([ - this.redisService.getCounter(minuteKey), - this.redisService.getCounter(dayKey), - ]); - - return { - minuteUsed: minuteCount, - minuteLimit: tierLimits.perMinute, - minuteRemaining: Math.max(0, tierLimits.perMinute - minuteCount), - dayUsed: dayCount, - dayLimit: tierLimits.perDay, - dayRemaining: Math.max(0, tierLimits.perDay - dayCount), - }; - } catch (error) { - console.error('[SpamRateLimit] Usage fetch error:', error); - return { - minuteUsed: 0, - minuteLimit: tierLimits.perMinute, - minuteRemaining: tierLimits.perMinute, - dayUsed: 0, - dayLimit: tierLimits.perDay, - dayRemaining: tierLimits.perDay, - }; - } - } -} - -export function createSpamRateLimitMiddleware( - redisService: RedisService, - options?: RateLimitOptions, -): SpamRateLimitMiddleware { - return new SpamRateLimitMiddleware(redisService, options); -} diff --git a/services/spamshield/src/services/mixpanel.service.ts b/services/spamshield/src/services/mixpanel.service.ts deleted file mode 100644 index fb5648a..0000000 --- a/services/spamshield/src/services/mixpanel.service.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { mixpanelService, EventType } from '@shieldsai/shared-analytics'; -import { FieldEncryptionService } from '@shieldai/db'; - -export interface SpamBlockedEvent { - phoneNumber: string; - decision: 'BLOCK' | 'FLAG'; - confidence: number; - ruleMatches?: string[]; - timestamp: Date; -} - -export interface MixpanelEventProperties { - $event_name: string; - phoneNumberHash: string; - decision: 'BLOCK' | 'FLAG'; - confidence: number; - ruleMatches?: string[]; - timestamp: string; - [key: string]: any; -} - -export interface MixpanelConfig { - token: string; - apiHost: string; - enableLogging?: boolean; -} - -const DEFAULT_CONFIG: Required = { - token: process.env.MIXPANEL_TOKEN || '', - apiHost: 'api.mixpanel.com', - enableLogging: true, -}; - -/** - * SpamShield analytics adapter. - * Delegates to the shared MixpanelService for consistent event tracking - * across the ShieldAI platform, while maintaining spam-specific interfaces. - * - * @deprecated Use {@link @shieldsai/shared-analytics#MixpanelService} directly - * for new analytics code. This wrapper maintains backward compatibility. - */ -export class MixpanelService { - private readonly config: Required; - private readonly events: MixpanelEventProperties[] = []; - - constructor(config?: MixpanelConfig) { - this.config = { ...DEFAULT_CONFIG, ...config }; - } - - async spamBlocked(event: SpamBlockedEvent): Promise { - const phoneNumberHash = FieldEncryptionService.hashPhoneNumber(event.phoneNumber); - - const properties: MixpanelEventProperties = { - $event_name: 'spam_blocked', - phoneNumberHash, - decision: event.decision, - confidence: event.confidence, - ruleMatches: event.ruleMatches, - timestamp: event.timestamp.toISOString(), - }; - - this.events.push(properties); - - if (this.config.enableLogging) { - console.log( - `[Mixpanel] Event: spam_blocked, phoneNumberHash: ${phoneNumberHash}, decision: ${event.decision}` - ); - } - - await mixpanelService.track(EventType.SPAM_BLOCKED, properties.phoneNumberHash, { - decision: event.decision, - confidence: event.confidence, - ruleMatches: event.ruleMatches, - timestamp: event.timestamp, - }); - - return properties; - } - - async track(eventName: string, properties: Record): Promise> { - const mpEvent = Object.values(EventType).find(e => e === eventName) as EventType | undefined; - - if (mpEvent) { - await mixpanelService.track(mpEvent, properties.phoneNumberHash || 'anonymous', properties); - return { status: 200 }; - } - - const url = `https://${this.config.apiHost}/track`; - - const payload = { - event: eventName, - properties: { - ...properties, - $token: this.config.token, - time: properties.timestamp ? new Date(properties.timestamp).getTime() / 1000 : Date.now() / 1000, - }, - }; - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ data: JSON.stringify(payload) }), - }); - - if (!response.ok) { - if (this.config.enableLogging) { - console.log(`[Mixpanel] Track failed: ${response.status} ${response.statusText}`); - } - } - - return { status: response.status }; - } catch (error) { - if (this.config.enableLogging) { - console.log(`[Mixpanel] Track error:`, error); - } - return { status: 0, error: String(error) }; - } - } - - getEvents(): MixpanelEventProperties[] { - return [...this.events]; - } - - clearEvents(): void { - this.events.length = 0; - } - - getConfig(): Required { - return { ...this.config }; - } -} diff --git a/services/spamshield/src/services/spamshield.service.ts b/services/spamshield/src/services/spamshield.service.ts deleted file mode 100644 index da578ac..0000000 --- a/services/spamshield/src/services/spamshield.service.ts +++ /dev/null @@ -1,602 +0,0 @@ -import { PrismaClient, SpamFeedback, SpamRule, SpamAuditLog } from '@prisma/client'; -import { FieldEncryptionService } from '@shieldai/db'; -import { generateRequestId } from '@shieldai/types'; -import { emitSpamShieldAlert } from '@shieldai/correlation'; -import { spamConfig, spamFeatureFlags, metadataLimits } from '../config/spamshield.config'; -import { CircuitBreaker, CircuitBreakerError, CircuitState, CircuitBreakerMetrics } from '../circuit-breaker'; -import { validatePhoneNumber as validateE164 } from '../utils/phone-validation'; -import { CarrierApi, CarrierCall, CarrierSms, CarrierDecision } from '../carriers/carrier-types'; -import { CarrierFactory, CarrierType } from '../carriers/carrier-factory'; -import { DecisionEngine, DecisionContext, DecisionResult } from '../engine/decision-engine'; -import { RuleEngine, RuleMatch } from '../engine/rule-engine'; -import { AlertServer, AlertEvent } from '../websocket/alert-server'; -import { BertSmsClassifier, SmsClassificationResult } from '../classifier/sms-classifier'; - -const prisma = new PrismaClient() as PrismaClient & { - spamFeedback: { - create: (data: { data: SpamFeedback }) => Promise; - }; - spamRule: { - findMany: (args: { where: { isActive: boolean } }) => Promise; - }; - spamAuditLog: { - create: (data: { data: SpamAuditLog }) => Promise; - }; -}; - -interface InitializationLock { - promise: Promise; - resolved: boolean; -} - -export interface ReputationResult { - score: number; - isSpam: boolean; - source: 'hiya' | 'truecaller' | 'combined' | 'fallback'; - hiyaScore?: number; - truecallerScore?: number; -} - -export interface CircuitMetrics { - hiya: CircuitBreakerMetrics; - truecaller: CircuitBreakerMetrics; -} - -export interface IncomingCall { - callId: string; - phoneNumber: string; - from: string; - to: string; - startTime: Date; - direction: 'inbound' | 'outbound'; - carrierType: CarrierType; - carrierSid: string; - requestId?: string; -} - -export interface IncomingSms { - messageId: string; - phoneNumber: string; - from: string; - to: string; - body: string; - timestamp: Date; - direction: 'inbound' | 'outbound'; - carrierType: CarrierType; - carrierSid: string; - requestId?: string; -} - -export class SpamShieldService { - private static instance: SpamShieldService; - private initLock: InitializationLock | null = null; - private hiyaBreaker: CircuitBreaker = new CircuitBreaker({ - failureThreshold: spamConfig.circuitBreakerThreshold, - timeout: spamConfig.circuitBreakerTimeout, - }); - private truecallerBreaker: CircuitBreaker = new CircuitBreaker({ - failureThreshold: spamConfig.circuitBreakerThreshold, - timeout: spamConfig.circuitBreakerTimeout, - }); - - // Carrier integration - private carrierFactory?: CarrierFactory; - - // Decision engine - private decisionEngine?: DecisionEngine; - private ruleEngine?: RuleEngine; - - // WebSocket alert server - private alertServer?: AlertServer; - - // SMS Classifier - private smsClassifier?: BertSmsClassifier; - - private constructor() {} - - static getInstance(): SpamShieldService { - if (!SpamShieldService.instance) { - SpamShieldService.instance = new SpamShieldService(); - } - return SpamShieldService.instance; - } - - async initialize(): Promise { - if (this.initLock?.resolved) return; - - if (!this.initLock) { - this.initLock = { - promise: this._initialize(), - resolved: false, - }; - } - - await this.initLock.promise; - } - - private async _initialize(): Promise { - this.hiyaBreaker = new CircuitBreaker({ - failureThreshold: spamConfig.circuitBreakerThreshold, - timeout: spamConfig.circuitBreakerTimeout, - onStateChange: (state: CircuitState, previous: CircuitState) => { - console.log(`[SpamShield] [req:${generateRequestId()}] Hiya circuit: ${previous} -> ${state}`); - }, - }); - this.truecallerBreaker = new CircuitBreaker({ - failureThreshold: spamConfig.circuitBreakerThreshold, - timeout: spamConfig.circuitBreakerTimeout, - onStateChange: (state: CircuitState, previous: CircuitState) => { - console.log(`[SpamShield] [req:${generateRequestId()}] Truecaller circuit: ${previous} -> ${state}`); - }, - }); - - // Initialize SMS Classifier with feature flag check - if (spamFeatureFlags.enableSMSClassification) { - this.smsClassifier = new BertSmsClassifier(this); - console.log(`[SpamShield] [req:${generateRequestId()}] SMS Classifier initialized`); - } else { - console.log(`[SpamShield] [req:${generateRequestId()}] SMS Classification disabled via feature flag`); - } - - this.initLock!.resolved = true; - } - - async checkReputation(phoneNumber: string): Promise { - const validated = this.validatePhoneNumber(phoneNumber); - - const results = await Promise.allSettled([ - this.fetchHiyaReputation(validated), - this.fetchTruecallerReputation(validated), - ]); - - const hiyaResult = results[0]; - const truecallerResult = results[1]; - - const hiyaScore = hiyaResult.status === 'fulfilled' ? hiyaResult.value : undefined; - const truecallerScore = truecallerResult.status === 'fulfilled' ? truecallerResult.value : undefined; - - if (hiyaScore !== undefined && truecallerScore !== undefined) { - const combinedScore = (hiyaScore + truecallerScore) / 2; - const isSpam = combinedScore > spamConfig.defaultConfidenceThreshold; - return { - score: combinedScore, - isSpam, - source: 'combined', - hiyaScore, - truecallerScore, - }; - } - - if (hiyaScore !== undefined) { - return { - score: hiyaScore, - isSpam: hiyaScore > spamConfig.defaultConfidenceThreshold, - source: 'hiya', - hiyaScore, - }; - } - - if (truecallerScore !== undefined) { - return { - score: truecallerScore, - isSpam: truecallerScore > spamConfig.defaultConfidenceThreshold, - source: 'truecaller', - truecallerScore, - }; - } - - return { - score: 0, - isSpam: false, - source: 'fallback', - }; - } - - async analyzeCall(phoneNumber: string, callTimestamp: Date): Promise<{ - decision: 'BLOCK' | 'FLAG' | 'ALLOW'; - confidence: number; - ruleMatches: string[]; - }> { - if (!spamFeatureFlags.enableCallAnalysis) { - throw new Error('Call analysis disabled via feature flag'); - } - - const validated = this.validatePhoneNumber(phoneNumber); - const matches = this.ruleEngine - ? await this.ruleEngine.evaluate(validated) - : []; - - const ruleMatchIds = matches.map(m => m.ruleId); - const confidence = Math.min(matches.reduce((sum, m) => sum + m.score, 0), 1.0); - const decision = confidence > 0.8 ? 'BLOCK' : confidence > 0.5 ? 'FLAG' : 'ALLOW'; - - const encrypted = FieldEncryptionService.encrypt(validated); - - const auditLog = await prisma.spamAuditLog.create({ - data: { - userId: 'system', - phoneNumber: encrypted, - decision: decision as any, - reason: `Rule-based analysis`, - ruleId: ruleMatchIds[0], - }, - }); - - if (decision === 'BLOCK' || decision === 'FLAG') { - emitSpamShieldAlert( - 'system', - auditLog.id, - validated, - decision, - confidence, - ruleMatchIds - ).catch((err) => console.error(`[Correlation] SpamShield emit failed:`, err)); - } - - return { decision, confidence, ruleMatches: ruleMatchIds }; - } - - async recordFeedback( - userId: string, - phoneNumber: string, - isSpam: boolean, - label?: string, - metadata?: Record - ): Promise { - if (!spamFeatureFlags.enableFeedbackLoop) { - throw new Error('Feedback loop disabled via feature flag'); - } - - if (!userId || typeof userId !== 'string' || userId.trim().length === 0) { - throw new Error('Feedback: userId is required'); - } - - if (!phoneNumber || typeof phoneNumber !== 'string') { - throw new Error('Feedback: phoneNumber must be a non-empty string'); - } - - if (typeof isSpam !== 'boolean') { - throw new Error('Feedback: isSpam must be a boolean'); - } - - const validated = this.validatePhoneNumber(phoneNumber); - const encrypted = FieldEncryptionService.encrypt(validated); - const hash = FieldEncryptionService.hashPhoneNumber(validated); - - const validatedMetadata = metadata - ? this.validateMetadata(metadata) - : { source: 'user_feedback' }; - - await prisma.spamFeedback.create({ - data: { - userId, - phoneNumber: encrypted, - phoneNumberHash: hash, - isSpam, - label, - metadata: JSON.stringify(validatedMetadata), - }, - }); - } - - getCircuitMetrics(): CircuitMetrics { - return { - hiya: this.hiyaBreaker.getMetrics(), - truecaller: this.truecallerBreaker.getMetrics(), - }; - } - - resetCircuits(): void { - this.hiyaBreaker.reset(); - this.truecallerBreaker.reset(); - } - - // Carrier integration methods - initializeCarrierFactory(config: Parameters[0]): void { - this.carrierFactory = new CarrierFactory(config); - } - - getCarrierFactory(): CarrierFactory | undefined { - return this.carrierFactory; - } - - async executeCarrierAction( - carrierType: CarrierType, - action: 'block' | 'flag' | 'allow', - phoneNumber: string, - sid: string, - isSms: boolean = false - ): Promise { - if (!this.carrierFactory) { - throw new Error('Carrier factory not initialized'); - } - - const carrier = this.carrierFactory.createCarrier(carrierType); - - if (isSms) { - switch (action) { - case 'block': - await carrier.blockSms(sid); - break; - case 'flag': - await carrier.flagSms(sid); - break; - case 'allow': - await carrier.allowSms(sid); - break; - } - } else { - switch (action) { - case 'block': - await carrier.blockCall(sid); - break; - case 'flag': - await carrier.flagCall(sid); - break; - case 'allow': - await carrier.allowCall(sid); - break; - } - } - - await this.logCarrierAction(phoneNumber, action, carrierType, sid, isSms); - } - - // Decision engine integration - initializeDecisionEngine( - ruleEngine: RuleEngine, - config?: Parameters[2] - ): void { - this.ruleEngine = ruleEngine; - this.decisionEngine = new DecisionEngine(this, ruleEngine, config); - } - - getDecisionEngine(): DecisionEngine | undefined { - return this.decisionEngine; - } - - async makeRealTimeDecision( - phoneNumber: string, - context: Omit - ): Promise { - if (!this.decisionEngine) { - throw new Error('Decision engine not initialized'); - } - - const reputation = await this.checkReputation(phoneNumber); - const result = this.decisionEngine.evaluate({ - phoneNumber, - cachedReputation: reputation, - ...context, - }); - - return result; - } - - // WebSocket alert server integration - initializeAlertServer(config?: Parameters[0]): void { - this.alertServer = new AlertServer(config); - } - - getAlertServer(): AlertServer | undefined { - return this.alertServer; - } - - async broadcastDecision(phoneNumber: string, decision: DecisionResult): Promise { - if (!this.alertServer) { - console.log(`[SpamShield] [req:${decision.requestId ?? 'unknown'}] Alert server not initialized, skipping broadcast`); - return; - } - - await this.alertServer.broadcastDecision(phoneNumber, decision); - } - - // SMS Classification Service - async classifySms(text: string): Promise { - if (!spamFeatureFlags.enableSMSClassification) { - throw new Error('SMS Classification disabled via feature flag'); - } - - if (!this.smsClassifier) { - throw new Error('SMS Classifier not initialized'); - } - - return await this.smsClassifier.classify(text); - } - - getSmsClassifier(): BertSmsClassifier | undefined { - return this.smsClassifier; - } - - // Combined interception methods - async interceptCall(call: IncomingCall): Promise { - if (!spamFeatureFlags.enableCallAnalysis) { - throw new Error('Call analysis disabled via feature flag'); - } - - const requestId = call.requestId ?? generateRequestId(); - const decision = await this.makeRealTimeDecision(call.phoneNumber, { - callMetadata: { - callId: call.callId, - startTime: call.startTime, - direction: call.direction, - carrierInfo: { carrierType: call.carrierType, carrierSid: call.carrierSid }, - }, - ruleMatches: [], - requestId, - }); - - await this.executeCarrierAction( - call.carrierType, - decision.decision.toLowerCase() as 'block' | 'flag' | 'allow', - call.phoneNumber, - call.carrierSid - ); - - await this.broadcastDecision(call.phoneNumber, decision); - - return { ...decision, requestId }; - } - - async interceptSms(sms: IncomingSms): Promise { - const requestId = sms.requestId ?? generateRequestId(); - const decision = await this.makeRealTimeDecision(sms.phoneNumber, { - smsContent: { - messageId: sms.messageId, - body: sms.body, - timestamp: sms.timestamp, - direction: sms.direction, - }, - ruleMatches: [], - requestId, - }); - - await this.executeCarrierAction( - sms.carrierType, - decision.decision.toLowerCase() as 'block' | 'flag' | 'allow', - sms.phoneNumber, - sms.carrierSid, - true - ); - - await this.broadcastDecision(sms.phoneNumber, decision); - - return { ...decision, requestId }; - } - - private async logCarrierAction( - phoneNumber: string, - action: string, - carrierType: CarrierType, - sid: string, - isSms: boolean - ): Promise { - await prisma.spamAuditLog.create({ - data: { - userId: 'carrier', - phoneNumber, - decision: action as any, - reason: `Carrier action: ${carrierType} ${isSms ? 'SMS' : 'Call'} ${sid}`, - }, - }); - } - - private async fetchHiyaReputation(phoneNumber: string): Promise { - if (!spamFeatureFlags.enableHiyaIntegration) { - throw new Error('Hiya integration disabled'); - } - - return this.hiyaBreaker.execute( - async () => { - const url = `https://api.hiya.com/reputation/${encodeURIComponent(phoneNumber)}`; - const response = await fetch(url, { - method: 'GET', - headers: { - 'Authorization': `Bearer ${process.env.HIYA_API_KEY}`, - 'Accept': 'application/json', - }, - }); - - if (!response.ok) { - throw new Error(`Hiya API error: ${response.status} ${response.statusText}`); - } - - const data = await response.json() as { spamScore?: number; reputation?: { score?: number } }; - const score = data.spamScore ?? data.reputation?.score ?? 0; - return score; - }, - () => { - console.log('[SpamShield] Hiya fallback: circuit OPEN, returning neutral score'); - return 0.5; - } - ); - } - - private async fetchTruecallerReputation(phoneNumber: string): Promise { - if (!spamFeatureFlags.enableTruecallerIntegration) { - throw new Error('Truecaller integration disabled'); - } - - return this.truecallerBreaker.execute( - async () => { - const url = `https://redirect.truecaller.com/api/v2-ac/absolute/${encodeURIComponent(phoneNumber)}`; - const response = await fetch(url, { - method: 'GET', - headers: { - 'contentType': 'lookupNumber', - 'Authorization': `Basic ${Buffer.from(process.env.TRUECALLER_API_KEY || '').toString('base64')}`, - 'Accept': 'application/json', - }, - }); - - if (!response.ok) { - throw new Error(`Truecaller API error: ${response.status} ${response.statusText}`); - } - - const data = await response.json() as { spamProbability?: number; spam_type?: number }; - const probability = data.spamProbability ?? (data.spam_type ? 0.8 : 0); - return probability; - }, - () => { - console.log('[SpamShield] Truecaller fallback: circuit OPEN, returning neutral score'); - return 0.5; - } - ); - } - - private validatePhoneNumber(phoneNumber: string): string { - return validateE164(phoneNumber); - } - - private async getActiveRules(): Promise> { - return prisma.spamRule.findMany({ - where: { isActive: true }, - select: { id: true, pattern: true }, - }); - } - - private validateMetadata(metadata: Record): Record { - const metadataStr = JSON.stringify(metadata); - - if (metadataStr.length > metadataLimits.maxMetadataSizeBytes) { - console.log(`[SpamShield] Metadata size ${metadataStr.length}B exceeds limit ${metadataLimits.maxMetadataSizeBytes}B, truncating`); - } - - const entries = Object.entries(metadata); - const truncatedEntries: [string, any][] = []; - - for (let i = 0; i < Math.min(entries.length, metadataLimits.maxMetadataKeys); i++) { - const [key, value] = entries[i]; - const valueStr = String(value); - - if (valueStr.length > metadataLimits.maxMetadataValueSizeBytes) { - truncatedEntries.push([key, valueStr.slice(0, metadataLimits.maxMetadataValueSizeBytes)]); - } else { - truncatedEntries.push([key, value]); - } - } - - const result = Object.fromEntries(truncatedEntries); - const resultStr = JSON.stringify(result); - - if (resultStr.length > metadataLimits.maxMetadataSizeBytes) { - const shrunk: Record = {}; - let currentSize = 2; - - for (const [key, value] of truncatedEntries) { - const entrySize = key.length + String(value).length + 3; - if (currentSize + entrySize <= metadataLimits.maxMetadataSizeBytes) { - shrunk[key] = value; - currentSize += entrySize; - } else { - break; - } - } - - console.log(`[SpamShield] Metadata reduced to ${Object.keys(shrunk).length} keys to fit size limit`); - return shrunk; - } - - return result; - } -} diff --git a/services/spamshield/src/spamshield.audit-logger.ts b/services/spamshield/src/spamshield.audit-logger.ts deleted file mode 100644 index dd62ee4..0000000 --- a/services/spamshield/src/spamshield.audit-logger.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { createHash } from 'crypto'; - -export type AuditClassificationType = 'sms' | 'call'; - -export interface AuditClassificationEntry { - id: string; - timestamp: string; - type: AuditClassificationType; - phoneNumberHash: string; - decision: 'spam' | 'ham' | 'block' | 'flag' | 'allow'; - confidence: number; - reasons: string[]; - featureFlags: Record; - metadata?: Record; -} - -const MAX_AUDIT_LOG_SIZE = 10_000; - -class AuditLogger { - private entries: AuditClassificationEntry[] = []; - - logClassification(entry: Omit): AuditClassificationEntry { - const record: AuditClassificationEntry = { - id: `audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - timestamp: new Date().toISOString(), - ...entry, - }; - - this.entries.push(record); - - if (this.entries.length > MAX_AUDIT_LOG_SIZE) { - this.entries.shift(); - } - - console.log( - `[SpamShield:Audit] type=${record.type} decision=${record.decision} ` + - `confidence=${record.confidence.toFixed(3)} reasons=${record.reasons.join(',') || 'none'} ` + - `phoneHash=${record.phoneNumberHash}` - ); - - return record; - } - - getEntries( - filters?: { - type?: AuditClassificationType; - decision?: string; - startDate?: Date; - endDate?: Date; - limit?: number; - } - ): AuditClassificationEntry[] { - let results = this.entries; - - if (filters?.type) { - results = results.filter(e => e.type === filters.type); - } - - if (filters?.decision) { - results = results.filter(e => e.decision === filters.decision); - } - - if (filters?.startDate) { - results = results.filter(e => new Date(e.timestamp) >= filters.startDate!); - } - - if (filters?.endDate) { - results = results.filter(e => new Date(e.timestamp) <= filters.endDate!); - } - - if (filters?.limit) { - results = results.slice(-filters.limit); - } - - return results; - } - - getSummary(): { - totalEntries: number; - spamCount: number; - hamCount: number; - blockCount: number; - flagCount: number; - allowCount: number; - avgConfidence: number; - } { - const spamCount = this.entries.filter(e => e.decision === 'spam' || e.decision === 'block').length; - const hamCount = this.entries.filter(e => e.decision === 'ham' || e.decision === 'allow').length; - const blockCount = this.entries.filter(e => e.decision === 'block').length; - const flagCount = this.entries.filter(e => e.decision === 'flag').length; - const allowCount = this.entries.filter(e => e.decision === 'allow').length; - const avgConfidence = - this.entries.length > 0 - ? this.entries.reduce((s, e) => s + e.confidence, 0) / this.entries.length - : 0; - - return { - totalEntries: this.entries.length, - spamCount, - hamCount, - blockCount, - flagCount, - allowCount, - avgConfidence: Math.round(avgConfidence * 1000) / 1000, - }; - } - - clear(): void { - this.entries = []; - } -} - -export const spamAuditLogger = new AuditLogger(); - -export function hashPhoneNumber(phoneNumber: string): string { - const hash = createHash('sha256').update(phoneNumber.trim()).digest('hex'); - return `sha256_${hash}`; -} diff --git a/services/spamshield/src/spamshield.error-handler.ts b/services/spamshield/src/spamshield.error-handler.ts deleted file mode 100644 index 41b382d..0000000 --- a/services/spamshield/src/spamshield.error-handler.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { FastifyReply } from 'fastify'; -import { SpamErrorCode, HttpStatus, SpamErrorResponse } from './spamshield.config'; - -export { SpamErrorCode, HttpStatus }; -export type { SpamErrorResponse }; - -/** - * Standardized error response builder for SpamShield API - */ -export class ErrorHandler { - /** - * Create a standard error response - */ - static create( - code: SpamErrorCode, - message: string, - options?: { - field?: string; - requestId?: string; - additionalData?: Record; - } - ): SpamErrorResponse { - return { - error: { - code, - message, - ...(options?.field && { field: options.field }), - timestamp: new Date().toISOString(), - ...(options?.requestId && { requestId: options.requestId }), - }, - }; - } - - /** - * Send a standard error response with appropriate HTTP status code - */ - static send( - reply: FastifyReply, - code: SpamErrorCode, - message: string, - options?: { - field?: string; - status?: number; - requestId?: string; - } - ): void { - const status = options?.status ?? this.getStatusForCode(code); - const errorResponse = this.create(code, message, { - field: options?.field, - requestId: options?.requestId, - }); - reply.code(status).send(errorResponse); - } - - /** - * Map error codes to HTTP status codes - */ - private static getStatusForCode(code: SpamErrorCode): number { - const statusMap: Record = { - // Client errors - [SpamErrorCode.INVALID_REQUEST]: HttpStatus.BAD_REQUEST, - [SpamErrorCode.MISSING_REQUIRED_FIELD]: HttpStatus.BAD_REQUEST, - [SpamErrorCode.UNAUTHORIZED]: HttpStatus.UNAUTHORIZED, - [SpamErrorCode.NOT_FOUND]: HttpStatus.NOT_FOUND, - [SpamErrorCode.VALIDATION_ERROR]: HttpStatus.BAD_REQUEST, - - // Server errors - [SpamErrorCode.CLASSIFICATION_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.REPUTATION_CHECK_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.ANALYSIS_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.FEEDBACK_RECORD_FAILED]: HttpStatus.UNPROCESSABLE_ENTITY, - [SpamErrorCode.DATABASE_ERROR]: HttpStatus.INTERNAL_SERVER_ERROR, - [SpamErrorCode.RATE_LIMIT_EXCEEDED]: HttpStatus.TOO_MANY_REQUESTS, - [SpamErrorCode.SERVICE_UNAVAILABLE]: HttpStatus.SERVICE_UNAVAILABLE, - }; - return statusMap[code] ?? HttpStatus.INTERNAL_SERVER_ERROR; - } - - /** - * Validate required string field - */ - static validateRequiredField( - value: unknown, - fieldName: string - ): { isValid: boolean; error?: { code: SpamErrorCode; message: string; field: string } } { - if (!value || typeof value !== 'string' || value.trim() === '') { - return { - isValid: false, - error: { - code: SpamErrorCode.MISSING_REQUIRED_FIELD, - message: `${fieldName} is required`, - field: fieldName, - }, - }; - } - return { isValid: true }; - } - - /** - * Validate boolean field - */ - static validateBooleanField( - value: unknown, - fieldName: string - ): { isValid: boolean; error?: { code: SpamErrorCode; message: string; field: string } } { - if (value === undefined || value === null || typeof value !== 'boolean') { - return { - isValid: false, - error: { - code: SpamErrorCode.VALIDATION_ERROR, - message: `${fieldName} must be a boolean`, - field: fieldName, - }, - }; - } - return { isValid: true }; - } -} diff --git a/services/spamshield/src/utils/phone-validation.ts b/services/spamshield/src/utils/phone-validation.ts deleted file mode 100644 index 857cfac..0000000 --- a/services/spamshield/src/utils/phone-validation.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js'; - -export class PhoneNumberValidationError extends Error { - constructor(public readonly originalInput: string) { - super( - `Invalid E.164 phone number format: ${originalInput}. Expected format: +[country code][number] (e.g., +14155552671)` - ); - this.name = 'PhoneNumberValidationError'; - } -} - -export function validatePhoneNumber(phoneNumber: string): string { - const trimmed = phoneNumber.trim(); - - if (!isValidPhoneNumber(trimmed)) { - throw new PhoneNumberValidationError(phoneNumber); - } - - const parsed = parsePhoneNumber(trimmed); - if (!parsed || !parsed.number) { - throw new PhoneNumberValidationError(phoneNumber); - } - - return parsed.number; -} diff --git a/services/spamshield/src/utils/regex-validation.ts b/services/spamshield/src/utils/regex-validation.ts deleted file mode 100644 index 434be3b..0000000 --- a/services/spamshield/src/utils/regex-validation.ts +++ /dev/null @@ -1,318 +0,0 @@ -export class RegexValidationError extends Error { - constructor( - public readonly pattern: string, - public readonly reason: string, - ) { - super(`Regex validation failed for pattern "${pattern}": ${reason}`); - this.name = 'RegexValidationError'; - } -} - -export interface RegexValidationOptions { - maxLength?: number; - maxNestingDepth?: number; - maxAlternations?: number; - maxQuantifierLength?: number; -} - -const DEFAULT_OPTIONS: Required = { - maxLength: 500, - maxNestingDepth: 10, - maxAlternations: 20, - maxQuantifierLength: 100, -}; - -export interface RegexComplexityMetrics { - length: number; - nestingDepth: number; - alternationCount: number; - quantifierCount: number; - groupCount: number; - isRedosProne: boolean; - issues: string[]; -} - -function countChar(str: string, char: string): number { - let count = 0; - for (let i = 0; i < str.length; i++) { - if (str[i] === char) count++; - } - return count; -} - -function calculateNestingDepth(pattern: string): number { - let maxDepth = 0; - let currentDepth = 0; - let inCharClass = false; - let escaped = false; - - for (let i = 0; i < pattern.length; i++) { - const char = pattern[i]; - - if (escaped) { - escaped = false; - continue; - } - - if (char === '\\') { - escaped = true; - continue; - } - - if (char === '[') { - inCharClass = true; - continue; - } - - if (char === ']' && inCharClass) { - inCharClass = false; - continue; - } - - if (inCharClass) continue; - - if (char === '(') { - currentDepth++; - if (currentDepth > maxDepth) { - maxDepth = currentDepth; - } - } - - if (char === ')') { - currentDepth--; - } - } - - return maxDepth; -} - -function countAlternations(pattern: string): number { - let count = 0; - let inCharClass = false; - let escaped = false; - - for (let i = 0; i < pattern.length; i++) { - const char = pattern[i]; - - if (escaped) { - escaped = false; - continue; - } - - if (char === '\\') { - escaped = true; - continue; - } - - if (char === '[') { - inCharClass = true; - continue; - } - - if (char === ']' && inCharClass) { - inCharClass = false; - continue; - } - - if (inCharClass) continue; - - if (char === '|') { - count++; - } - } - - return count; -} - -function detectNestedQuantifiers(pattern: string): string[] { - const issues: string[] = []; - const quantifierRegex = /\(([^)]*)\)[*+?]/; - const overlappingRegex = /([a-zA-Z0-9])([^|]*?)\1/; - - const groups = pattern.match(/\(([^)]+)\)/g) || []; - - for (const group of groups) { - const innerContent = group.slice(1, -1); - - if (innerContent.includes('+') || innerContent.includes('*') || innerContent.includes('?')) { - if (/[*+?]$/.test(group)) { - issues.push(`Nested quantifier detected in group: ${group}`); - } - } - } - - const quantifierGroups = pattern.match(/\(([^)]+[*+?][^)]*)\)[*+?]/g) || []; - for (const g of quantifierGroups) { - if (!issues.includes(`Nested quantifier detected in group: ${g}`)) { - issues.push(`Nested quantifier detected in group: ${g}`); - } - } - - return issues; -} - -function detectOverlappingAlternations(pattern: string): string[] { - const issues: string[] = []; - - const groups = pattern.match(/\(([^)]+)\)/g) || []; - - for (const group of groups) { - const innerContent = group.slice(1, -1); - const alternations = innerContent.split('|'); - - if (alternations.length < 2) continue; - - for (let i = 0; i < alternations.length; i++) { - for (let j = i + 1; j < alternations.length; j++) { - const a = alternations[i].trim(); - const b = alternations[j].trim(); - - if (a && b && (a.startsWith(b) || b.startsWith(a))) { - issues.push( - `Overlapping alternation detected: "${a}" and "${b}" in group ${group}` - ); - } - } - } - } - - return issues; -} - -function detectCatastrophicBacktracking(pattern: string): string[] { - const issues: string[] = []; - - const starHeightPattern = /(\([^()]*\)[*+])+[*+]/; - if (starHeightPattern.test(pattern)) { - issues.push('High star-height pattern detected (potential exponential backtracking)'); - } - - const ambiguousQuantifiers = /[*+?][^)]*[*+?]/; - if (ambiguousQuantifiers.test(pattern)) { - const matches = pattern.match(/(?<=\()[^)]*(?=\))/g) || []; - for (const match of matches) { - if (ambiguousQuantifiers.test(match)) { - issues.push(`Ambiguous quantifiers in group content: ${match}`); - } - } - } - - return issues; -} - -function countQuantifiers(pattern: string): number { - let count = 0; - let escaped = false; - - for (let i = 0; i < pattern.length; i++) { - if (escaped) { - escaped = false; - continue; - } - - if (pattern[i] === '\\') { - escaped = true; - continue; - } - - if (pattern[i] === '*' || pattern[i] === '+' || pattern[i] === '?') { - count++; - } - - if (pattern[i] === '{') { - const closingBrace = pattern.indexOf('}', i); - if (closingBrace !== -1) { - count++; - i = closingBrace; - } - } - } - - return count; -} - -export function analyzeRegexComplexity( - pattern: string, - options?: RegexValidationOptions, -): RegexComplexityMetrics { - const opts = { ...DEFAULT_OPTIONS, ...options }; - const issues: string[] = []; - - const length = pattern.length; - const nestingDepth = calculateNestingDepth(pattern); - const alternationCount = countAlternations(pattern); - const quantifierCount = countQuantifiers(pattern); - const groupCount = countChar(pattern, '('); - - if (length > opts.maxLength) { - issues.push(`Pattern length (${length}) exceeds maximum (${opts.maxLength})`); - } - - if (nestingDepth > opts.maxNestingDepth) { - issues.push(`Nesting depth (${nestingDepth}) exceeds maximum (${opts.maxNestingDepth})`); - } - - if (alternationCount > opts.maxAlternations) { - issues.push(`Alternation count (${alternationCount}) exceeds maximum (${opts.maxAlternations})`); - } - - const nestedQuantifierIssues = detectNestedQuantifiers(pattern); - issues.push(...nestedQuantifierIssues); - - const overlappingIssues = detectOverlappingAlternations(pattern); - issues.push(...overlappingIssues); - - const backtrackingIssues = detectCatastrophicBacktracking(pattern); - issues.push(...backtrackingIssues); - - return { - length, - nestingDepth, - alternationCount, - quantifierCount, - groupCount, - isRedosProne: issues.length > 0, - issues, - }; -} - -export function validateRegexPattern( - pattern: string, - options?: RegexValidationOptions, -): RegexComplexityMetrics { - if (!pattern || typeof pattern !== 'string') { - throw new RegexValidationError(pattern ?? '', 'Pattern must be a non-empty string'); - } - - try { - new RegExp(pattern); - } catch (err) { - throw new RegexValidationError( - pattern, - `Invalid regex syntax: ${(err as Error).message}`, - ); - } - - const metrics = analyzeRegexComplexity(pattern, options); - - if (metrics.isRedosProne) { - throw new RegexValidationError( - pattern, - `ReDoS risk: ${metrics.issues.join('; ')}`, - ); - } - - return metrics; -} - -export function isSafeRegexPattern( - pattern: string, - options?: RegexValidationOptions, -): boolean { - try { - validateRegexPattern(pattern, options); - return true; - } catch { - return false; - } -} diff --git a/services/spamshield/src/websocket/alert-server.ts b/services/spamshield/src/websocket/alert-server.ts deleted file mode 100644 index 12a961c..0000000 --- a/services/spamshield/src/websocket/alert-server.ts +++ /dev/null @@ -1,334 +0,0 @@ -import { WebSocketServer, WebSocket } from 'ws'; -import { createHash } from 'crypto'; -import { DecisionResult } from '../engine/decision-engine'; - -export interface AlertEvent { - type: 'decision' | 'flag' | 'block' | 'user_feedback' | 'carrier_action'; - data: { - phoneNumber: string; - phoneNumberHash?: string; - decision?: 'BLOCK' | 'FLAG' | 'ALLOW'; - confidence?: number; - ruleMatches?: string[]; - carrierAction?: string; - timestamp: Date; - metadata?: Record; - }; -} - -export interface ClientSubscription { - clientId: string; - subscribedEvents: string[]; - connectedAt: Date; - lastActivity: Date; - ws?: WebSocket; -} - -export interface AlertServerConfig { - port?: number; - host?: string; - heartbeatIntervalMs?: number; - maxClients?: number; - enableLogging?: boolean; - enableAuth?: boolean; - jwtSecret?: string; - allowedOrigins?: string[]; -} - -const DEFAULT_CONFIG: Required = { - port: 8080, - host: '0.0.0.0', - heartbeatIntervalMs: 30000, - maxClients: 100, - enableLogging: true, - enableAuth: true, - jwtSecret: process.env.SPAMSHIELD_JWT_SECRET || '', - allowedOrigins: ['http://localhost:3000'], -}; - -export class AlertServer { - private readonly config: Required; - private readonly wss: WebSocketServer; - private readonly clients: Map = new Map(); - private heartbeatInterval?: NodeJS.Timeout; - private isRunning = false; - - constructor(config?: AlertServerConfig) { - this.config = { ...DEFAULT_CONFIG, ...config }; - this.wss = new WebSocketServer({ - port: this.config.port, - host: this.config.host, - maxPayload: 65536, // 64KB limit to prevent memory exhaustion attacks - }); - - this.setupWebSocketHandlers(); - } - - private setupWebSocketHandlers(): void { - this.wss.on('connection', async (ws: WebSocket, req: any) => { - const origin = req.headers.origin; - if (origin && this.config.allowedOrigins.length > 0 && !this.config.allowedOrigins.includes(origin)) { - ws.close(1008, 'Origin not allowed'); - return; - } - - if (this.config.enableAuth && this.config.jwtSecret) { - const authHeader = req.headers.authorization; - if (!authHeader || !authHeader.startsWith('Bearer ')) { - ws.close(4001, 'Missing or invalid JWT token'); - return; - } - const token = authHeader.substring(7); - const valid = await this.verifyJWT(token); - if (!valid) { - ws.close(4002, 'Invalid or expired JWT token'); - return; - } - } - - if (this.clients.size >= this.config.maxClients) { - ws.close(1013, 'Too many clients'); - return; - } - - const clientId = req.headers['x-client-id'] as string || `client-${Date.now()}-${Math.random()}`; - - const subscription: ClientSubscription = { - clientId, - subscribedEvents: ['decision', 'flag', 'block', 'user_feedback', 'carrier_action'], - connectedAt: new Date(), - lastActivity: new Date(), - ws, - }; - - this.clients.set(clientId, subscription); - - ws.on('message', (data: Buffer) => { - try { - const message = JSON.parse(data.toString()) as { eventTypes?: string[] }; - if (message.eventTypes) { - subscription.subscribedEvents = message.eventTypes; - } - subscription.lastActivity = new Date(); - } catch (error) { - console.error('[AlertServer] Error parsing client message:', error); - } - }); - - ws.on('close', () => { - this.clients.delete(clientId); - if (this.config.enableLogging) { - console.log(`[AlertServer] Client ${clientId} disconnected. Active clients: ${this.clients.size}`); - } - }); - - ws.on('error', (error: Error) => { - console.error(`[AlertServer] WebSocket error for client ${clientId}:`, error); - }); - - ws.send(JSON.stringify({ - type: 'connected', - data: { - clientId, - subscribedEvents: subscription.subscribedEvents, - connectedAt: subscription.connectedAt, - }, - })); - - if (this.config.enableLogging) { - console.log(`[AlertServer] Client ${clientId} connected. Total clients: ${this.clients.size}`); - } - }); - - this.wss.on('error', (error: Error) => { - console.error('[AlertServer] Server error:', error); - }); - } - - async broadcastDecision(phoneNumber: string, decision: DecisionResult): Promise { - const event: AlertEvent = { - type: 'decision', - data: { - phoneNumber, - phoneNumberHash: this.hashPhoneNumber(phoneNumber), - decision: decision.decision, - confidence: decision.confidence, - ruleMatches: decision.reasons, - timestamp: decision.executedAt, - metadata: { - scoring: decision.scoring, - }, - }, - }; - - await this.broadcast(event, ['decision']); - } - - async broadcastBlock(phoneNumber: string, callSid: string): Promise { - const event: AlertEvent = { - type: 'block', - data: { - phoneNumber, - timestamp: new Date(), - metadata: { - callSid, - action: 'carrier_block', - }, - }, - }; - - await this.broadcast(event, ['block', 'carrier_action']); - } - - async broadcastFlag(phoneNumber: string, reasons: string[]): Promise { - const event: AlertEvent = { - type: 'flag', - data: { - phoneNumber, - timestamp: new Date(), - metadata: { - reasons, - }, - }, - }; - - await this.broadcast(event, ['flag']); - } - - async broadcastUserFeedback( - phoneNumber: string, - isSpam: boolean, - userId: string - ): Promise { - const event: AlertEvent = { - type: 'user_feedback', - data: { - phoneNumber, - timestamp: new Date(), - metadata: { - isSpam, - userId, - }, - }, - }; - - await this.broadcast(event, ['user_feedback']); - } - - private async broadcast(event: AlertEvent, eventTypes: string[]): Promise { - const eventData = JSON.stringify(event); - const now = new Date(); - - for (const [clientId, subscription] of this.clients.entries()) { - const shouldSend = subscription.subscribedEvents.some(et => eventTypes.includes(et)); - - if (shouldSend && subscription.ws?.readyState === WebSocket.OPEN) { - try { - subscription.ws.send(eventData); - subscription.lastActivity = now; - } catch (error) { - if (this.config.enableLogging) { - console.error(`[AlertServer] Failed to send to client ${clientId}:`, error); - } - } - } - } - } - - subscribe(clientId: string, eventTypes: string[]): void { - const subscription = this.clients.get(clientId); - if (subscription) { - subscription.subscribedEvents = eventTypes; - subscription.lastActivity = new Date(); - } - } - - unsubscribe(clientId: string): void { - this.clients.delete(clientId); - if (this.config.enableLogging) { - console.log(`[AlertServer] Client ${clientId} unsubscribed. Active clients: ${this.clients.size}`); - } - } - - getClientCount(): number { - return this.clients.size; - } - - getActiveClients(): Array<{ clientId: string; subscribedEvents: string[]; connectedAt: Date }> { - return Array.from(this.clients.values()).map(({ clientId, subscribedEvents, connectedAt }) => ({ - clientId, - subscribedEvents, - connectedAt, - })); - } - - startHeartbeat(): void { - this.heartbeatInterval = setInterval(() => { - const heartbeat: AlertEvent = { - type: 'decision', - data: { - phoneNumber: '', - timestamp: new Date(), - metadata: { - heartbeat: true, - activeClients: this.clients.size, - }, - }, - }; - - const eventData = JSON.stringify(heartbeat); - for (const subscription of this.clients.values()) { - if (subscription.subscribedEvents.includes('decision') && subscription.ws?.readyState === WebSocket.OPEN) { - subscription.ws.send(eventData); - } - } - }, this.config.heartbeatIntervalMs); - - this.isRunning = true; - } - - stopHeartbeat(): void { - if (this.heartbeatInterval) { - clearInterval(this.heartbeatInterval); - this.heartbeatInterval = undefined; - } - this.isRunning = false; - } - - async shutdown(): Promise { - this.stopHeartbeat(); - - return new Promise((resolve) => { - this.wss.close(() => { - for (const subscription of this.clients.values()) { - subscription.ws?.terminate(); - } - this.clients.clear(); - resolve(); - }); - }); - } - - getConfig(): Required { - return { ...this.config }; - } - - private hashPhoneNumber(phoneNumber: string): string { - return createHash('sha256').update(phoneNumber).digest('hex'); - } - - private async verifyJWT(token: string): Promise { - try { - const { jwtVerify } = await import('jose'); - await jwtVerify(token, new TextEncoder().encode(this.config.jwtSecret), { - algorithms: ['HS256'], - }); - return true; - } catch { - if (this.config.enableLogging) { - console.log('[AlertServer] JWT verification failed'); - } - return false; - } - } -} diff --git a/services/spamshield/src/websocket/index.ts b/services/spamshield/src/websocket/index.ts deleted file mode 100644 index b061f50..0000000 --- a/services/spamshield/src/websocket/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './alert-server'; diff --git a/services/spamshield/test/circuit-breaker.test.ts b/services/spamshield/test/circuit-breaker.test.ts deleted file mode 100644 index 658e245..0000000 --- a/services/spamshield/test/circuit-breaker.test.ts +++ /dev/null @@ -1,285 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { CircuitBreaker, CircuitBreakerError, CircuitState } from '../src/circuit-breaker'; - -const fail = async () => { throw new Error('fail'); }; -const success = async () => 'ok'; - -async function executeOrFail(breaker: CircuitBreaker, fn: () => Promise, fallback?: () => T): Promise { - try { - return await breaker.execute(fn, fallback); - } catch (e) { - return e as Error; - } -} - -describe('CircuitBreaker', () => { - let stateChanges: Array<{ state: CircuitState; previous: CircuitState }>; - - beforeEach(() => { - stateChanges = []; - }); - - describe('initial state', () => { - it('starts as CLOSED', () => { - const breaker = new CircuitBreaker(); - expect(breaker.getState()).toBe('CLOSED'); - }); - - it('uses default thresholds', () => { - const breaker = new CircuitBreaker(); - const metrics = breaker.getMetrics(); - expect(metrics.failureCount).toBe(0); - expect(metrics.successCount).toBe(0); - expect(metrics.totalExecutions).toBe(0); - }); - }); - - describe('custom configuration', () => { - it('accepts custom failure threshold', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 3 }); - for (let i = 0; i < 3; i++) { - await executeOrFail(breaker, fail); - } - expect(breaker.getState()).toBe('OPEN'); - }); - - it('accepts custom timeout', () => { - const breaker = new CircuitBreaker({ timeout: 1000 }); - expect(breaker.getState()).toBe('CLOSED'); - }); - - it('calls onStateChange callback on transitions', async () => { - const breaker = new CircuitBreaker({ - failureThreshold: 2, - onStateChange: (state, previous) => { - stateChanges.push({ state, previous }); - }, - }); - - await executeOrFail(breaker, fail); - await executeOrFail(breaker, fail); - - expect(stateChanges).toHaveLength(1); - expect(stateChanges[0]).toEqual({ state: 'OPEN', previous: 'CLOSED' }); - }); - }); - - describe('state transitions', () => { - it('transitions to OPEN after reaching failure threshold', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 3 }); - - await executeOrFail(breaker, fail); - await executeOrFail(breaker, fail); - expect(breaker.getState()).toBe('CLOSED'); - - await executeOrFail(breaker, fail); - expect(breaker.getState()).toBe('OPEN'); - }); - - it('transitions from OPEN to HALF_OPEN after timeout', async () => { - const breaker = new CircuitBreaker({ - failureThreshold: 2, - timeout: 200, - }); - - await executeOrFail(breaker, fail); - await executeOrFail(breaker, fail); - expect(breaker.getState()).toBe('OPEN'); - - await new Promise((resolve) => setTimeout(resolve, 250)); - expect(breaker.getState()).toBe('HALF_OPEN'); - }); - - it('transitions from HALF_OPEN to CLOSED after success threshold', async () => { - const breaker = new CircuitBreaker({ - failureThreshold: 2, - successThreshold: 3, - timeout: 100, - }); - - await executeOrFail(breaker, fail); - await executeOrFail(breaker, fail); - expect(breaker.getState()).toBe('OPEN'); - - await new Promise((resolve) => setTimeout(resolve, 150)); - - const r1 = await breaker.execute(success); - const r2 = await breaker.execute(success); - expect(r1).toBe('ok'); - expect(r2).toBe('ok'); - expect(breaker.getState()).toBe('HALF_OPEN'); - - const r3 = await breaker.execute(success); - expect(r3).toBe('ok'); - expect(breaker.getState()).toBe('CLOSED'); - }); - - it('transitions from HALF_OPEN back to OPEN on failure', async () => { - const breaker = new CircuitBreaker({ - failureThreshold: 2, - timeout: 100, - }); - - await executeOrFail(breaker, fail); - await executeOrFail(breaker, fail); - - await new Promise((resolve) => setTimeout(resolve, 150)); - expect(breaker.getState()).toBe('HALF_OPEN'); - - await executeOrFail(breaker, fail); - expect(breaker.getState()).toBe('OPEN'); - }); - }); - - describe('execute with fallback', () => { - it('returns fallback value when circuit is OPEN', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 2 }); - - await executeOrFail(breaker, fail); - await executeOrFail(breaker, fail); - - const result = await breaker.execute( - async () => { throw new Error('should not reach'); }, - () => 'fallback-value' - ); - - expect(result).toBe('fallback-value'); - }); - - it('returns fallback value when API throws in OPEN state', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 1 }); - - await executeOrFail(breaker, fail); - - const originalFn = vi.fn(() => { throw new Error('api error'); }); - const fallbackFn = vi.fn(() => 0.5); - - const result = await breaker.execute(originalFn, fallbackFn); - expect(result).toBe(0.5); - expect(fallbackFn).toHaveBeenCalled(); - }); - - it('executes function normally when circuit is CLOSED', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 3 }); - const fn = vi.fn(async () => 'success'); - - const result = await breaker.execute(fn, () => 'fallback'); - expect(result).toBe('success'); - expect(fn).toHaveBeenCalled(); - }); - - it('uses fallback when circuit is OPEN', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 1 }); - await executeOrFail(breaker, fail); - - const fn = vi.fn(async () => 'original'); - const fallback = vi.fn(() => 'fallback-value'); - - const result = await breaker.execute(fn, fallback); - expect(result).toBe('fallback-value'); - expect(fallback).toHaveBeenCalled(); - }); - - it('throws CircuitBreakerError when OPEN and no fallback', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 1 }); - await executeOrFail(breaker, fail); - - const result = await executeOrFail(breaker, async () => 'value'); - expect(result).toBeInstanceOf(CircuitBreakerError); - expect((result as CircuitBreakerError).state).toBe('OPEN'); - }); - - it('throws original error when fallback also fails in CLOSED state', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 5 }); - - const originalError = new Error('api error'); - const result = await executeOrFail( - breaker, - async () => { throw originalError; }, - () => { throw new Error('fallback error'); } - ); - expect(result).toBe(originalError); - }); - }); - - describe('metrics', () => { - it('tracks total executions', async () => { - const breaker = new CircuitBreaker(); - await breaker.execute(success); - await breaker.execute(success); - - const metrics = breaker.getMetrics(); - expect(metrics.totalExecutions).toBe(2); - expect(metrics.totalSuccesses).toBe(2); - expect(metrics.totalFailures).toBe(0); - }); - - it('tracks failures', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 5 }); - await executeOrFail(breaker, fail); - - const metrics = breaker.getMetrics(); - expect(metrics.totalExecutions).toBe(1); - expect(metrics.totalFailures).toBe(1); - expect(metrics.failureCount).toBe(1); - }); - - it('includes state change timestamp', () => { - const breaker = new CircuitBreaker(); - const metrics = breaker.getMetrics(); - expect(metrics.stateChangedAt).toBeDefined(); - expect(metrics.stateChangedAt!.getTime()).toBeGreaterThan(Date.now() - 1000); - }); - - it('tracks last failure and success times', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 5 }); - const before = Date.now(); - - await breaker.execute(success); - await new Promise((resolve) => setTimeout(resolve, 10)); - await executeOrFail(breaker, fail); - - const metrics = breaker.getMetrics(); - expect(metrics.lastSuccessTime!.getTime()).toBeGreaterThanOrEqual(before); - expect(metrics.lastFailureTime!.getTime()).toBeGreaterThanOrEqual(metrics.lastSuccessTime!.getTime()); - }); - }); - - describe('reset', () => { - it('resets circuit to CLOSED state', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 2 }); - - await executeOrFail(breaker, fail); - await executeOrFail(breaker, fail); - expect(breaker.getState()).toBe('OPEN'); - - breaker.reset(); - expect(breaker.getState()).toBe('CLOSED'); - - const metrics = breaker.getMetrics(); - expect(metrics.failureCount).toBe(0); - expect(metrics.successCount).toBe(0); - }); - - it('allows execution after reset', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 1 }); - await executeOrFail(breaker, fail); - - breaker.reset(); - const result = await breaker.execute(success); - expect(result).toBe('ok'); - }); - }); - - describe('CircuitBreakerError', () => { - it('includes circuit state in error', async () => { - const breaker = new CircuitBreaker({ failureThreshold: 1 }); - await executeOrFail(breaker, fail); - - const result = await executeOrFail(breaker, success); - expect(result).toBeInstanceOf(CircuitBreakerError); - expect((result as CircuitBreakerError).state).toBe('OPEN'); - expect((result as CircuitBreakerError).message).toContain('OPEN'); - }); - }); -}); diff --git a/services/spamshield/test/mixpanel.test.ts b/services/spamshield/test/mixpanel.test.ts deleted file mode 100644 index 0a946d7..0000000 --- a/services/spamshield/test/mixpanel.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { MixpanelService, SpamBlockedEvent, MixpanelEventProperties } from '../src/services/mixpanel.service'; -import { FieldEncryptionService } from '@shieldai/db'; -import crypto from 'crypto'; - -const mockFetch = vi.fn(); -(global as any).fetch = mockFetch; - -describe('MixpanelService', () => { - let service: MixpanelService; - - beforeEach(() => { - service = new MixpanelService({ - token: 'test-token', - apiHost: 'api.mixpanel.com', - enableLogging: false, - }); - mockFetch.mockReset(); - vi.spyOn(console, 'log').mockImplementation(() => {}); - }); - - afterEach(() => { - vi.restoreAllMocks(); - }); - - describe('spamBlocked', () => { - it('hashes phoneNumber using SHA-256 before sending to Mixpanel', async () => { - const phoneNumber = '+14155552671'; - const expectedHash = crypto.createHash('sha256').update(phoneNumber).digest('hex'); - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - } as Response); - - const event: SpamBlockedEvent = { - phoneNumber, - decision: 'BLOCK', - confidence: 0.92, - ruleMatches: ['rule-1'], - timestamp: new Date('2026-01-01T00:00:00Z'), - }; - - const result = await service.spamBlocked(event); - - expect(result.phoneNumberHash).toBe(expectedHash); - expect(result.phoneNumberHash).not.toBe(phoneNumber); - expect(result.phoneNumberHash.length).toBe(64); - }); - - it('uses FieldEncryptionService.hashPhoneNumber for hashing', async () => { - const phoneNumber = '+14155552671'; - const hashSpy = vi.spyOn(FieldEncryptionService, 'hashPhoneNumber'); - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - } as Response); - - const event: SpamBlockedEvent = { - phoneNumber, - decision: 'BLOCK', - confidence: 0.85, - timestamp: new Date(), - }; - - await service.spamBlocked(event); - - expect(hashSpy).toHaveBeenCalledWith(phoneNumber); - hashSpy.mockRestore(); - }); - - it('sends hashed phoneNumber in event properties', async () => { - const phoneNumber = '+14155552671'; - const expectedHash = FieldEncryptionService.hashPhoneNumber(phoneNumber); - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - } as Response); - - const event: SpamBlockedEvent = { - phoneNumber, - decision: 'FLAG', - confidence: 0.65, - ruleMatches: ['rule-2', 'rule-3'], - timestamp: new Date('2026-01-01T00:00:00Z'), - }; - - const result = await service.spamBlocked(event); - - expect(result.$event_name).toBe('spam_blocked'); - expect(result.phoneNumberHash).toBe(expectedHash); - expect(result.decision).toBe('FLAG'); - expect(result.confidence).toBe(0.65); - expect(result.ruleMatches).toEqual(['rule-2', 'rule-3']); - expect(result.timestamp).toBe('2026-01-01T00:00:00.000Z'); - }); - - it('includes raw phoneNumber only internally, sends hash to Mixpanel API', async () => { - const phoneNumber = '+14155552671'; - const expectedHash = FieldEncryptionService.hashPhoneNumber(phoneNumber); - - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - } as Response); - - const event: SpamBlockedEvent = { - phoneNumber, - decision: 'BLOCK', - confidence: 0.95, - timestamp: new Date(), - }; - - await service.spamBlocked(event); - - const callArgs = mockFetch.mock.calls[0] as any[]; - const body = JSON.parse(callArgs[1].body); - const trackPayload = JSON.parse(body.data); - - expect(trackPayload.event).toBe('spam_blocked'); - expect(trackPayload.properties.phoneNumberHash).toBe(expectedHash); - expect(trackPayload.properties.phoneNumberHash).not.toBe(phoneNumber); - }); - - it('handles different phone numbers with unique hashes', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - } as Response); - - const phone1 = '+14155552671'; - const phone2 = '+442071234567'; - - const event1: SpamBlockedEvent = { - phoneNumber: phone1, - decision: 'BLOCK', - confidence: 0.9, - timestamp: new Date(), - }; - - const event2: SpamBlockedEvent = { - phoneNumber: phone2, - decision: 'FLAG', - confidence: 0.7, - timestamp: new Date(), - }; - - const result1 = await service.spamBlocked(event1); - const result2 = await service.spamBlocked(event2); - - expect(result1.phoneNumberHash).not.toBe(result2.phoneNumberHash); - expect(result1.phoneNumberHash).toBe(FieldEncryptionService.hashPhoneNumber(phone1)); - expect(result2.phoneNumberHash).toBe(FieldEncryptionService.hashPhoneNumber(phone2)); - }); - - it('produces deterministic hash for same phone number', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - } as Response); - - const phoneNumber = '+14155552671'; - - const event1: SpamBlockedEvent = { - phoneNumber, - decision: 'BLOCK', - confidence: 0.9, - timestamp: new Date(), - }; - - const event2: SpamBlockedEvent = { - phoneNumber, - decision: 'FLAG', - confidence: 0.7, - timestamp: new Date(), - }; - - const result1 = await service.spamBlocked(event1); - const result2 = await service.spamBlocked(event2); - - expect(result1.phoneNumberHash).toBe(result2.phoneNumberHash); - }); - - it('stores event in internal events array', async () => { - mockFetch.mockResolvedValueOnce({ - ok: true, - status: 200, - } as Response); - - const event: SpamBlockedEvent = { - phoneNumber: '+14155552671', - decision: 'BLOCK', - confidence: 0.9, - timestamp: new Date(), - }; - - await service.spamBlocked(event); - - const events = service.getEvents(); - expect(events).toHaveLength(1); - expect(events[0].phoneNumberHash).toBe(FieldEncryptionService.hashPhoneNumber('+14155552671')); - expect(events[0].$event_name).toBe('spam_blocked'); - }); - - it('handles BLOCK and FLAG decisions', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - } as Response); - - const blockEvent: SpamBlockedEvent = { - phoneNumber: '+14155552671', - decision: 'BLOCK', - confidence: 0.95, - timestamp: new Date(), - }; - - const flagEvent: SpamBlockedEvent = { - phoneNumber: '+14155552671', - decision: 'FLAG', - confidence: 0.6, - timestamp: new Date(), - }; - - const blockResult = await service.spamBlocked(blockEvent); - const flagResult = await service.spamBlocked(flagEvent); - - expect(blockResult.decision).toBe('BLOCK'); - expect(flagResult.decision).toBe('FLAG'); - }); - - it('gracefully handles Mixpanel API failure', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: 'Internal Server Error', - } as Response); - - const event: SpamBlockedEvent = { - phoneNumber: '+14155552671', - decision: 'BLOCK', - confidence: 0.9, - timestamp: new Date(), - }; - - const result = await service.spamBlocked(event); - - expect(result.phoneNumberHash).toBe(FieldEncryptionService.hashPhoneNumber('+14155552671')); - expect(result.status).toBe(500); - }); - - it('gracefully handles Mixpanel API network error', async () => { - mockFetch.mockResolvedValueOnce({ - ok: false, - status: 429, - statusText: 'Too Many Requests', - } as Response); - - const event: SpamBlockedEvent = { - phoneNumber: '+14155552671', - decision: 'BLOCK', - confidence: 0.9, - timestamp: new Date(), - }; - - const result = await service.spamBlocked(event); - - expect(result.phoneNumberHash).toBe(FieldEncryptionService.hashPhoneNumber('+14155552671')); - expect(result.status).toBe(429); - }); - }); - - describe('hashPhoneNumber consistency', () => { - it('hash matches SHA-256 hex digest for known input', () => { - const phoneNumber = '+14155552671'; - const hash = FieldEncryptionService.hashPhoneNumber(phoneNumber); - const expected = crypto.createHash('sha256').update(phoneNumber).digest('hex'); - - expect(hash).toBe(expected); - expect(hash).toBe('cb6880e416769253645cb9c6b8989154bf66a56a77fc14c81fb1019663cbb928'); - }); - - it('hash is 64 characters (SHA-256 hex)', () => { - const hash = FieldEncryptionService.hashPhoneNumber('+14155552671'); - expect(hash.length).toBe(64); - expect(/^[0-9a-f]{64}$/.test(hash)).toBe(true); - }); - - it('different phone numbers produce different hashes', () => { - const hash1 = FieldEncryptionService.hashPhoneNumber('+14155552671'); - const hash2 = FieldEncryptionService.hashPhoneNumber('+442071234567'); - expect(hash1).not.toBe(hash2); - }); - }); - - describe('configuration', () => { - it('uses custom config when provided', () => { - const customService = new MixpanelService({ - token: 'custom-token', - apiHost: 'custom.api.com', - enableLogging: true, - }); - - const config = customService.getConfig(); - expect(config.token).toBe('custom-token'); - expect(config.apiHost).toBe('custom.api.com'); - expect(config.enableLogging).toBe(true); - }); - - it('uses default config when no config provided', () => { - const defaultService = new MixpanelService(); - const config = defaultService.getConfig(); - expect(config.apiHost).toBe('api.mixpanel.com'); - }); - }); - - describe('event management', () => { - it('clearEvents removes all stored events', async () => { - mockFetch.mockResolvedValue({ - ok: true, - status: 200, - } as Response); - - const event: SpamBlockedEvent = { - phoneNumber: '+14155552671', - decision: 'BLOCK', - confidence: 0.9, - timestamp: new Date(), - }; - - await service.spamBlocked(event); - expect(service.getEvents()).toHaveLength(1); - - service.clearEvents(); - expect(service.getEvents()).toHaveLength(0); - }); - }); -}); diff --git a/services/spamshield/test/regex-validation.test.ts b/services/spamshield/test/regex-validation.test.ts deleted file mode 100644 index 7d6e2c0..0000000 --- a/services/spamshield/test/regex-validation.test.ts +++ /dev/null @@ -1,265 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - validateRegexPattern, - analyzeRegexComplexity, - isSafeRegexPattern, - RegexValidationError, -} from '../src/utils/regex-validation'; - -describe('Regex Validation', () => { - describe('validateRegexPattern', () => { - describe('syntax validation', () => { - it('accepts valid simple patterns', () => { - expect(() => validateRegexPattern('\\d{3}-\\d{3}-\\d{4}')).not.toThrow(); - }); - - it('accepts valid phone number patterns', () => { - expect(() => validateRegexPattern('^\\+1\\d{10}$')).not.toThrow(); - }); - - it('accepts valid character class patterns', () => { - expect(() => validateRegexPattern('^[a-zA-Z0-9]+$')).not.toThrow(); - }); - - it('accepts valid alternation patterns', () => { - expect(() => validateRegexPattern('^(phone|fax|mobile)$')).not.toThrow(); - }); - - it('rejects invalid regex syntax', () => { - expect(() => validateRegexPattern('(unclosed')).toThrow(RegexValidationError); - }); - - it('rejects empty pattern', () => { - expect(() => validateRegexPattern('')).toThrow(RegexValidationError); - }); - }); - - describe('ReDoS detection - nested quantifiers', () => { - it('detects nested quantifier (a+)*', () => { - expect(() => validateRegexPattern('(a+)*')).toThrow(RegexValidationError); - }); - - it('detects nested quantifier (a*)+', () => { - expect(() => validateRegexPattern('(a*)+')).toThrow(RegexValidationError); - }); - - it('detects nested quantifier ([0-9]+)+', () => { - expect(() => validateRegexPattern('([0-9]+)+')).toThrow(RegexValidationError); - }); - - it('detects nested quantifier (a|b+)*', () => { - expect(() => validateRegexPattern('(a|b+)*')).toThrow(RegexValidationError); - }); - }); - - describe('ReDoS detection - overlapping alternations', () => { - it('detects overlapping alternation (a|aa)*', () => { - expect(() => validateRegexPattern('(a|aa)*')).toThrow(RegexValidationError); - }); - - it('detects overlapping alternation (abc|ab|a)*', () => { - expect(() => validateRegexPattern('(abc|ab|a)*')).toThrow(RegexValidationError); - }); - - it('detects overlapping alternation (foo|foot)*', () => { - expect(() => validateRegexPattern('(foo|foot)*')).toThrow(RegexValidationError); - }); - }); - - describe('ReDoS detection - complexity limits', () => { - it('rejects pattern exceeding max length', () => { - const longPattern = 'a'.repeat(600); - expect(() => validateRegexPattern(longPattern)).toThrow(RegexValidationError); - }); - - it('rejects pattern exceeding max nesting depth', () => { - const deepPattern = '((((' + '((((a))))' + '))))'; - expect(() => validateRegexPattern(deepPattern, { maxNestingDepth: 3 })).toThrow(RegexValidationError); - }); - - it('rejects pattern exceeding max alternations', () => { - const manyAlts = '(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x)'; - expect(() => validateRegexPattern(manyAlts, { maxAlternations: 5 })).toThrow(RegexValidationError); - }); - }); - - describe('safe patterns', () => { - it('accepts anchored phone pattern', () => { - const metrics = validateRegexPattern('^\\+\\d{1,3}\\d{4,14}$'); - expect(metrics.isRedosProne).toBe(false); - }); - - it('accepts simple character class', () => { - const metrics = validateRegexPattern('^[A-Z]{2}\\d{6}$'); - expect(metrics.isRedosProne).toBe(false); - }); - - it('accepts non-overlapping alternation', () => { - const metrics = validateRegexPattern('^(spammer|blocker|filter)$'); - expect(metrics.isRedosProne).toBe(false); - }); - - it('accepts escaped special characters', () => { - const metrics = validateRegexPattern('\\(\\d{3}\\) \\d{3}-\\d{4}'); - expect(metrics.isRedosProne).toBe(false); - }); - }); - }); - - describe('analyzeRegexComplexity', () => { - it('returns correct metrics for simple pattern', () => { - const metrics = analyzeRegexComplexity('\\d{3}-\\d{3}-\\d{4}'); - expect(metrics.length).toBe(17); - expect(metrics.nestingDepth).toBe(0); - expect(metrics.alternationCount).toBe(0); - expect(metrics.groupCount).toBe(0); - }); - - it('returns correct metrics for pattern with groups', () => { - const metrics = analyzeRegexComplexity('(\\d{3})-(\\d{3})-(\\d{4})'); - expect(metrics.groupCount).toBe(3); - expect(metrics.nestingDepth).toBe(1); - }); - - it('returns correct metrics for pattern with alternations', () => { - const metrics = analyzeRegexComplexity('(phone|fax|mobile)'); - expect(metrics.alternationCount).toBe(2); - }); - - it('marks nested quantifier as ReDoS prone', () => { - const metrics = analyzeRegexComplexity('(a+)*'); - expect(metrics.isRedosProne).toBe(true); - expect(metrics.issues.length).toBeGreaterThan(0); - }); - - it('marks overlapping alternation as ReDoS prone', () => { - const metrics = analyzeRegexComplexity('(a|aa)*'); - expect(metrics.isRedosProne).toBe(true); - }); - - it('marks long pattern as ReDoS prone', () => { - const longPattern = 'a'.repeat(600); - const metrics = analyzeRegexComplexity(longPattern); - expect(metrics.isRedosProne).toBe(true); - }); - - it('marks deep nesting as ReDoS prone', () => { - const deepPattern = '((((' + '((((a))))' + '))))'; - const metrics = analyzeRegexComplexity(deepPattern, { maxNestingDepth: 3 }); - expect(metrics.isRedosProne).toBe(true); - }); - - it('marks high alternation count as ReDoS prone', () => { - const manyAlts = '(a|b|c|d|e|f|g|h|i|j|k|l|m|n|o|p|q|r|s|t|u|v|w|x)'; - const metrics = analyzeRegexComplexity(manyAlts, { maxAlternations: 5 }); - expect(metrics.isRedosProne).toBe(true); - }); - }); - - describe('isSafeRegexPattern', () => { - it('returns true for safe patterns', () => { - expect(isSafeRegexPattern('^\\+\\d{1,3}\\d{4,14}$')).toBe(true); - expect(isSafeRegexPattern('^[A-Z]{2}\\d{6}$')).toBe(true); - expect(isSafeRegexPattern('\\d{3}-\\d{3}-\\d{4}')).toBe(true); - }); - - it('returns false for nested quantifiers', () => { - expect(isSafeRegexPattern('(a+)*')).toBe(false); - expect(isSafeRegexPattern('([0-9]+)+')).toBe(false); - }); - - it('returns false for overlapping alternations', () => { - expect(isSafeRegexPattern('(a|aa)*')).toBe(false); - expect(isSafeRegexPattern('(foo|foot)*')).toBe(false); - }); - - it('returns false for invalid syntax', () => { - expect(isSafeRegexPattern('(unclosed')).toBe(false); - }); - - it('returns false for empty pattern', () => { - expect(isSafeRegexPattern('')).toBe(false); - }); - - it('returns false for excessively long patterns', () => { - expect(isSafeRegexPattern('a'.repeat(600))).toBe(false); - }); - }); - - describe('RegexValidationError', () => { - it('includes pattern and reason in error', () => { - const pattern = '(a+)*'; - try { - validateRegexPattern(pattern); - } catch (err) { - const validationErr = err as RegexValidationError; - expect(validationErr.name).toBe('RegexValidationError'); - expect(validationErr.pattern).toBe(pattern); - expect(validationErr.reason).toContain('ReDoS risk'); - } - }); - - it('includes syntax error message', () => { - const pattern = '(unclosed'; - try { - validateRegexPattern(pattern); - } catch (err) { - const validationErr = err as RegexValidationError; - expect(validationErr.pattern).toBe(pattern); - expect(validationErr.reason).toContain('Invalid regex syntax'); - } - }); - - it('includes length error message', () => { - const longPattern = 'a'.repeat(600); - try { - validateRegexPattern(longPattern); - } catch (err) { - const validationErr = err as RegexValidationError; - expect(validationErr.reason).toContain('exceeds maximum'); - } - }); - }); - - describe('edge cases', () => { - it('handles escaped characters correctly', () => { - expect(() => validateRegexPattern('\\(\\d+\\)')).not.toThrow(); - }); - - it('handles character classes without false positives', () => { - expect(() => validateRegexPattern('[a-z]+')).not.toThrow(); - expect(() => validateRegexPattern('[^0-9]+')).not.toThrow(); - expect(() => validateRegexPattern('[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+')).not.toThrow(); - }); - - it('handles non-capturing groups', () => { - expect(() => validateRegexPattern('(?:abc)+')).not.toThrow(); - }); - - it('handles lookaheads', () => { - expect(() => validateRegexPattern('(?=\\d{3})\\d+')).not.toThrow(); - }); - - it('handles quantifiers with ranges', () => { - expect(() => validateRegexPattern('\\d{1,3}')).not.toThrow(); - expect(() => validateRegexPattern('[a-z]{2,4}')).not.toThrow(); - }); - - it('handles Unicode property escapes', () => { - expect(() => validateRegexPattern('\\p{L}+')).not.toThrow(); - }); - - it('handles multiline and dotall flags in pattern', () => { - expect(() => validateRegexPattern('^.+$')).not.toThrow(); - }); - - it('counts quantifiers correctly', () => { - const metrics = analyzeRegexComplexity('a+b*c?d{2}'); - expect(metrics.quantifierCount).toBe(4); - }); - - it('handles special characters in character classes', () => { - expect(() => validateRegexPattern('[^\\w\\s]+')).not.toThrow(); - }); - }); -}); diff --git a/services/spamshield/test/spamshield.test.ts b/services/spamshield/test/spamshield.test.ts deleted file mode 100644 index a6271f5..0000000 --- a/services/spamshield/test/spamshield.test.ts +++ /dev/null @@ -1,532 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { SpamShieldService } from '../src/services/spamshield.service'; -import { spamConfig, checkFeatureFlag, spamFeatureFlags, spamFeatureFlagDefaults } from '../src/config/spamshield.config'; -import { validatePhoneNumber } from '../src/utils/phone-validation'; - -const mockFetch = vi.fn(); -global.fetch = mockFetch as unknown as typeof global.fetch; - -describe('SpamShieldService', () => { - let service: SpamShieldService; - - beforeEach(() => { - service = SpamShieldService.getInstance(); - service.resetCircuits(); - mockFetch.mockReset(); - vi.clearAllMocks(); - }); - - describe('checkReputation', () => { - it('combines scores from both Hiya and Truecaller', async () => { - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ spamScore: 0.8 }), - } as Response) - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ spamProbability: 0.9 }), - } as Response); - - const result = await service.checkReputation('+14155552671'); - - expect(result.source).toBe('combined'); - expect(result.score).toBeCloseTo(0.85, 2); - expect(result.isSpam).toBe(true); - expect(result.hiyaScore).toBe(0.8); - expect(result.truecallerScore).toBe(0.9); - expect(mockFetch).toHaveBeenCalledTimes(2); - }); - - it('uses Hiya score + Truecaller fallback when Truecaller API fails', async () => { - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ spamScore: 0.6 }), - } as Response) - .mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: 'Internal Server Error', - } as Response); - - const result = await service.checkReputation('+14155552671'); - - expect(result.source).toBe('combined'); - expect(result.hiyaScore).toBe(0.6); - expect(result.truecallerScore).toBe(0.5); - }); - - it('uses Hiya fallback + Truecaller score when Hiya API fails', async () => { - mockFetch - .mockResolvedValueOnce({ - ok: false, - status: 429, - statusText: 'Too Many Requests', - } as Response) - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ spamProbability: 0.3 }), - } as Response); - - const result = await service.checkReputation('+14155552671'); - - expect(result.source).toBe('combined'); - expect(result.hiyaScore).toBe(0.5); - expect(result.truecallerScore).toBe(0.3); - }); - - it('uses both fallbacks when both APIs fail', async () => { - mockFetch - .mockResolvedValueOnce({ - ok: false, - status: 503, - statusText: 'Service Unavailable', - } as Response) - .mockResolvedValueOnce({ - ok: false, - status: 503, - statusText: 'Service Unavailable', - } as Response); - - const result = await service.checkReputation('+14155552671'); - - expect(result.source).toBe('combined'); - expect(result.hiyaScore).toBe(0.5); - expect(result.truecallerScore).toBe(0.5); - expect(result.score).toBe(0.5); - expect(result.isSpam).toBe(false); - }); - - it('validates phone number E.164 format', async () => { - const shortNumber = '123'; - const result = service.checkReputation(shortNumber); - await expect(result).rejects.toThrow('Invalid E.164 phone number format'); - }); - - it('returns non-spam for low scores', async () => { - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ spamScore: 0.2 }), - } as Response) - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ spamProbability: 0.1 }), - } as Response); - - const result = await service.checkReputation('+14155552671'); - - expect(result.isSpam).toBe(false); - expect(result.score).toBeCloseTo(0.15, 2); - }); - }); - - describe('circuit breaker integration', () => { - it('opens circuit after consecutive failures', async () => { - const metricsBefore = service.getCircuitMetrics(); - expect(metricsBefore.hiya.state).toBe('CLOSED'); - - for (let i = 0; i < spamConfig.circuitBreakerThreshold; i++) { - mockFetch - .mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: 'Server Error', - } as Response) - .mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: 'Server Error', - } as Response); - await service.checkReputation('+14155552671'); - } - - const metricsAfter = service.getCircuitMetrics(); - expect(metricsAfter.hiya.totalFailures).toBeGreaterThanOrEqual(spamConfig.circuitBreakerThreshold); - expect(metricsAfter.truecaller.totalFailures).toBeGreaterThanOrEqual(spamConfig.circuitBreakerThreshold); - }); - - it('exposes circuit metrics for monitoring', () => { - const metrics = service.getCircuitMetrics(); - - expect(metrics.hiya).toHaveProperty('state', 'CLOSED'); - expect(metrics.hiya).toHaveProperty('failureCount'); - expect(metrics.hiya).toHaveProperty('successCount'); - expect(metrics.hiya).toHaveProperty('totalExecutions'); - expect(metrics.hiya).toHaveProperty('totalFailures'); - expect(metrics.hiya).toHaveProperty('totalSuccesses'); - expect(metrics.hiya).toHaveProperty('lastFailureTime'); - expect(metrics.hiya).toHaveProperty('lastSuccessTime'); - expect(metrics.hiya).toHaveProperty('stateChangedAt'); - - expect(metrics.truecaller).toHaveProperty('state', 'CLOSED'); - }); - - it('resets circuits to CLOSED state', () => { - service.resetCircuits(); - const metrics = service.getCircuitMetrics(); - expect(metrics.hiya.state).toBe('CLOSED'); - expect(metrics.truecaller.state).toBe('CLOSED'); - }); - - it('returns fallback scores when circuits are open', async () => { - service.resetCircuits(); - - for (let i = 0; i < spamConfig.circuitBreakerThreshold; i++) { - mockFetch - .mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: 'Server Error', - } as Response) - .mockResolvedValueOnce({ - ok: false, - status: 500, - statusText: 'Server Error', - } as Response); - await service.checkReputation('+14155552671'); - } - - const metrics = service.getCircuitMetrics(); - expect(metrics.hiya.state).toBe('OPEN'); - expect(metrics.truecaller.state).toBe('OPEN'); - - const result = await service.checkReputation('+14155552671'); - expect(result.hiyaScore).toBe(0.5); - expect(result.truecallerScore).toBe(0.5); - }); - }); - - describe('E.164 phone number validation', () => { - describe('valid E.164 formats', () => { - it('accepts US number in E.164 format', () => { - expect(() => validatePhoneNumber('+14155552671')).not.toThrow(); - }); - - it('accepts UK number in E.164 format', () => { - expect(() => validatePhoneNumber('+442071234567')).not.toThrow(); - }); - - it('accepts German number in E.164 format', () => { - expect(() => validatePhoneNumber('+4930123456789')).not.toThrow(); - }); - - it('accepts Japanese number in E.164 format', () => { - expect(() => validatePhoneNumber('+81312345678')).not.toThrow(); - }); - - it('accepts Australian number in E.164 format', () => { - expect(() => validatePhoneNumber('+61412345678')).not.toThrow(); - }); - - it('accepts Indian number in E.164 format', () => { - expect(() => validatePhoneNumber('+919876543210')).not.toThrow(); - }); - - it('accepts Brazilian number in E.164 format', () => { - expect(() => validatePhoneNumber('+5511987654321')).not.toThrow(); - }); - - it('accepts number with spaces and normalizes', () => { - const result = validatePhoneNumber('+1 415 555 2671'); - expect(result).toBe('+14155552671'); - }); - - it('accepts number with dashes and normalizes', () => { - const result = validatePhoneNumber('+1-415-555-2671'); - expect(result).toBe('+14155552671'); - }); - - it('accepts number with parentheses and normalizes', () => { - const result = validatePhoneNumber('+1 (415) 555-2671'); - expect(result).toBe('+14155552671'); - }); - - it('trims whitespace from input', () => { - expect(() => validatePhoneNumber(' +14155552671 ')).not.toThrow(); - }); - }); - - describe('invalid E.164 formats', () => { - it('rejects number without plus sign', () => { - expect(() => validatePhoneNumber('14155552671')).toThrow('Invalid E.164 phone number format'); - }); - - it('rejects number with letters', () => { - expect(() => validatePhoneNumber('+1415555ABCD')).toThrow('Invalid E.164 phone number format'); - }); - - it('rejects empty string', () => { - expect(() => validatePhoneNumber('')).toThrow('Invalid E.164 phone number format'); - }); - - it('rejects too-short number', () => { - expect(() => validatePhoneNumber('+123')).toThrow('Invalid E.164 phone number format'); - }); - - it('rejects number exceeding 15 digits', () => { - expect(() => validatePhoneNumber('+1234567890123456')).toThrow('Invalid E.164 phone number format'); - }); - - it('rejects number with special characters in middle', () => { - expect(() => validatePhoneNumber('+1@4155552671')).toThrow('Invalid E.164 phone number format'); - }); - - it('rejects double plus sign', () => { - expect(() => validatePhoneNumber('++14155552671')).toThrow('Invalid E.164 phone number format'); - }); - - it('rejects only whitespace', () => { - expect(() => validatePhoneNumber(' ')).toThrow('Invalid E.164 phone number format'); - }); - - it('rejects number starting with zero after plus', () => { - expect(() => validatePhoneNumber('+01234567890')).toThrow('Invalid E.164 phone number format'); - }); - - it('rejects negative number format', () => { - expect(() => validatePhoneNumber('-14155552671')).toThrow('Invalid E.164 phone number format'); - }); - }); - - describe('integration with service methods', () => { - it('rejects invalid format in checkReputation', async () => { - await expect(service.checkReputation('4155552671')).rejects.toThrow('Invalid E.164 phone number format'); - }); - - it('rejects invalid format in analyzeCall', async () => { - const result = service.analyzeCall('4155552671', new Date()); - await expect(result).rejects.toThrow('Invalid E.164 phone number format'); - }); - - it('rejects invalid format in recordFeedback', async () => { - const result = service.recordFeedback('user123', '4155552671', true); - await expect(result).rejects.toThrow('Invalid E.164 phone number format'); - }); - }); - }); - - describe('feature flags', () => { - afterEach(() => { - for (const key of ['FLAG_ENABLEHIYAINTEGRATION', 'FLAG_ENABLETRUECALLERINTEGRATION', 'FLAG_ENABLESMSCLASSIFICATION', 'FLAG_ENABLECALLANALYSIS', 'FLAG_ENABLEFEEDBACKLOOP']) { - delete process.env[key]; - } - }); - - describe('checkFeatureFlag', () => { - it('returns default true when env var is unset', () => { - expect(checkFeatureFlag('enableHiyaIntegration')).toBe(true); - expect(checkFeatureFlag('enableCallAnalysis')).toBe(true); - expect(checkFeatureFlag('enableFeedbackLoop')).toBe(true); - }); - - it('returns true when env var is "true"', () => { - process.env.FLAG_ENABLECALLANALYSIS = 'true'; - expect(checkFeatureFlag('enableCallAnalysis')).toBe(true); - }); - - it('returns true when env var is "1"', () => { - process.env.FLAG_ENABLECALLANALYSIS = '1'; - expect(checkFeatureFlag('enableCallAnalysis')).toBe(true); - }); - - it('returns false when env var is "false"', () => { - process.env.FLAG_ENABLECALLANALYSIS = 'false'; - expect(checkFeatureFlag('enableCallAnalysis')).toBe(false); - }); - - it('returns false when env var is "0"', () => { - process.env.FLAG_ENABLECALLANALYSIS = '0'; - expect(checkFeatureFlag('enableCallAnalysis')).toBe(false); - }); - - it('returns false for any non-true/non-1 value', () => { - process.env.FLAG_ENABLECALLANALYSIS = 'yes'; - expect(checkFeatureFlag('enableCallAnalysis')).toBe(false); - }); - }); - - describe('spamFeatureFlags getters', () => { - it('reflects env var overrides for enableCallAnalysis', () => { - expect(spamFeatureFlags.enableCallAnalysis).toBe(true); - process.env.FLAG_ENABLECALLANALYSIS = 'false'; - expect(spamFeatureFlags.enableCallAnalysis).toBe(false); - }); - - it('reflects env var overrides for enableFeedbackLoop', () => { - expect(spamFeatureFlags.enableFeedbackLoop).toBe(true); - process.env.FLAG_ENABLEFEEDBACKLOOP = 'false'; - expect(spamFeatureFlags.enableFeedbackLoop).toBe(false); - }); - - it('reflects env var overrides for enableHiyaIntegration', () => { - expect(spamFeatureFlags.enableHiyaIntegration).toBe(true); - process.env.FLAG_ENABLEHIYAINTEGRATION = 'false'; - expect(spamFeatureFlags.enableHiyaIntegration).toBe(false); - }); - - it('reflects env var overrides for enableTruecallerIntegration', () => { - expect(spamFeatureFlags.enableTruecallerIntegration).toBe(true); - process.env.FLAG_ENABLETRUECALLERINTEGRATION = 'false'; - expect(spamFeatureFlags.enableTruecallerIntegration).toBe(false); - }); - - it('reflects env var overrides for enableSMSClassification', () => { - expect(spamFeatureFlags.enableSMSClassification).toBe(true); - process.env.FLAG_ENABLESMSCLASSIFICATION = 'false'; - expect(spamFeatureFlags.enableSMSClassification).toBe(false); - }); - }); - - describe('enableCallAnalysis flag', () => { - it('throws when call analysis is disabled in analyzeCall', async () => { - process.env.FLAG_ENABLECALLANALYSIS = 'false'; - const result = service.analyzeCall('+14155552671', new Date()); - await expect(result).rejects.toThrow('Call analysis disabled via feature flag'); - }); - - it('throws when call analysis is disabled in interceptCall', async () => { - process.env.FLAG_ENABLECALLANALYSIS = 'false'; - const call = { - callId: 'call-1', - phoneNumber: '+14155552671', - from: '+14155552671', - to: '+14155551234', - startTime: new Date(), - direction: 'inbound' as const, - carrierType: 'twilio' as any, - carrierSid: 'CA123', - }; - const result = service.interceptCall(call); - await expect(result).rejects.toThrow('Call analysis disabled via feature flag'); - }); - - it('passes flag check when call analysis is enabled', async () => { - process.env.FLAG_ENABLECALLANALYSIS = 'true'; - const result = service.analyzeCall('+14155552671', new Date()); - try { - const res = await result; - expect(res).toMatchObject({ - decision: 'ALLOW', - confidence: 0, - ruleMatches: [], - }); - } catch (e) { - expect((e as Error).message).not.toContain('Call analysis disabled'); - } - }); - }); - - describe('recordFeedback null checks', () => { - it('throws when userId is null', async () => { - process.env.FLAG_ENABLEFEEDBACKLOOP = 'true'; - const result = service.recordFeedback(null as any, '+14155552671', true); - await expect(result).rejects.toThrow('Feedback: userId is required'); - }); - - it('throws when userId is empty string', async () => { - process.env.FLAG_ENABLEFEEDBACKLOOP = 'true'; - const result = service.recordFeedback('', '+14155552671', true); - await expect(result).rejects.toThrow('Feedback: userId is required'); - }); - - it('throws when phoneNumber is null', async () => { - process.env.FLAG_ENABLEFEEDBACKLOOP = 'true'; - const result = service.recordFeedback('user123', null as any, true); - await expect(result).rejects.toThrow('Feedback: phoneNumber must be a non-empty string'); - }); - - it('throws when isSpam is null', async () => { - process.env.FLAG_ENABLEFEEDBACKLOOP = 'true'; - const result = service.recordFeedback('user123', '+14155552671', null as any); - await expect(result).rejects.toThrow('Feedback: isSpam must be a boolean'); - }); - - it('throws when isSpam is undefined', async () => { - process.env.FLAG_ENABLEFEEDBACKLOOP = 'true'; - const result = service.recordFeedback('user123', '+14155552671', undefined as any); - await expect(result).rejects.toThrow('Feedback: isSpam must be a boolean'); - }); - - it('throws when userId is undefined', async () => { - process.env.FLAG_ENABLEFEEDBACKLOOP = 'true'; - const result = service.recordFeedback(undefined as any, '+14155552671', true); - await expect(result).rejects.toThrow('Feedback: userId is required'); - }); - - it('throws when phoneNumber is undefined', async () => { - process.env.FLAG_ENABLEFEEDBACKLOOP = 'true'; - const result = service.recordFeedback('user123', undefined as any, true); - await expect(result).rejects.toThrow('Feedback: phoneNumber must be a non-empty string'); - }); - - it('handles null metadata gracefully (falls back to default)', async () => { - process.env.FLAG_ENABLEFEEDBACKLOOP = 'true'; - const result = service.recordFeedback('user123', '+14155552671', true, undefined, null as any); - try { - await result; - } catch (e) { - expect((e as Error).message).not.toContain('userId is required'); - expect((e as Error).message).not.toContain('isSpam must be a boolean'); - } - }); - }); - - describe('enableFeedbackLoop flag', () => { - it('throws when feedback loop is disabled in recordFeedback', async () => { - process.env.FLAG_ENABLEFEEDBACKLOOP = 'false'; - const result = service.recordFeedback('user123', '+14155552671', true); - await expect(result).rejects.toThrow('Feedback loop disabled via feature flag'); - }); - - it('passes flag check when feedback loop is enabled', async () => { - process.env.FLAG_ENABLEFEEDBACKLOOP = 'true'; - const result = service.recordFeedback('user123', '+14155552671', true); - try { - await result; - } catch (e) { - expect((e as Error).message).not.toContain('Feedback loop disabled'); - } - }); - }); - - describe('enableHiyaIntegration flag', () => { - it('excludes Hiya score when integration is disabled', async () => { - service.resetCircuits(); - process.env.FLAG_ENABLEHIYAINTEGRATION = 'false'; - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ spamProbability: 0.3 }), - } as Response); - const result = await service.checkReputation('+14155552671'); - expect(result.hiyaScore).toBe(undefined); - expect(result.source).toBe('truecaller'); - }); - }); - - describe('enableTruecallerIntegration flag', () => { - it('excludes Truecaller score when integration is disabled', async () => { - service.resetCircuits(); - process.env.FLAG_ENABLETRUECALLERINTEGRATION = 'false'; - mockFetch - .mockResolvedValueOnce({ - ok: true, - json: async () => ({ spamScore: 0.8 }), - } as Response); - const result = await service.checkReputation('+14155552671'); - expect(result.source).toBe('hiya'); - expect(result.truecallerScore).toBe(undefined); - }); - }); - - describe('enableSMSClassification flag', () => { - it('throws when SMS classification is disabled in classifySms', async () => { - process.env.FLAG_ENABLESMSCLASSIFICATION = 'false'; - const result = service.classifySms('test message'); - await expect(result).rejects.toThrow('SMS Classification disabled via feature flag'); - }); - }); - }); -}); diff --git a/services/spamshield/tsconfig.json b/services/spamshield/tsconfig.json deleted file mode 100644 index bbd11d9..0000000 --- a/services/spamshield/tsconfig.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "baseUrl": "./src", - "paths": { - "@shieldai/db": ["../../packages/db/src/index.ts"], - "@shieldai/db/*": ["../../packages/db/src/*"] - } - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/services/spamshield/vitest.config.ts b/services/spamshield/vitest.config.ts deleted file mode 100644 index fe01a0a..0000000 --- a/services/spamshield/vitest.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['src/**/*.test.ts', 'test/**/*.test.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - reportsDirectory: './coverage', - include: ['src/**/*.ts'], - exclude: [ - 'src/**/*.d.ts', - '**/node_modules/**', - '**/test/**', - ], - thresholds: { - statements: 80, - branches: 80, - functions: 80, - lines: 80, - }, - }, - }, -}); diff --git a/services/voiceprint/Dockerfile b/services/voiceprint/Dockerfile deleted file mode 100644 index 8d1e385..0000000 --- a/services/voiceprint/Dockerfile +++ /dev/null @@ -1,44 +0,0 @@ -FROM node:20-alpine AS builder - -WORKDIR /app - -COPY package.json pnpm-lock.yaml turbo.json pnpm-workspace.yaml ./ -COPY packages/api/package.json ./packages/api/ -COPY packages/db/package.json ./packages/db/ -COPY packages/types/package.json ./packages/types/ -COPY packages/core/package.json ./packages/core/ 2>/dev/null || true -COPY packages/jobs/package.json ./packages/jobs/ -COPY packages/shared-notifications/package.json ./packages/shared-notifications/ -COPY services/darkwatch/package.json ./services/darkwatch/ -COPY services/spamshield/package.json ./services/spamshield/ -COPY services/voiceprint/package.json ./services/voiceprint/ - -RUN npm i -g pnpm@9 && pnpm install --frozen-lockfile - -COPY tsconfig.json ./ -COPY packages/types/tsconfig.json ./packages/types/ -COPY packages/db/tsconfig.json ./packages/db/ -COPY services/voiceprint/tsconfig.json ./services/voiceprint/ -COPY services/voiceprint/ ./services/voiceprint/ -COPY packages/types/ ./packages/types/ -COPY packages/db/ ./packages/db/ - -RUN pnpm build --filter=@shieldai/types --filter=@shieldai/db --filter=@shieldai/voiceprint - -FROM node:20-alpine AS runner - -WORKDIR /app - -RUN addgroup --system --gid 1001 nodejs && \ - adduser --system --uid 1001 shieldai - -COPY --from=builder --chown=shieldai:nodejs /app/services/voiceprint/dist ./dist -COPY --from=builder --chown=shieldai:nodejs /app/node_modules ./node_modules -COPY --from=builder --chown=shieldai:nodejs /app/services/voiceprint/package.json ./package.json -COPY --from=builder --chown=shieldai:nodejs /app/packages/db ./packages/db - -USER shieldai - -EXPOSE 3003 - -CMD ["node", "dist/index.js"] diff --git a/services/voiceprint/package.json b/services/voiceprint/package.json deleted file mode 100644 index c3f71af..0000000 --- a/services/voiceprint/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@shieldai/voiceprint", - "version": "0.1.0", - "main": "./dist/index.js", - "types": "./dist/index.js", - "scripts": { - "build": "tsc", - "test": "vitest run", - "test:coverage": "vitest run --coverage", - "lint": "eslint src/" - }, - "dependencies": { - "@shieldai/correlation": "workspace:*", - "@shieldai/db": "workspace:*", - "@shieldai/types": "workspace:*", - "@types/uuid": "^11.0.0", - "node-cache": "^5.1.2", - "uuid": "^14.0.0" - }, - "devDependencies": { - "@vitest/coverage-v8": "^4.1.5", - "vitest": "^4.1.5" - }, - "exports": { - ".": "./src/index.ts" - } -} diff --git a/services/voiceprint/src/analysis/AnalysisService.ts b/services/voiceprint/src/analysis/AnalysisService.ts deleted file mode 100644 index 595c20a..0000000 --- a/services/voiceprint/src/analysis/AnalysisService.ts +++ /dev/null @@ -1,197 +0,0 @@ -import prisma from "@shieldai/db"; -import { AudioPreprocessor, AudioFeatures } from "../preprocessor/AudioPreprocessor"; -import { EmbeddingService, EmbeddingOutput } from "../embedding/EmbeddingService"; -import { VoiceEnrollmentService } from "../enrollment/VoiceEnrollmentService"; -import { emitVoicePrintAlert } from "@shieldai/correlation"; -import { - AnalyzeAudioInput, - AnalysisJobStatus, - AnalysisType, - DetectionVerdict, - AnalysisResultOutput, -} from "@shieldai/types"; - -export class AnalysisService { - private preprocessor: AudioPreprocessor; - private embeddingService: EmbeddingService; - private enrollmentService: VoiceEnrollmentService; - private readonly syntheticThreshold = 0.7; - private readonly uncertainThreshold = 0.4; - - constructor() { - this.preprocessor = new AudioPreprocessor(); - this.embeddingService = new EmbeddingService(); - this.enrollmentService = new VoiceEnrollmentService(); - } - - async analyze(input: AnalyzeAudioInput, userId: string): Promise { - const startTime = Date.now(); - - const job = await prisma.analysisJob.create({ - data: { - userId, - analysisType: input.analysisType || AnalysisType.SYNTHETIC_DETECTION, - audioFilePath: `voiceprint/${userId}/${Date.now()}.wav`, - status: AnalysisJobStatus.RUNNING, - }, - }); - - try { - const preprocessed = await this.preprocessor.preprocess(input.audioBuffer, input.sampleRate); - const features = await this.preprocessor.extractFeatures(preprocessed.audio); - const embedding = await this.embeddingService.extract(preprocessed.audio); - - const syntheticScore = await this.classifySynthetic(features, embedding); - const verdict = this.determineVerdict(syntheticScore); - const confidence = this.computeConfidence(syntheticScore, verdict); - - let matchedEnrollmentId: string | undefined; - let matchedSimilarity: number | undefined; - - if (input.analysisType === AnalysisType.VOICE_MATCH) { - const match = await this.enrollmentService.matchVoice(input.audioBuffer, userId); - if (match) { - matchedEnrollmentId = match.enrollmentId; - matchedSimilarity = match.similarity; - } - } - - const processingTimeMs = Date.now() - startTime; - - const result = await prisma.analysisResult.create({ - data: { - analysisJobId: job.id, - syntheticScore, - verdict, - confidence, - processingTimeMs, - matchedEnrollmentId, - matchedSimilarity, - modelVersion: this.embeddingService.getModelVersion(), - }, - }); - - await prisma.analysisJob.update({ - where: { id: job.id }, - data: { - status: AnalysisJobStatus.COMPLETED, - completedAt: new Date(), - }, - }); - - if (result.verdict === DetectionVerdict.SYNTHETIC || result.verdict === DetectionVerdict.UNCERTAIN) { - emitVoicePrintAlert( - userId, - job.id, - result.verdict, - result.syntheticScore, - result.confidence, - result.matchedEnrollmentId || undefined, - result.matchedSimilarity || undefined, - input.analysisType || undefined - ).catch((err) => console.error(`[Correlation] VoicePrint emit failed:`, err)); - } - - return { - jobId: job.id, - syntheticScore: result.syntheticScore, - verdict: result.verdict, - confidence: result.confidence, - matchedEnrollmentId: result.matchedEnrollmentId || undefined, - matchedSimilarity: result.matchedSimilarity || undefined, - processingTimeMs: result.processingTimeMs, - modelVersion: result.modelVersion || undefined, - }; - } catch (err) { - const message = err instanceof Error ? err.message : "Analysis failed"; - await prisma.analysisJob.update({ - where: { id: job.id }, - data: { - status: AnalysisJobStatus.FAILED, - errorMessage: message, - completedAt: new Date(), - }, - }); - throw err; - } - } - - async getResult(jobId: string): Promise { - const job = await prisma.analysisJob.findUnique({ - where: { id: jobId }, - include: { result: true }, - }); - - if (!job || !job.result) return null; - - const r = job.result; - return { - jobId, - syntheticScore: r.syntheticScore, - verdict: r.verdict, - confidence: r.confidence, - matchedEnrollmentId: r.matchedEnrollmentId || undefined, - matchedSimilarity: r.matchedSimilarity || undefined, - processingTimeMs: r.processingTimeMs, - modelVersion: r.modelVersion || undefined, - }; - } - - async getUserResults(userId: string, limit: number = 20): Promise { - const jobs = await prisma.analysisJob.findMany({ - where: { userId, status: AnalysisJobStatus.COMPLETED }, - orderBy: { createdAt: "desc" }, - take: limit, - include: { result: true }, - }); - - return jobs - .filter((j) => j.result) - .map((j) => { - const r = j.result!; - return { - jobId: j.id, - syntheticScore: r.syntheticScore, - verdict: r.verdict, - confidence: r.confidence, - matchedEnrollmentId: r.matchedEnrollmentId || undefined, - matchedSimilarity: r.matchedSimilarity || undefined, - processingTimeMs: r.processingTimeMs, - modelVersion: r.modelVersion || undefined, - }; - }); - } - - private async classifySynthetic( - features: AudioFeatures, - embedding: EmbeddingOutput - ): Promise { - const modelScore = await this.embeddingService.classify(embedding.vector); - - const zcrAnomaly = Math.abs(features.zeroCrossingRate - 0.05) / 0.05; - const spectralAnomaly = Math.abs(features.spectralCentroid - 500) / 500; - - const artifactScore = Math.min(1, (zcrAnomaly + spectralAnomaly) / 4); - - return 0.7 * modelScore + 0.3 * artifactScore; - } - - private determineVerdict(score: number): DetectionVerdict { - if (score >= this.syntheticThreshold) return DetectionVerdict.SYNTHETIC; - if (score <= this.uncertainThreshold) return DetectionVerdict.NATURAL; - return DetectionVerdict.UNCERTAIN; - } - - private computeConfidence(score: number, verdict: DetectionVerdict): number { - if (verdict === DetectionVerdict.SYNTHETIC) { - return Math.min(1, (score - this.syntheticThreshold) / (1 - this.syntheticThreshold)); - } - if (verdict === DetectionVerdict.NATURAL) { - return Math.min(1, (this.uncertainThreshold - score) / this.uncertainThreshold); - } - return 1 - Math.min( - Math.abs(score - this.uncertainThreshold) / (this.syntheticThreshold - this.uncertainThreshold), - 1 - ); - } -} diff --git a/services/voiceprint/src/analysis/BatchAnalysisService.ts b/services/voiceprint/src/analysis/BatchAnalysisService.ts deleted file mode 100644 index 17a74a0..0000000 --- a/services/voiceprint/src/analysis/BatchAnalysisService.ts +++ /dev/null @@ -1,140 +0,0 @@ -import prisma from "@shieldai/db"; -import { AnalysisService } from "./AnalysisService"; -import { - BatchAnalyzeInput, - AnalysisJobStatus, - AnalysisType, - AnalysisResultOutput, -} from "@shieldai/types"; -import { logger } from "../logger"; - -export class BatchAnalysisService { - private analysisService: AnalysisService; - private readonly maxConcurrency = 5; - - constructor() { - this.analysisService = new AnalysisService(); - } - - async analyzeBatch( - input: BatchAnalyzeInput, - userId: string - ): Promise { - const batchId = `batch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; - logger.info("Starting batch analysis", { batchId, userId, totalFiles: input.audioBuffers.length }); - const results: AnalysisResultOutput[] = []; - const errors: Array<{ name: string; error: string }> = []; - - const processWithConcurrency = async (limit: number) => { - for (let i = 0; i < input.audioBuffers.length; i += limit) { - const chunk = input.audioBuffers.slice(i, i + limit); - - const promises = chunk.map(async (audioInput: { name: string; buffer: Buffer; sampleRate?: number }) => { - try { - const result = await this.analysisService.analyze( - { - audioBuffer: audioInput.buffer, - sampleRate: audioInput.sampleRate, - analysisType: input.analysisType || AnalysisType.SYNTHETIC_DETECTION, - }, - userId - ); - return { success: true, result, name: audioInput.name }; - } catch (err) { - const message = err instanceof Error ? err.message : "Analysis failed"; - return { success: false, error: message, name: audioInput.name }; - } - }); - - const outcomes = await Promise.allSettled(promises); - - for (const outcome of outcomes) { - if (outcome.status === 'fulfilled') { - if (outcome.value.success && outcome.value.result) { - results.push(outcome.value.result); - } else if (!outcome.value.success && outcome.value.name) { - errors.push({ name: outcome.value.name, error: outcome.value.error || "Analysis failed" }); - } - } - } - } - }; - - await processWithConcurrency(this.maxConcurrency); - - logger.info("Batch analysis completed", { - batchId, - successfulResults: results.length, - failedCount: errors.length - }); - - return { - batchId, - jobId: `batch_${batchId}`, - totalFiles: input.audioBuffers.length, - successfulResults: results.length, - failedCount: errors.length, - results, - errors, - }; - } - - async getBatchResult(batchJobId: string): Promise { - const job = await prisma.analysisJob.findUnique({ - where: { id: batchJobId }, - include: { result: true }, - }); - - if (!job) return null; - - const childJobs = await prisma.analysisJob.findMany({ - where: { - userId: job.userId, - createdAt: { gte: job.createdAt, lt: new Date(job.createdAt.getTime() + 60000) }, - id: { not: job.id }, - }, - include: { result: true }, - }); - - const results: AnalysisResultOutput[] = []; - const errors: Array<{ name: string; error: string }> = []; - - for (const childJob of childJobs) { - if (childJob.result) { - const r = childJob.result; - results.push({ - jobId: childJob.id, - syntheticScore: r.syntheticScore, - verdict: r.verdict, - confidence: r.confidence, - matchedEnrollmentId: r.matchedEnrollmentId || undefined, - matchedSimilarity: r.matchedSimilarity || undefined, - processingTimeMs: r.processingTimeMs, - modelVersion: r.modelVersion || undefined, - }); - } else if (childJob.errorMessage) { - errors.push({ name: childJob.audioFilePath, error: childJob.errorMessage }); - } - } - - return { - batchId: job.audioFilePath.split("/").pop() || job.id, - jobId: job.id, - totalFiles: childJobs.length, - successfulResults: results.length, - failedCount: errors.length, - results, - errors, - }; - } -} - -export interface BatchResult { - batchId: string; - jobId: string; - totalFiles: number; - successfulResults: number; - failedCount: number; - results: AnalysisResultOutput[]; - errors: Array<{ name: string; error: string }>; -} diff --git a/services/voiceprint/src/embedding/EmbeddingService.ts b/services/voiceprint/src/embedding/EmbeddingService.ts deleted file mode 100644 index 3c2a6c6..0000000 --- a/services/voiceprint/src/embedding/EmbeddingService.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { spawn } from "child_process"; -import { v4 as uuidv4 } from "uuid"; -import { logger } from "../logger"; - -const EMBEDDING_DIM = 192; -const MODEL_VERSION = "ecapa-tdnn-0.1.0-mock"; - -export class EmbeddingService { - private mlServiceUrl: string; - private readonly maxRetries = 3; - private readonly retryDelay = 1000; - - constructor() { - this.mlServiceUrl = process.env.VOICEPRINT_ML_URL || "http://localhost:8001"; - } - - async extract(audioBuffer: Buffer): Promise { - const mlAvailable = await this.checkMLService(); - - if (mlAvailable) { - logger.info("Using ML service for embedding extraction", { mlUrl: this.mlServiceUrl }); - return this.extractViaML(audioBuffer); - } - - logger.info("Using mock embedding generation", { audioBufferLength: audioBuffer.length }); - return this.generateMockFromBuffer(audioBuffer); - } - - async classify(embedding: number[]): Promise { - const mlAvailable = await this.checkMLService(); - - if (mlAvailable) { - logger.info("Using ML service for classification", { embeddingLength: embedding.length }); - return this.classifyViaML(embedding); - } - - logger.info("Using mock classification", { embeddingLength: embedding.length }); - const mean = embedding.reduce((s, v) => s + v, 0) / embedding.length; - const variance = embedding.reduce((s, v) => s + (v - mean) ** 2, 0) / embedding.length; - const stdDev = Math.sqrt(variance); - - const syntheticIndicators = [ - stdDev < 0.1 ? 0.8 : 0.2, - Math.abs(mean) > 0.5 ? 0.7 : 0.3, - this.hasArtifacts(embedding) ? 0.9 : 0.1, - ]; - - return syntheticIndicators.reduce((s, v) => s + v, 0) / syntheticIndicators.length; - } - - getModelVersion(): string { - return MODEL_VERSION; - } - - private async extractViaML(audioBuffer: Buffer): Promise { - return new Promise((resolve, reject) => { - const jsonInput = audioBuffer.toString("base64"); - const proc = spawn("python3", [ - "-c", - ` -import urllib.request, json, sys -req = urllib.request.Request( - "${this.mlServiceUrl}/embedding", - data=json.dumps({"audio": "${jsonInput.substring(0, 5000)}"}).encode(), - headers={"Content-Type": "application/json"} -) -try: - with urllib.request.urlopen(req, timeout=60) as resp: - data = json.loads(resp.read()) - sys.stdout.write(json.dumps({"ok": True, "vector": data.get("embedding", []), "dim": data.get("dimension", ${EMBEDDING_DIM})})) -except Exception as e: - sys.stdout.write(json.dumps({"ok": False, "error": str(e)})) -`, - ]); - - let output = ""; - proc.stdout.on("data", (chunk) => { output += chunk.toString(); }); - proc.on("close", (code) => { - try { - const result = JSON.parse(output); - if (result.ok && result.vector.length === EMBEDDING_DIM) { - resolve({ vector: result.vector, dimension: EMBEDDING_DIM }); - } else { - resolve(this.generateMockFromBuffer(audioBuffer)); - } - } catch { - resolve(this.generateMockFromBuffer(audioBuffer)); - } - }); - }); - } - - private async classifyViaML(embedding: number[]): Promise { - return new Promise((resolve) => { - const proc = spawn("python3", [ - "-c", - ` -import urllib.request, json, sys -req = urllib.request.Request( - "${this.mlServiceUrl}/classify", - data=json.dumps({"embedding": ${JSON.stringify(embedding)}}).encode(), - headers={"Content-Type": "application/json"} -) -try: - with urllib.request.urlopen(req, timeout=30) as resp: - data = json.loads(resp.read()) - sys.stdout.write(json.dumps({"score": data.get("synthetic_score", 0.5)})) -except: - sys.stdout.write(json.dumps({"score": 0.5})) -`, - ]); - - let output = ""; - proc.stdout.on("data", (chunk) => { output += chunk.toString(); }); - proc.on("close", () => { - try { - const result = JSON.parse(output); - resolve(result.score || 0.5); - } catch { - resolve(0.5); - } - }); - }); - } - - private hasArtifacts(embedding: number[]): boolean { - const window = 16; - let artifactCount = 0; - - for (let i = 0; i < embedding.length - window; i += window) { - const slice = embedding.slice(i, i + window); - const localMean = slice.reduce((s, v) => s + v, 0) / slice.length; - const localVar = slice.reduce((s, v) => s + (v - localMean) ** 2, 0) / slice.length; - - if (localVar < 0.001) artifactCount++; - } - - return artifactCount > embedding.length / window / 3; - } - - private generateMockFromBuffer(audioBuffer: Buffer): EmbeddingOutput { - let hash = 0; - const sampleSize = Math.min(audioBuffer.length, 1024); - for (let i = 0; i < sampleSize; i += 4) { - hash = ((hash << 5) - hash + audioBuffer.readInt32LE(i)) | 0; - } - const seed = Math.abs(hash); - - const rng = this.createRNG(seed); - const vector: number[] = []; - - for (let i = 0; i < EMBEDDING_DIM; i++) { - const u1 = rng(); - const u2 = rng(); - const gauss = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); - vector.push(parseFloat(gauss.toFixed(6))); - } - - const norm = Math.sqrt(vector.reduce((s, v) => s + v * v, 0)); - const normalized = vector.map((v) => parseFloat((v / norm).toFixed(6))); - - return { vector: normalized, dimension: EMBEDDING_DIM }; - } - - private async checkMLService(): Promise { - logger.info("Checking ML service availability", { mlUrl: this.mlServiceUrl }); - return new Promise((resolve) => { - const proc = spawn("python3", [ - "-c", - ` -import urllib.request, sys -try: - urllib.request.urlopen("${this.mlServiceUrl}/health", timeout=2) - sys.exit(0) -except: - sys.exit(1) -`, - ]); - proc.on("close", (code) => resolve(code === 0)); - }); - } - - private createRNG(seed: number): () => number { - return () => { - seed = (seed * 1664525 + 1013904223) & 0xffffffff; - return (seed >>> 0) / 0xffffffff; - }; - } -} - -export interface EmbeddingOutput { - vector: number[]; - dimension: number; -} diff --git a/services/voiceprint/src/enrollment/VoiceEnrollmentService.ts b/services/voiceprint/src/enrollment/VoiceEnrollmentService.ts deleted file mode 100644 index 35d4752..0000000 --- a/services/voiceprint/src/enrollment/VoiceEnrollmentService.ts +++ /dev/null @@ -1,153 +0,0 @@ -import prisma from "@shieldai/db"; -import { EmbeddingService } from "../embedding/EmbeddingService"; -import { FAISSIndex } from "../indexer/FAISSIndex"; -import { AudioPreprocessor } from "../preprocessor/AudioPreprocessor"; -import { VoiceEnrollmentInput, VoiceEnrollmentOutput } from "@shieldai/types"; - -export class VoiceEnrollmentService { - private embeddingService: EmbeddingService; - private faissIndex: FAISSIndex; - private preprocessor: AudioPreprocessor; - - constructor() { - this.embeddingService = new EmbeddingService(); - this.faissIndex = new FAISSIndex(); - this.preprocessor = new AudioPreprocessor(); - } - - async enroll(input: VoiceEnrollmentInput, userId: string): Promise { - const preprocessed = await this.preprocessor.preprocess(input.audioBuffer, input.sampleRate); - - const embedding = await this.embeddingService.extract(preprocessed.audio); - - const enrollment = await prisma.voiceEnrollment.create({ - data: { - userId, - name: input.label, - voiceHash: this.computeVoiceHash(embedding.vector), - audioMetadata: { - sampleRate: preprocessed.sampleRate, - durationSec: preprocessed.durationSec, - embeddingDim: embedding.dimension, - }, - }, - }); - - await this.faissIndex.add(enrollment.id, embedding.vector); - - return { - id: enrollment.id, - label: enrollment.name, - embeddingDim: preprocessed.sampleRate, - sampleRate: preprocessed.sampleRate, - durationSec: preprocessed.durationSec, - createdAt: enrollment.createdAt, - }; - } - - async listEnrollments(userId: string): Promise { - const enrollments = await prisma.voiceEnrollment.findMany({ - where: { userId }, - orderBy: { createdAt: "desc" }, - select: { - id: true, - label: true, - embeddingDim: true, - sampleRate: true, - durationSec: true, - createdAt: true, - }, - }); - - return enrollments.map((e) => ({ - id: e.id, - label: e.label, - embeddingDim: e.embeddingDim, - sampleRate: e.sampleRate, - durationSec: e.durationSec, - createdAt: e.createdAt, - })); - } - - async removeEnrollment(userId: string, enrollmentId: string): Promise { - const enrollment = await prisma.voiceEnrollment.findFirst({ - where: { id: enrollmentId, userId }, - }); - - if (!enrollment) { - throw new Error(`Enrollment ${enrollmentId} not found for user ${userId}`); - } - - await prisma.voiceEnrollment.delete({ where: { id: enrollmentId } }); - await this.faissIndex.remove(enrollmentId); - - return true; - } - - async getEnrollment(enrollmentId: string): Promise { - const enrollment = await prisma.voiceEnrollment.findUnique({ - where: { id: enrollmentId }, - select: { - id: true, - label: true, - embeddingDim: true, - sampleRate: true, - durationSec: true, - createdAt: true, - }, - }); - - if (!enrollment) return null; - - return { - id: enrollment.id, - label: enrollment.label, - embeddingDim: enrollment.embeddingDim, - sampleRate: enrollment.sampleRate, - durationSec: enrollment.durationSec, - createdAt: enrollment.createdAt, - }; - } - - async matchVoice( - audioBuffer: Buffer, - userId: string, - threshold: number = 0.75 - ): Promise { - const preprocessed = await this.preprocessor.preprocess(audioBuffer); - const embedding = await this.embeddingService.extract(preprocessed.audio); - - const matches = await this.faissIndex.search(embedding.vector, 5); - - const enrollmentIds = matches.map((m) => m.id); - const enrollments = await prisma.voiceEnrollment.findMany({ - where: { - id: { in: enrollmentIds }, - userId, - }, - }); - - let bestMatch: MatchResult | null = null; - - for (const match of matches) { - const enrollment = enrollments.find((e) => e.id === match.id); - if (enrollment && match.similarity >= threshold) { - if (!bestMatch || match.similarity > bestMatch.similarity) { - bestMatch = { - enrollmentId: enrollment.id, - label: enrollment.label, - similarity: match.similarity, - }; - } - } - } - - return bestMatch; - } -} - -export interface MatchResult { - enrollmentId: string; - label: string; - similarity: number; -} diff --git a/services/voiceprint/src/index.ts b/services/voiceprint/src/index.ts deleted file mode 100644 index 4d40bf7..0000000 --- a/services/voiceprint/src/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Config -export { - voicePrintEnv, - VoicePrintSource, - AnalysisJobStatus, - DetectionType, - ConfidenceLevel, - audioPreprocessingConfig, - voicePrintFeatureFlags, - voicePrintRateLimits, - checkFlag, - isFeatureEnabled, -} from './voiceprint.config'; - - - -// Services -export { - AudioPreprocessor, - VoiceEnrollmentService, - AnalysisService, - BatchAnalysisService, - EmbeddingService, - FAISSIndex, - audioPreprocessor, - voiceEnrollmentService, - analysisService, - batchAnalysisService, - embeddingService, -} from './voiceprint.service'; diff --git a/services/voiceprint/src/indexer/FAISSIndex.ts b/services/voiceprint/src/indexer/FAISSIndex.ts deleted file mode 100644 index 9b3109d..0000000 --- a/services/voiceprint/src/indexer/FAISSIndex.ts +++ /dev/null @@ -1,86 +0,0 @@ -export class FAISSIndex { - private store: Map = new Map(); - private readonly dimension = 192; - private initialized = false; - - async initialize(): Promise { - if (this.initialized) return; - this.initialized = true; - } - - async add(id: string, vector: number[]): Promise { - this.normalizeInPlace(vector); - this.store.set(id, [...vector]); - } - - async remove(id: string): Promise { - this.store.delete(id); - } - - async search( - queryVector: number[], - topK: number = 5 - ): Promise { - const normalized = [...queryVector]; - this.normalizeInPlace(normalized); - - const scores: Array<{ id: string; similarity: number }> = []; - - for (const [id, vector] of this.store.entries()) { - const similarity = this.cosineSimilarity(normalized, vector); - scores.push({ id, similarity }); - } - - scores.sort((a, b) => b.similarity - a.similarity); - return scores.slice(0, topK).map((s, i) => ({ rank: i + 1, id: s.id, similarity: s.similarity })); - } - - async count(): Promise { - return this.store.size; - } - - async clear(): Promise { - this.store.clear(); - } - - async loadFromDatabase(): Promise { - const prisma = (await import("@shieldai/db")).default; - const enrollments = await prisma.voiceEnrollment.findMany({ - select: { id: true, embeddingVector: true }, - }); - - for (const enrollment of enrollments) { - this.store.set(enrollment.id, enrollment.embeddingVector); - } - } - - private cosineSimilarity(a: number[], b: number[]): number { - let dotProduct = 0; - let normA = 0; - let normB = 0; - - for (let i = 0; i < a.length; i++) { - dotProduct += a[i] * b[i]; - normA += a[i] * a[i]; - normB += b[i] * b[i]; - } - - const denominator = Math.sqrt(normA) * Math.sqrt(normB); - return denominator > 0 ? dotProduct / denominator : 0; - } - - private normalizeInPlace(vector: number[]): void { - const norm = Math.sqrt(vector.reduce((s, v) => s + v * v, 0)); - if (norm > 0) { - for (let i = 0; i < vector.length; i++) { - vector[i] /= norm; - } - } - } -} - -export interface SearchResult { - rank: number; - id: string; - similarity: number; -} diff --git a/services/voiceprint/src/logger.ts b/services/voiceprint/src/logger.ts deleted file mode 100644 index 34c4118..0000000 --- a/services/voiceprint/src/logger.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { FastifyLoggerOptions } from 'fastify'; - -export interface Logger { - info(message: string, context?: Record): void; - warn(message: string, context?: Record): void; - error(message: string, context?: Record): void; - debug(message: string, context?: Record): void; -} - -export class ConsoleLogger implements Logger { - info(message: string, context?: Record): void { - const timestamp = new Date().toISOString(); - const logContext = context ? ` ${JSON.stringify(context)}` : ''; - console.log(`[${timestamp}] [INFO] ${message}${logContext}`); - } - - warn(message: string, context?: Record): void { - const timestamp = new Date().toISOString(); - const logContext = context ? ` ${JSON.stringify(context)}` : ''; - console.warn(`[${timestamp}] [WARN] ${message}${logContext}`); - } - - error(message: string, context?: Record): void { - const timestamp = new Date().toISOString(); - const logContext = context ? ` ${JSON.stringify(context)}` : ''; - console.error(`[${timestamp}] [ERROR] ${message}${logContext}`); - } - - debug(message: string, context?: Record): void { - const timestamp = new Date().toISOString(); - const logContext = context ? ` ${JSON.stringify(context)}` : ''; - console.debug(`[${timestamp}] [DEBUG] ${message}${logContext}`); - } -} - -export const logger = new ConsoleLogger(); diff --git a/services/voiceprint/src/preprocessor/AudioPreprocessor.ts b/services/voiceprint/src/preprocessor/AudioPreprocessor.ts deleted file mode 100644 index 1b1e749..0000000 --- a/services/voiceprint/src/preprocessor/AudioPreprocessor.ts +++ /dev/null @@ -1,327 +0,0 @@ -import { spawn } from "child_process"; -import { randomBytes } from "crypto"; -import { tmpdir } from "os"; -import { join } from "path"; - -export class AudioPreprocessor { - private readonly targetSampleRate = 16000; - private readonly channels = 1; - - async preprocess(audioBuffer: Buffer, inputSampleRate?: number): Promise { - const tempInput = this.writeTempFile(audioBuffer, ".wav"); - const tempOutput = this.writeTempFile(Buffer.alloc(0), ".wav"); - - const outputSampleRate = inputSampleRate || this.detectSampleRate(audioBuffer); - - try { - await this.runPythonPreprocess(tempInput, tempOutput, outputSampleRate); - - const processed = await this.readFile(tempOutput); - return { - audio: processed, - sampleRate: this.targetSampleRate, - channels: this.channels, - durationSec: processed.length / (this.targetSampleRate * this.channels * 2), - }; - } catch (err) { - const fallback = await this.jsFallback(audioBuffer, outputSampleRate); - return fallback; - } - } - - async preprocessBatch( - inputs: Array<{ buffer: Buffer; sampleRate?: number }> - ): Promise { - const promises = inputs.map(async (input) => { - return this.preprocess(input.buffer, input.sampleRate); - }); - return Promise.all(promises); - } - - applyVAD(audioBuffer: Buffer, sampleRate: number): Buffer { - const int16Array = this.bufferToInt16(audioBuffer); - const silenceThreshold = 500; - const windowSize = Math.floor(sampleRate * 0.02); - - const segments: number[][] = []; - let currentSegment: number[] = []; - - for (let i = 0; i < int16Array.length; i += windowSize) { - const window = int16Array.slice(i, i + windowSize); - const rms = Math.sqrt( - window.reduce((sum, val) => sum + val * val, 0) / window.length - ); - - if (rms > silenceThreshold) { - currentSegment.push(...window); - } else if (currentSegment.length > 0) { - segments.push(currentSegment); - currentSegment = []; - } - } - - if (currentSegment.length > 0) { - segments.push(currentSegment); - } - - const merged = segments.flat(); - return this.int16ToBuffer(merged); - } - - normalizeAudio(audioBuffer: Buffer): Buffer { - const int16Array = this.bufferToInt16(audioBuffer); - const maxAmplitude = Math.max(...int16Array.map((v) => Math.abs(v))); - const targetMax = 32767 * 0.95; - - if (maxAmplitude === 0) return audioBuffer; - - const scale = targetMax / maxAmplitude; - const normalized = int16Array.map((v) => Math.round(v * scale)); - return this.int16ToBuffer(normalized); - } - - async extractFeatures(audioBuffer: Buffer): Promise { - const int16Array = this.bufferToInt16(audioBuffer); - const sampleRate = this.targetSampleRate; - const windowSize = Math.floor(sampleRate * 0.025); - const hopLength = Math.floor(sampleRate * 0.01); - - const mfccs: number[][] = []; - const numCoeffs = 13; - - for (let i = 0; i < int16Array.length - windowSize; i += hopLength) { - const frame = int16Array.slice(i, i + windowSize); - const coeffs = this.computeMFCC(frame, numCoeffs); - mfccs.push(coeffs); - } - - const zeroCrossingRate = this.computeZCR(int16Array); - const spectralCentroid = this.computeSpectralCentroid(int16Array); - const spectralRollOff = this.computeSpectralRollOff(int16Array); - - return { - mfccs, - zeroCrossingRate, - spectralCentroid, - spectralRollOff, - durationSec: int16Array.length / sampleRate, - }; - } - - private async runPythonPreprocess( - inputPath: string, - outputPath: string, - inputSampleRate: number - ): Promise { - return new Promise((resolve, reject) => { - const mlServiceUrl = process.env.VOICEPRINT_ML_URL || "http://localhost:8001"; - const proc = spawn("python3", [ - "-c", - ` -import urllib.request, json, sys -req = urllib.request.Request( - "${mlServiceUrl}/preprocess", - data=json.dumps({"input_path": "${inputPath}", "output_path": "${outputPath}", "input_sr": ${inputSampleRate}}).encode(), - headers={"Content-Type": "application/json"} -) -try: - with urllib.request.urlopen(req, timeout=30) as resp: - sys.exit(0) if resp.status == 200 else sys.exit(1) -except: - sys.exit(1) -`, - ]); - - proc.on("close", (code) => { - code === 0 ? resolve() : reject(new Error(`Python preprocess exited with code ${code}`)); - }); - }); - } - - private async jsFallback( - audioBuffer: Buffer, - inputSampleRate: number - ): Promise { - let processed = audioBuffer; - - if (inputSampleRate !== this.targetSampleRate) { - processed = this.resample(audioBuffer, inputSampleRate, this.targetSampleRate); - } - - processed = this.applyVAD(processed, this.targetSampleRate); - processed = this.normalizeAudio(processed); - - return { - audio: processed, - sampleRate: this.targetSampleRate, - channels: this.channels, - durationSec: processed.length / (this.targetSampleRate * this.channels * 2), - }; - } - - private resample(buffer: Buffer, fromRate: number, toRate: number): Buffer { - const int16 = this.bufferToInt16(buffer); - const ratio = fromRate / toRate; - const newLength = Math.round(int16.length / ratio); - const resampled: number[] = []; - - for (let i = 0; i < newLength; i++) { - const srcIdx = Math.floor(i * ratio); - const nextIdx = Math.min(srcIdx + 1, int16.length - 1); - const frac = (i * ratio) - srcIdx; - resampled.push(Math.round(int16[srcIdx] * (1 - frac) + int16[nextIdx] * frac)); - } - - return this.int16ToBuffer(resampled); - } - - private detectSampleRate(buffer: Buffer): number { - if (buffer.length < 44) return 16000; - - const fmtOffset = buffer.toString("ascii", 12, 16).trim() === "fmt " ? 16 : 40; - if (fmtOffset + 4 <= buffer.length) { - const sr = buffer.readUInt32LE(fmtOffset + 4); - if (sr >= 8000 && sr <= 48000) return sr; - } - return 16000; - } - - private bufferToInt16(buffer: Buffer): number[] { - const arr: number[] = new Array(buffer.length / 2); - for (let i = 0; i < arr.length; i++) { - arr[i] = buffer.readInt16LE(i * 2); - } - return arr; - } - - private int16ToBuffer(arr: number[]): Buffer { - const buf = Buffer.alloc(arr.length * 2); - for (let i = 0; i < arr.length; i++) { - buf.writeInt16LE(arr[i], i * 2); - } - return buf; - } - - private computeMFCC(frame: number[], numCoeffs: number): number[] { - const n = frame.length; - const fftSize = Math.pow(2, Math.ceil(Math.log2(n)) + 1); - const spectrum: number[] = new Array(fftSize / 2 + 1); - - for (let k = 0; k < spectrum.length; k++) { - let real = 0, imag = 0; - for (let n = 0; n < frame.length; n++) { - const angle = (2 * Math.PI * k * n) / fftSize; - real += frame[n] * Math.cos(angle); - imag -= frame[n] * Math.sin(angle); - } - spectrum[k] = Math.sqrt(real * real + imag * imag); - } - - const numFilters = 20; - const filterbank = this.createFilterbank(spectrum.length, numFilters); - const logEnergies: number[] = []; - - for (let m = 0; m < numFilters; m++) { - let energy = 0; - for (let k = 0; k < spectrum.length; k++) { - energy += filterbank[m][k] * spectrum[k] * spectrum[k]; - } - logEnergies.push(Math.log(Math.max(energy, 1e-10))); - } - - const mfccs: number[] = []; - for (let d = 0; d < numCoeffs; d++) { - let coeff = 0; - for (let m = 0; m < numFilters; m++) { - coeff += logEnergies[m] * Math.cos(((2 * m + 1) * (d + 1) * Math.PI) / (2 * numFilters)); - } - mfccs.push(coeff); - } - - return mfccs; - } - - private createFilterbank(numBins: number, numFilters: number): number[][] { - const filterbank: number[][] = Array.from({ length: numFilters }, () => - new Array(numBins).fill(0) - ); - const low = Math.floor(20 * numBins / 8000); - const high = Math.floor(5500 * numBins / 8000); - const spacing = (high - low) / (numFilters + 1); - - for (let m = 1; m <= numFilters; m++) { - const center = Math.floor(low + m * spacing); - const prev = Math.floor(low + (m - 1) * spacing); - const next = Math.floor(low + (m + 1) * spacing); - - for (let k = prev; k < center; k++) { - if (k > 0) filterbank[m - 1][k] = (k - prev) / (center - prev); - } - for (let k = center; k < next; k++) { - if (next - center > 0) filterbank[m - 1][k] = (next - k) / (next - center); - } - } - - return filterbank; - } - - private computeZCR(int16: number[]): number { - let crossings = 0; - for (let i = 1; i < int16.length; i++) { - if ((int16[i] > 0 && int16[i - 1] <= 0) || (int16[i] <= 0 && int16[i - 1] > 0)) { - crossings++; - } - } - return crossings / int16.length; - } - - private computeSpectralCentroid(int16: number[]): number { - const n = int16.length; - let num = 0, den = 0; - for (let i = 0; i < n; i++) { - num += i * int16[i] * int16[i]; - den += int16[i] * int16[i]; - } - return den > 0 ? num / den : 0; - } - - private computeSpectralRollOff(int16: number[]): number { - const n = int16.length; - let totalEnergy = 0; - for (let i = 0; i < n; i++) { - totalEnergy += int16[i] * int16[i]; - } - const threshold = totalEnergy * 0.85; - let cumulative = 0; - for (let i = 0; i < n; i++) { - cumulative += int16[i] * int16[i]; - if (cumulative >= threshold) return i; - } - return n - 1; - } - - private writeTempFile(content: Buffer, ext: string): string { - const file = join(tmpdir(), `vp_${randomBytes(8).toString("hex")}${ext}`); - require("fs").writeFileSync(file, content); - return file; - } - - private async readFile(path: string): Promise { - return require("fs").readFileSync(path); - } -} - -export interface PreprocessedAudio { - audio: Buffer; - sampleRate: number; - channels: number; - durationSec: number; -} - -export interface AudioFeatures { - mfccs: number[][]; - zeroCrossingRate: number; - spectralCentroid: number; - spectralRollOff: number; - durationSec: number; -} diff --git a/services/voiceprint/src/voiceprint.config.ts b/services/voiceprint/src/voiceprint.config.ts deleted file mode 100644 index f3bad81..0000000 --- a/services/voiceprint/src/voiceprint.config.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { z } from 'zod'; -import { existsSync } from 'fs'; -import { checkFlag } from './voiceprint.feature-flags'; - -// P3-4 fix: Use strict() to catch typos in env var names -// P3-1 fix: Use safeParse() to avoid module-level crash on missing env vars -const envSchema = z.object({ - ECAPA_TDNN_MODEL_PATH: z.string().default('./models/ecapa-tdnn'), - ML_SERVICE_URL: z.string().default('http://localhost:8001'), - FAISS_INDEX_PATH: z.string().default('./data/voiceprint_faiss.index'), - AUDIO_STORAGE_BUCKET: z.string().default('voiceprint-audio'), - AUDIO_STORAGE_ENDPOINT: z.string().default('http://localhost:9000'), - SYNTHETIC_THRESHOLD: z.string().transform(Number).default('0.75'), - ENROLLMENT_MIN_DURATION_SEC: z.string().transform(Number).default('3'), - ENROLLMENT_MAX_DURATION_SEC: z.string().transform(Number).default('60'), - EMBEDDING_DIMENSIONS: z.string().transform(Number).default('192'), - BATCH_MAX_FILES: z.string().transform(Number).default('20'), - ANALYSIS_TIMEOUT_MS: z.string().transform(Number).default('30000'), -}).strict(); - -const envInput = { - ECAPA_TDNN_MODEL_PATH: process.env.ECAPA_TDNN_MODEL_PATH, - ML_SERVICE_URL: process.env.ML_SERVICE_URL, - FAISS_INDEX_PATH: process.env.FAISS_INDEX_PATH, - AUDIO_STORAGE_BUCKET: process.env.AUDIO_STORAGE_BUCKET, - AUDIO_STORAGE_ENDPOINT: process.env.AUDIO_STORAGE_ENDPOINT, - SYNTHETIC_THRESHOLD: process.env.SYNTHETIC_THRESHOLD, - ENROLLMENT_MIN_DURATION_SEC: process.env.ENROLLMENT_MIN_DURATION_SEC, - ENROLLMENT_MAX_DURATION_SEC: process.env.ENROLLMENT_MAX_DURATION_SEC, - EMBEDDING_DIMENSIONS: process.env.EMBEDDING_DIMENSIONS, - BATCH_MAX_FILES: process.env.BATCH_MAX_FILES, - ANALYSIS_TIMEOUT_MS: process.env.ANALYSIS_TIMEOUT_MS, -}; - -const parsed = envSchema.safeParse(envInput); -export const voicePrintEnv = parsed.success - ? parsed.data - : envSchema.parse({}); // fallback to all defaults - -// P3-3 fix: Validate model path exists at startup (warn, not crash) -if (voicePrintEnv.ECAPA_TDNN_MODEL_PATH && !existsSync(voicePrintEnv.ECAPA_TDNN_MODEL_PATH)) { - console.warn( - `[VoicePrint] Model path not found: ${voicePrintEnv.ECAPA_TDNN_MODEL_PATH} (using mock model)` - ); -} - -if (!parsed.success) { - console.warn('[VoicePrint] Env validation warnings:', parsed.error.issues.map((i: z.ZodIssue) => `${i.path.join('.')}: ${i.message}`).join(', ')); -} - -// Audio source types -export enum VoicePrintSource { - UPLOAD = 'upload', - S3 = 's3', - URL = 'url', - REALTIME = 'realtime', -} - -// Analysis job status -export enum AnalysisJobStatus { - PENDING = 'pending', - PROCESSING = 'processing', - COMPLETED = 'completed', - FAILED = 'failed', - CANCELLED = 'cancelled', -} - -// Detection result types -export enum DetectionType { - SYNTHETIC_VOICE = 'synthetic_voice', - VOICE_CLONE = 'voice_clone', - DEEPFAKE = 'deepfake', - NATURAL = 'natural', -} - -// Confidence levels -export enum ConfidenceLevel { - LOW = 'low', - MEDIUM = 'medium', - HIGH = 'high', - VERY_HIGH = 'very_high', -} - -// Audio preprocessing configuration -export const audioPreprocessingConfig = { - sampleRate: 16000, - channels: 1, - bitDepth: 16, - vadThreshold: 0.5, - noiseReduction: true, - maxSilenceDurationMs: 500, -}; - -// Feature flags - use centralized system -export const voicePrintFeatureFlags = { - enableMLService: checkFlag('voiceprint.enable.ml.service', false), - enableFAISSIndex: checkFlag('voiceprint.enable.faiss.index', true), - enableBatchAnalysis: checkFlag('voiceprint.enable.batch.analysis', true), - enableRealtimeAnalysis: checkFlag('voiceprint.enable.realtime.analysis', false), - enableMockModel: checkFlag('voiceprint.enable.mock.model', true), -}; - -// Rate limits for voice analysis -export const voicePrintRateLimits = { - basic: { - analysesPerMinute: 5, - enrollmentsPerDay: 10, - maxAudioFileSizeMB: 50, - }, - plus: { - analysesPerMinute: 30, - enrollmentsPerDay: 50, - maxAudioFileSizeMB: 200, - }, - premium: { - analysesPerMinute: 100, - enrollmentsPerDay: 500, - maxAudioFileSizeMB: 500, - }, -}; diff --git a/services/voiceprint/src/voiceprint.feature-flags.ts b/services/voiceprint/src/voiceprint.feature-flags.ts deleted file mode 100644 index c4c664d..0000000 --- a/services/voiceprint/src/voiceprint.feature-flags.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * VoicePrint Feature Flags - * Re-exports the checkFlag function from the centralized feature flag system - */ - -// Re-export the checkFlag function from the spamshield feature flags module -export { checkFlag } from '../spamshield/feature-flags'; diff --git a/services/voiceprint/src/voiceprint.service.ts b/services/voiceprint/src/voiceprint.service.ts deleted file mode 100644 index 0170f59..0000000 --- a/services/voiceprint/src/voiceprint.service.ts +++ /dev/null @@ -1,649 +0,0 @@ -/** - * VoicePrint Service - Legacy Module - * - * @deprecated This file contains legacy service implementations. - * Migrate to the new modular structure: - * - Use `import { AnalysisService } from './analysis/AnalysisService'` for analysis - * - Use `import { BatchAnalysisService } from './analysis/BatchAnalysisService'` for batch operations - * - Use `import { EmbeddingService } from './embedding/EmbeddingService'` for embeddings - * - Use `import { VoiceEnrollmentService } from './enrollment/VoiceEnrollmentService'` for enrollment - */ - -import { prisma, VoiceEnrollment, VoiceAnalysis } from '@shieldai/db'; -import { - voicePrintEnv, - AnalysisJobStatus, - DetectionType, - ConfidenceLevel, - audioPreprocessingConfig, - voicePrintFeatureFlags, -} from './voiceprint.config'; -import { checkFlag } from './voiceprint.feature-flags'; -import { createHash } from 'crypto'; -import { logger } from './logger'; - -// Audio preprocessing service -export class AudioPreprocessor { - /** - * Normalize audio to 16kHz mono with VAD and noise reduction. - * Returns preprocessing metadata and the processed audio buffer. - */ - async preprocess( - audioBuffer: Buffer, - options?: { - sourceSampleRate?: number; - channels?: number; - } - ): Promise<{ - buffer: Buffer; - metadata: { - sampleRate: number; - channels: number; - duration: number; - format: string; - }; - }> { - const duration = this.estimateDuration(audioBuffer, options?.sourceSampleRate ?? 44100); - - if (duration < voicePrintEnv.ENROLLMENT_MIN_DURATION_SEC) { - throw new Error( - `Audio too short: ${duration.toFixed(1)}s < ${voicePrintEnv.ENROLLMENT_MIN_DURATION_SEC}s minimum` - ); - } - - if (duration > voicePrintEnv.ENROLLMENT_MAX_DURATION_SEC) { - throw new Error( - `Audio too long: ${duration.toFixed(1)}s > ${voicePrintEnv.ENROLLMENT_MAX_DURATION_SEC}s maximum` - ); - } - - // TODO: Integrate with Python librosa/torchaudio for actual preprocessing - // For MVP, return original buffer with target metadata - return { - buffer: audioBuffer, - metadata: { - sampleRate: audioPreprocessingConfig.sampleRate, - channels: audioPreprocessingConfig.channels, - duration, - format: 'wav', - }, - }; - } - - /** - * Apply Voice Activity Detection to remove silence segments. - */ - async applyVAD(buffer: Buffer): Promise { - // TODO: Integrate with Python webrtcvad or silero-vad - // For MVP, return original buffer - return buffer; - } - - /** - * Estimate audio duration from buffer size and sample rate. - */ - private estimateDuration( - buffer: Buffer, - sampleRate: number - ): number { - const bytesPerSample = 2; - const channels = 1; - const samples = buffer.length / (bytesPerSample * channels); - return samples / sampleRate; - } -} - -// Voice enrollment service -export class VoiceEnrollmentService { - /** - * Enroll a new voice profile from audio data. - */ - async enroll( - userId: string, - name: string, - audioBuffer: Buffer - ): Promise { - const preprocessor = new AudioPreprocessor(); - const processed = await preprocessor.preprocess(audioBuffer); - - const embeddingService = new EmbeddingService(); - const embedding = await embeddingService.extract(processed.buffer); - const voiceHash = this.computeEmbeddingHash(embedding); - - const enrollment = await prisma.voiceEnrollment.create({ - data: { - userId, - name, - voiceHash, - audioMetadata: { - ...processed.metadata, - embeddingDimensions: embedding.length, - enrollmentTimestamp: new Date().toISOString(), - }, - }, - }); - - // Index in FAISS for similarity search - const faissIndex = new FAISSIndex(); - await faissIndex.add(enrollment.id, embedding); - - return enrollment; - } - - /** - * List all enrollments for a user. - */ - async listEnrollments( - userId: string, - options?: { - isActive?: boolean; - limit?: number; - offset?: number; - } - ): Promise { - return prisma.voiceEnrollment.findMany({ - where: { - userId, - ...(options?.isActive !== undefined && { isActive: options.isActive }), - }, - orderBy: { createdAt: 'desc' }, - take: options?.limit ?? 50, - skip: options?.offset ?? 0, - }); - } - - /** - * Get a single enrollment by ID. - */ - async getEnrollment( - enrollmentId: string, - userId: string - ): Promise { - return prisma.voiceEnrollment.findFirst({ - where: { - id: enrollmentId, - userId, - }, - }); - } - - /** - * Remove (deactivate) an enrollment. - */ - async removeEnrollment( - enrollmentId: string, - userId: string - ): Promise { - const enrollment = await this.getEnrollment(enrollmentId, userId); - if (!enrollment) { - throw new Error('Enrollment not found'); - } - - const faissIndex = new FAISSIndex(); - await faissIndex.remove(enrollmentId); - - return prisma.voiceEnrollment.update({ - where: { id: enrollmentId }, - data: { isActive: false }, - }); - } - - /** - * Search for similar enrollments using FAISS. - */ - async findSimilar( - embedding: number[], - topK: number = 5 - ): Promise> { - const faissIndex = new FAISSIndex(); - const results = await faissIndex.search(embedding, topK); - - const enrollmentIds = results.map((r) => r.id); - const enrollments = await prisma.voiceEnrollment.findMany({ - where: { id: { in: enrollmentIds } }, - }); - - return results.map((r, i) => ({ - enrollment: enrollments[i], - similarity: r.similarity, - })); - } - - private computeEmbeddingHash(embedding: number[]): string { - const hash = createHash('sha256') - .update(JSON.stringify(embedding)) - .digest('hex'); - return `vp_${hash.substring(0, 16)}_${embedding.length}`; - } -} - -// Audio analysis service -export class AnalysisService { - /** - * Analyze a single audio file for synthetic voice detection. - */ - async analyze( - userId: string, - audioBuffer: Buffer, - options?: { - enrollmentId?: string; - audioUrl?: string; - } - ): Promise { - const preprocessor = new AudioPreprocessor(); - const processed = await preprocessor.preprocess(audioBuffer); - - const audioHash = this.computeAudioHash(audioBuffer); - - const embeddingService = new EmbeddingService(); - const analysisResult = await embeddingService.analyze(processed.buffer); - - const isSynthetic = analysisResult.confidence >= voicePrintEnv.SYNTHETIC_THRESHOLD; - - const voiceAnalysis = await prisma.voiceAnalysis.create({ - data: { - userId, - enrollmentId: options?.enrollmentId, - audioHash, - isSynthetic, - confidence: analysisResult.confidence, - analysisResult: { - ...analysisResult, - processedMetadata: processed.metadata, - analysisTimestamp: new Date().toISOString(), - modelVersion: 'ecapa-tdnn-v1-mock', - }, - audioUrl: options?.audioUrl ?? '', - }, - }); - - return voiceAnalysis; - } - - /** - * Get analysis result by ID. - */ - async getResult( - analysisId: string, - userId: string - ): Promise { - return prisma.voiceAnalysis.findFirst({ - where: { - id: analysisId, - userId, - }, - }); - } - - /** - * Get analysis history for a user. - */ - async getHistory( - userId: string, - options?: { - limit?: number; - offset?: number; - isSynthetic?: boolean; - } - ): Promise { - return prisma.voiceAnalysis.findMany({ - where: { - userId, - ...(options?.isSynthetic !== undefined && { isSynthetic: options.isSynthetic }), - }, - orderBy: { createdAt: 'desc' }, - take: options?.limit ?? 50, - skip: options?.offset ?? 0, - }); - } - - private computeAudioHash(buffer: Buffer): string { - const hash = createHash('sha256') - .update(buffer) - .digest('hex'); - return `audio_${hash.substring(0, 16)}`; - } -} - -// Batch analysis service -export class BatchAnalysisService { - /** - * Analyze multiple audio files in a batch. - */ - async analyzeBatch( - userId: string, - files: Array<{ - name: string; - buffer: Buffer; - audioUrl?: string; - }>, - options?: { - enrollmentId?: string; - } - ): Promise<{ - jobId: string; - results: VoiceAnalysis[]; - summary: { - total: number; - synthetic: number; - natural: number; - failed: number; - }; - }> { - if (files.length > voicePrintEnv.BATCH_MAX_FILES) { - throw new Error( - `Batch too large: ${files.length} > ${voicePrintEnv.BATCH_MAX_FILES} max` - ); - } - - const jobId = `batch_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; - logger.info('Starting batch analysis', { jobId, userId, fileCount: files.length }); - - const analysisService = new AnalysisService(); - const results: VoiceAnalysis[] = []; - let synthetic = 0; - let natural = 0; - let failed = 0; - - // Process with concurrency control - const concurrencyLimit = 5; - for (let i = 0; i < files.length; i += concurrencyLimit) { - const chunk = files.slice(i, i + concurrencyLimit); - const promises = chunk.map(async (file) => { - try { - const result = await analysisService.analyze(userId, file.buffer, { - enrollmentId: options?.enrollmentId, - audioUrl: file.audioUrl, - }); - return { success: true as const, result, name: file.name }; - } catch (error) { - logger.error('Batch analysis failed for file', { fileName: file.name, jobId, error }); - return { success: false as const, error: error instanceof Error ? error.message : 'Unknown error', name: file.name }; - } - }); - - const outcomes = await Promise.allSettled(promises); - for (const outcome of outcomes) { - if (outcome.status === 'fulfilled') { - if (outcome.value.success) { - results.push(outcome.value.result); - if (outcome.value.result.isSynthetic) { - synthetic++; - } else { - natural++; - } - } else { - failed++; - } - } - } - } - - // Persist batch jobId to database - await prisma.$transaction([ - prisma.$executeRawUnsafe('INSERT INTO batch_jobs (id, user_id, total_files, status, created_at) VALUES ($1, $2, $3, $4, NOW()) ON CONFLICT (id) DO NOTHING', jobId, userId, files.length, 'completed'), - ...results.map(result => - prisma.$executeRawUnsafe('UPDATE voice_analysis SET batch_job_id = $1 WHERE id = $2', jobId, result.id) - ) - ]).catch(err => { - logger.warn('Failed to persist batch jobId', { jobId, error: err instanceof Error ? err.message : String(err) }); - }); - - logger.info('Batch analysis completed', { - jobId, - total: files.length, - synthetic, - natural, - failed - }); - - return { - jobId, - results, - summary: { - total: files.length, - synthetic, - natural, - failed, - }, - }; - } -} - -// Deprecated: Use embedding/EmbeddingService.ts instead -// This class is kept for backward compatibility but delegates to the canonical service -/** - * @deprecated Use `import { EmbeddingService } from './embedding/EmbeddingService'` instead - */ -export class EmbeddingService { - private initialized = false; - - /** - * Initialize the ECAPA-TDNN model. - * @deprecated Use the canonical EmbeddingService from embedding/EmbeddingService.ts - */ - async initialize(): Promise { - if (this.initialized) return; - this.initialized = true; - logger.warn('Deprecated EmbeddingService initialized - migrate to embedding/EmbeddingService.ts'); - } - - /** - * Extract voice embedding from audio. - * @deprecated Use the canonical EmbeddingService from embedding/EmbeddingService.ts - */ - async extract(audioBuffer: Buffer): Promise { - await this.initialize(); - // Delegate to canonical implementation - const canonicalService = new CanonicalEmbeddingService(); - const result = await canonicalService.extract(audioBuffer); - return result.vector; - } - - /** - * Run full analysis: embedding + synthetic detection. - * @deprecated Use AnalysisService from analysis/AnalysisService.ts instead - */ - async analyze(audioBuffer: Buffer): Promise<{ - confidence: number; - detectionType: DetectionType; - features: Record; - embedding: number[]; - }> { - const embeddingService = new CanonicalEmbeddingService(); - const result = await embeddingService.analyze(audioBuffer); - return { - confidence: result.confidence, - detectionType: result.detectionType, - features: result.features, - embedding: result.vector, - }; - } -} - -// Canonical embedding service - single source of truth for embedding logic -class CanonicalEmbeddingService { - private initialized = false; - - async initialize(): Promise { - if (this.initialized) return; - this.initialized = true; - logger.info('Canonical EmbeddingService initialized', { modelVersion: 'ecapa-tdnn-v1-mock' }); - } - - async extract(audioBuffer: Buffer): Promise<{ vector: number[]; dimension: number }> { - await this.initialize(); - // Use the same mock generation as embedding/EmbeddingService.ts for consistency - const dims = voicePrintEnv.EMBEDDING_DIMENSIONS; - let hash = 0; - const sampleSize = Math.min(audioBuffer.length, 1024); - for (let i = 0; i < sampleSize; i += 4) { - hash = ((hash << 5) - hash + audioBuffer.readInt32LE(i)) | 0; - } - const seed = Math.abs(hash); - const rng = this.createRNG(seed); - - const vector: number[] = []; - for (let i = 0; i < dims; i++) { - const u1 = rng(); - const u2 = rng(); - const gauss = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); - vector.push(parseFloat(gauss.toFixed(6))); - } - - const norm = Math.sqrt(vector.reduce((s, v) => s + v * v, 0)); - const normalized = vector.map((v) => parseFloat((v / norm).toFixed(6))); - return { vector: normalized, dimension: dims }; - } - - async analyze(audioBuffer: Buffer): Promise<{ - confidence: number; - detectionType: DetectionType; - features: Record; - vector: number[]; - }> { - const { vector } = await this.extract(audioBuffer); - - // Heuristic for synthetic detection - const meanAmplitude = audioBuffer.reduce((s, v) => s + v, 0) / audioBuffer.length / 255; - const embeddingStdDev = Math.sqrt( - vector.reduce((s, v) => s + (v - vector.reduce((a, b) => a + b) / vector.length) ** 2, 0) / vector.length - ) || 0; - - const amplitudeScore = Math.abs(meanAmplitude - 0.5) * 2; - const embeddingScore = 1.0 - Math.min(1.0, embeddingStdDev * 2); - const confidence = Math.min(1.0, amplitudeScore * 0.3 + embeddingScore * 0.4 + Math.random() * 0.3); - - const detectionType = confidence >= voicePrintEnv.SYNTHETIC_THRESHOLD - ? DetectionType.SYNTHETIC_VOICE - : DetectionType.NATURAL; - - const zeroCrossings = audioBuffer.reduce((count, v, i, arr) => { - return i > 0 && ((v - 128) * (arr[i - 1] - 128) < 0) ? count + 1 : count; - }, 0); - - const features = { - mean_amplitude: meanAmplitude, - zero_crossing_rate: zeroCrossings / audioBuffer.length, - embedding_energy: vector.reduce((s, v) => s + v * v, 0), - embedding_entropy: this.calculateEntropy(vector), - }; - - return { confidence, detectionType, features, vector }; - } - - private createRNG(seed: number): () => number { - return () => { - seed = (seed * 1664525 + 1013904223) & 0xffffffff; - return (seed >>> 0) / 0xffffffff; - }; - } - - private calculateEntropy(values: number[]): number { - const bins = 20; - const histogram = new Array(bins).fill(0); - const min = Math.min(...values); - const max = Math.max(...values); - const range = max - min || 1; - - for (const v of values) { - const bin = Math.min(bins - 1, Math.floor(((v - min) / range) * bins)); - histogram[bin]++; - } - - let entropy = 0; - const total = values.length; - for (const count of histogram) { - if (count > 0) { - const p = count / total; - entropy -= p * Math.log2(p); - } - } - return entropy; - } -} - -// FAISS index wrapper for voice fingerprint matching -export class FAISSIndex { - private indexPath: string; - private initialized = false; - - constructor(path?: string) { - this.indexPath = path ?? voicePrintEnv.FAISS_INDEX_PATH; - } - - /** - * Initialize or load the FAISS index. - */ - async initialize(): Promise { - if (this.initialized) return; - - // TODO: Load FAISS index from disk - // const faiss = require('faiss-node'); - // this.index = faiss.readIndex(this.indexPath); - - this.initialized = true; - logger.info('FAISS index initialized', { indexPath: this.indexPath }); - } - - /** - * Add an enrollment embedding to the index. - */ - async add(enrollmentId: string, embedding: number[]): Promise { - await this.initialize(); - - // TODO: Add to FAISS index - // this.index.add([embedding]); - // Store mapping: enrollmentId -> index position - logger.info('Added enrollment to FAISS index', { enrollmentId, embeddingDimensions: embedding.length }); - } - - /** - * Remove an enrollment from the index. - */ - async remove(enrollmentId: string): Promise { - await this.initialize(); - - // TODO: Remove from FAISS index - logger.info('Removed enrollment from FAISS index', { enrollmentId }); - } - - /** - * Search for similar voice embeddings. - */ - async search( - embedding: number[], - topK: number = 5 - ): Promise> { - await this.initialize(); - - // TODO: Query FAISS index - // const [distances, indices] = this.index.search([embedding], topK); - // Map indices back to enrollment IDs - - // Mock: return empty results - return []; - } - - /** - * Save the index to disk. - */ - async save(): Promise { - await this.initialize(); - // TODO: Write FAISS index to disk - logger.info('FAISS index saved', { indexPath: this.indexPath }); - } -} - -// Export classes only - use dependency injection for instantiation -// Deprecated singleton exports kept for backward compatibility only -/** @deprecated Use `new AudioPreprocessor()` instead */ -export const audioPreprocessor = new AudioPreprocessor(); -/** @deprecated Use `new VoiceEnrollmentService()` instead */ -export const voiceEnrollmentService = new VoiceEnrollmentService(); -/** @deprecated Use `new AnalysisService()` instead */ -export const analysisService = new AnalysisService(); -/** @deprecated Use `new BatchAnalysisService()` instead */ -export const batchAnalysisService = new BatchAnalysisService(); -/** @deprecated Use `new EmbeddingService()` instead */ -export const embeddingService = new EmbeddingService(); diff --git a/services/voiceprint/tsconfig.json b/services/voiceprint/tsconfig.json deleted file mode 100644 index 90d76d7..0000000 --- a/services/voiceprint/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/services/voiceprint/vitest.config.ts b/services/voiceprint/vitest.config.ts deleted file mode 100644 index fe01a0a..0000000 --- a/services/voiceprint/vitest.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - include: ['src/**/*.test.ts', 'test/**/*.test.ts'], - coverage: { - provider: 'v8', - reporter: ['text', 'json', 'html', 'lcov'], - reportsDirectory: './coverage', - include: ['src/**/*.ts'], - exclude: [ - 'src/**/*.d.ts', - '**/node_modules/**', - '**/test/**', - ], - thresholds: { - statements: 80, - branches: 80, - functions: 80, - lines: 80, - }, - }, - }, -}); diff --git a/test-classifier.ts b/test-classifier.ts deleted file mode 100644 index bec3c03..0000000 --- a/test-classifier.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { BertSmsClassifier } from './services/spamshield/src/classifier/sms-classifier'; - -// Mock SpamShieldService for testing -class MockSpamShield { - async checkReputation(phoneNumber: string) { - return { - score: 0, - isSpam: false, - source: 'fallback', - }; - } -} - -async function testClassifier(): Promise { - console.log('=== SMS Classifier Feature Flag Test ===\n'); - - // Test 1: Classifier with enabled feature flag - console.log('Test 1: Classifier with enabled feature flag'); - const mockShield = new MockSpamShield(); - const classifier = new BertSmsClassifier(mockShield as any); - - const result = await classifier.classify( - 'Congratulations! You have won $1,000,000! Click here: http://bit.ly/12345 to claim your prize now! Call 555-1234 immediately!' - ); - - console.log('Input:', result.body); - console.log('Classification:', result.isSpam ? 'SPAM' : 'NOT SPAM'); - console.log('Score:', result.score); - console.log('Features:', result.features); - console.log(''); - - // Test 2: Classifier with benign message - console.log('Test 2: Classifier with benign message'); - const benignResult = await classifier.classify( - 'Hello! Just checking in to see how you are doing. Hope you have a great day!' - ); - - console.log('Input:', benignResult.body); - console.log('Classification:', benignResult.isSpam ? 'SPAM' : 'NOT SPAM'); - console.log('Score:', benignResult.score); - console.log(''); - - // Test 3: Classifier metrics - console.log('Test 3: Classifier metrics'); - const metrics = classifier.getMetrics(); - console.log('Total classified:', metrics.totalClassified); - console.log('Spam detected:', metrics.spamDetected); - console.log('Accuracy:', metrics.totalClassified > 0 - ? ((metrics.spamDetected / metrics.totalClassified) * 100).toFixed(1) + '%' - : '0%'); - console.log(''); - - console.log('=== All tests completed successfully ==='); -} - -export { testClassifier }; - -if (import.meta.env?.MODE !== 'test') { - testClassifier().catch(console.error); -} diff --git a/test-maxpayload.ts b/test-maxpayload.ts deleted file mode 100644 index a776de4..0000000 --- a/test-maxpayload.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { WebSocketServer, WebSocket } from 'ws'; -import { createServer } from 'http'; -import { randomBytes } from 'crypto'; - -/** - * Test WebSocket maxPayload limit enforcement - */ - -async function testMaxPayloadLimit() { - console.log('Testing WebSocket maxPayload limit (64KB)...'); - - // Create HTTP server - const httpServer = createServer(); - - // Create WebSocket server with maxPayload = 64KB - const wss = new WebSocketServer({ - port: 0, // Use random available port - maxPayload: 65536, // 64KB - }); - - let testPassed = false; - - wss.on('connection', (ws) => { - console.log('✓ Client connected'); - - // Send a message larger than 64KB - const oversizedMessage = 'x'.repeat(70000); // 70KB - - console.log(`Attempting to send ${oversizedMessage.length} bytes...`); - ws.send(oversizedMessage, (err) => { - if (err) { - console.log('✓ Error received as expected:', err.message); - console.log('✓ maxPayload limit is correctly enforced!'); - testPassed = true; - } else { - console.log('✗ No error received - maxPayload NOT enforced!'); - } - }); - - ws.on('close', () => { - httpServer.close(() => { - wss.close(() => { - if (testPassed) { - console.log('\n✅ TEST PASSED: WebSocket maxPayload limit (64KB) is working correctly'); - process.exit(0); - } else { - console.log('\n❌ TEST FAILED: WebSocket maxPayload limit not enforced'); - process.exit(1); - } - }); - }); - }); - }); - - httpServer.listen(0, () => { - console.log(`WebSocket server listening on port ${httpServer.address().port}`); - }); -} - -testMaxPayloadLimit().catch(console.error); diff --git a/test-ws-maxpayload.js b/test-ws-maxpayload.js deleted file mode 100644 index a6f65f6..0000000 --- a/test-ws-maxpayload.js +++ /dev/null @@ -1,44 +0,0 @@ -const { WebSocketServer } = require('ws'); -const { createServer } = require('http'); - -// Test WebSocket maxPayload parameter -const httpServer = createServer(); -const wss = new WebSocketServer({ - port: 0, - maxPayload: 65536, // 64KB -}); - -let testPassed = false; - -wss.on('connection', (ws) => { - console.log('Client connected'); - - // Send message larger than 64KB - const oversized = 'x'.repeat(70000); - - console.log('Sending 70KB message...'); - ws.send(oversized, (err) => { - if (err) { - console.log('✓ Error received (expected):', err.message); - testPassed = true; - } else { - console.log('✗ No error - maxPayload NOT enforced!'); - } - - ws.close(); - httpServer.close(); - wss.close(); - - if (testPassed) { - console.log('✅ TEST PASSED: maxPayload (64KB) is enforced'); - process.exit(0); - } else { - console.log('❌ TEST FAILED'); - process.exit(1); - } - }); -}); - -httpServer.listen(0, () => { - console.log('Server listening on port', httpServer.address().port); -}); diff --git a/test-ws-maxpayload2.js b/test-ws-maxpayload2.js deleted file mode 100644 index 1759e66..0000000 --- a/test-ws-maxpayload2.js +++ /dev/null @@ -1,73 +0,0 @@ -const { WebSocketServer, WebSocket } = require('ws'); -const { createServer } = require('http'); - -const httpServer = createServer(); -const wss = new WebSocketServer({ - port: 0, - maxPayload: 65536, // 64KB -}); - -let testPassed = false; - -wss.on('connection', (ws) => { - console.log('Client connected'); - - // Send oversized message - const oversized = 'x'.repeat(70000); - console.log('Sending 70KB message...'); - - ws.send(oversized, (err) => { - if (err) { - console.log('✓ Error received (expected):', err.message); - testPassed = true; - } else { - console.log('✗ No error - maxPayload NOT enforced!'); - } - - ws.close(); - httpServer.close(); - wss.close(); - - if (testPassed) { - console.log('✅ TEST PASSED: maxPayload (64KB) is enforced'); - process.exit(0); - } else { - console.log('❌ TEST FAILED'); - process.exit(1); - } - }); -}); - -httpServer.listen(0, () => { - const port = httpServer.address().port; - console.log('Server listening on port', port); - - // Create client immediately - const ws = new WebSocket(`ws://localhost:${port}`); - - ws.on('open', () => { - console.log('Client connected to server'); - }); - - ws.on('error', (err) => { - console.log('Client error:', err.message); - httpServer.close(); - wss.close(); - }); - - ws.on('close', () => { - if (!testPassed) { - console.log('❌ Test timed out - no response received'); - process.exit(1); - } - }); -}); - -// Timeout after 5 seconds -setTimeout(() => { - console.log('❌ Test timed out'); - process.exit(1); -}, 5000); - -// Create client immediately -const ws = new WebSocket(`ws://localhost:${port}`); diff --git a/tsconfig.base.json b/tsconfig.base.json deleted file mode 100644 index 2def3a0..0000000 --- a/tsconfig.base.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", - "types": ["node"] - } -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 06383bd..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/turbo.json b/turbo.json index 843fd00..33a6f02 100644 --- a/turbo.json +++ b/turbo.json @@ -5,7 +5,7 @@ "tasks": { "build": { "dependsOn": ["^build"], - "outputs": ["dist/**", ".next/**", "!.next/cache/**"] + "outputs": ["dist/**", ".output/**"] }, "dev": { "cache": false, @@ -15,10 +15,6 @@ "dependsOn": ["^build"], "outputs": ["coverage/**"] }, - "test:coverage": { - "dependsOn": ["^build"], - "outputs": ["coverage/**"] - }, "lint": { "outputs": [] }, @@ -30,6 +26,9 @@ }, "db:migrate": { "cache": false + }, + "db:seed": { + "cache": false } } } diff --git a/vite.config.ts b/vite.config.ts deleted file mode 100644 index f2c20d0..0000000 --- a/vite.config.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { defineConfig } from 'vite'; -import solid from 'vite-plugin-solid'; -import { resolve } from 'path'; - -export default defineConfig({ - plugins: [solid()], - resolve: { - alias: { - '@lib': resolve(__dirname, './src/lib'), - '@components': resolve(__dirname, './src/components'), - '@types': resolve(__dirname, './src/types'), - }, - }, - build: { - target: 'esnext', - outDir: 'dist', - sourcemap: true, - rollupOptions: { - input: { - main: resolve(__dirname, 'index.html'), - }, - }, - }, - server: { - port: 3000, - proxy: { - '/sync': { - target: 'ws://localhost:8080', - ws: true, - }, - }, - }, -}); diff --git a/vitest.config.ts b/vitest.config.ts deleted file mode 100644 index acf3407..0000000 --- a/vitest.config.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - globals: true, - environment: 'node', - deps: { - interopDefault: true, - }, - env: { - HIYA_API_KEY: 'test-api-key', - HIYA_API_URL: 'https://api.hiya.com/v1', - }, - }, - optimizeDeps: { - include: ['ws'], - }, - ssr: { - noExternal: ['ws'], - }, -}); diff --git a/web/package.json b/web/package.json index 0aa5051..cec25a7 100644 --- a/web/package.json +++ b/web/package.json @@ -1,26 +1,32 @@ { - "name": "example-with-trpc", + "name": "web", "type": "module", "scripts": { "dev": "vite dev", "build": "vite build", "start": "vite start", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "lint": "tsc --noEmit" }, "dependencies": { "@solidjs/meta": "^0.29.4", "@solidjs/router": "^0.15.0", "@solidjs/start": "2.0.0-alpha.2", + "@solidjs/vite-plugin-nitro-2": "^0.1.0", + "@tailwindcss/vite": "^4.0.0", "@trpc/client": "^10.45.2", "@trpc/server": "^10.45.2", "@typeschema/valibot": "^0.13.4", "solid-js": "^1.9.5", + "tailwindcss": "^4.0.0", "valibot": "^0.29.0", - "vite": "^7.0.0", - "@solidjs/vite-plugin-nitro-2": "^0.1.0" + "vite": "^7.0.0" }, "engines": { "node": ">=22" }, - "devDependencies": {} + "devDependencies": { + "vitest": "^4.1.5" + } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml deleted file mode 100644 index 1dc56a0..0000000 --- a/web/pnpm-lock.yaml +++ /dev/null @@ -1,5040 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@solidjs/meta': - specifier: ^0.29.4 - version: 0.29.4(solid-js@1.9.11) - '@solidjs/router': - specifier: ^0.15.0 - version: 0.15.4(solid-js@1.9.11) - '@solidjs/start': - specifier: 2.0.0-alpha.2 - version: 2.0.0-alpha.2(crossws@0.4.4(srvx@0.11.8))(vite@7.3.1(jiti@2.6.1)(terser@5.46.0)) - '@solidjs/vite-plugin-nitro-2': - specifier: ^0.1.0 - version: 0.1.0(vite@7.3.1(jiti@2.6.1)(terser@5.46.0)) - '@trpc/client': - specifier: ^10.45.2 - version: 10.45.4(@trpc/server@10.45.4) - '@trpc/server': - specifier: ^10.45.2 - version: 10.45.4 - '@typeschema/valibot': - specifier: ^0.13.4 - version: 0.13.5(valibot@0.29.0) - solid-js: - specifier: ^1.9.5 - version: 1.9.11 - valibot: - specifier: ^0.29.0 - version: 0.29.0 - vite: - specifier: ^7.0.0 - version: 7.3.1(jiti@2.6.1)(terser@5.46.0) - -packages: - - '@babel/code-frame@7.27.1': - resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} - engines: {node: '>=6.9.0'} - - '@babel/code-frame@7.29.0': - resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.29.0': - resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.29.0': - resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.29.1': - resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-annotate-as-pure@7.27.3': - resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.28.6': - resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-create-class-features-plugin@7.28.6': - resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-globals@7.28.0': - resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-member-expression-to-functions@7.28.5': - resolution: {integrity: sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.18.6': - resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.28.6': - resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.28.6': - resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-optimise-call-expression@7.27.1': - resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-plugin-utils@7.28.6': - resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} - engines: {node: '>=6.9.0'} - - '@babel/helper-replace-supers@7.28.6': - resolution: {integrity: sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': - resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-string-parser@7.27.1': - resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.28.5': - resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.27.1': - resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.28.6': - resolution: {integrity: sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.29.0': - resolution: {integrity: sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/plugin-syntax-jsx@7.28.6': - resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-syntax-typescript@7.28.6': - resolution: {integrity: sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-modules-commonjs@7.28.6': - resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/plugin-transform-typescript@7.28.6': - resolution: {integrity: sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/preset-typescript@7.28.5': - resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 - - '@babel/template@7.28.6': - resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.29.0': - resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.29.0': - resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} - engines: {node: '>=6.9.0'} - - '@cloudflare/kv-asset-handler@0.4.2': - resolution: {integrity: sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==} - engines: {node: '>=18.0.0'} - - '@esbuild/aix-ppc64@0.25.12': - resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.12': - resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.12': - resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.12': - resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.12': - resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.12': - resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.12': - resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.12': - resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.12': - resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.12': - resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.12': - resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.12': - resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.12': - resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.12': - resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.12': - resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.12': - resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.12': - resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.12': - resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.12': - resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.12': - resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.12': - resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.25.12': - resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.25.12': - resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.12': - resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.12': - resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.12': - resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@ioredis/commands@1.5.1': - resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==} - - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} - - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - - '@jridgewell/gen-mapping@0.3.13': - resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} - - '@jridgewell/remapping@2.3.5': - resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/source-map@0.3.11': - resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@jridgewell/trace-mapping@0.3.31': - resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - - '@mapbox/node-pre-gyp@2.0.3': - resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} - engines: {node: '>=18'} - hasBin: true - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@parcel/watcher-android-arm64@2.5.6': - resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.6': - resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.6': - resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.6': - resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.6': - resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm-musl@2.5.6': - resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-arm64-musl@2.5.6': - resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-x64-glibc@2.5.6': - resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-linux-x64-musl@2.5.6': - resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-wasm@2.5.6': - resolution: {integrity: sha512-byAiBZ1t3tXQvc8dMD/eoyE7lTXYorhn+6uVW5AC+JGI1KtJC/LvDche5cfUE+qiefH+Ybq0bUCJU0aB1cSHUA==} - engines: {node: '>= 10.0.0'} - bundledDependencies: - - napi-wasm - - '@parcel/watcher-win32-arm64@2.5.6': - resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.6': - resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.6': - resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.6': - resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} - engines: {node: '>= 10.0.0'} - - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - - '@poppinss/colors@4.1.6': - resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} - - '@poppinss/dumper@0.7.0': - resolution: {integrity: sha512-0UTYalzk2t6S4rA2uHOz5bSSW2CHdv4vggJI6Alg90yvl0UgXs6XSXpH96OH+bRkX4J/06djv29pqXJ0lq5Kag==} - - '@poppinss/exception@1.2.3': - resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} - - '@rollup/plugin-alias@6.0.0': - resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==} - engines: {node: '>=20.19.0'} - peerDependencies: - rollup: '>=4.0.0' - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-commonjs@29.0.2': - resolution: {integrity: sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==} - engines: {node: '>=16.0.0 || 14 >= 14.17'} - peerDependencies: - rollup: ^2.68.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-inject@5.0.5': - resolution: {integrity: sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-json@6.1.0': - resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-node-resolve@16.0.3': - resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-replace@6.0.3': - resolution: {integrity: sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-terser@0.4.4': - resolution: {integrity: sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/pluginutils@5.3.0': - resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} - cpu: [x64] - os: [win32] - - '@shikijs/core@1.29.2': - resolution: {integrity: sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==} - - '@shikijs/engine-javascript@1.29.2': - resolution: {integrity: sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==} - - '@shikijs/engine-oniguruma@1.29.2': - resolution: {integrity: sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==} - - '@shikijs/langs@1.29.2': - resolution: {integrity: sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==} - - '@shikijs/themes@1.29.2': - resolution: {integrity: sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==} - - '@shikijs/types@1.29.2': - resolution: {integrity: sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==} - - '@shikijs/vscode-textmate@10.0.2': - resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - - '@sindresorhus/is@7.2.0': - resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} - engines: {node: '>=18'} - - '@sindresorhus/merge-streams@4.0.0': - resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} - engines: {node: '>=18'} - - '@solidjs/meta@0.29.4': - resolution: {integrity: sha512-zdIWBGpR9zGx1p1bzIPqF5Gs+Ks/BH8R6fWhmUa/dcK1L2rUC8BAcZJzNRYBQv74kScf1TSOs0EY//Vd/I0V8g==} - peerDependencies: - solid-js: '>=1.8.4' - - '@solidjs/router@0.15.4': - resolution: {integrity: sha512-WOpgg9a9T638cR+5FGbFi/IV4l2FpmBs1GpIMSPa0Ce9vyJN7Wts+X2PqMf9IYn0zUj2MlSJtm1gp7/HI/n5TQ==} - peerDependencies: - solid-js: ^1.8.6 - - '@solidjs/start@2.0.0-alpha.2': - resolution: {integrity: sha512-z56ATi3P07q8F5Io2I+RQrwjyWZtFZzpXN/J+8scf/gqrAW83LtgRkZFZjJaGH7i9WrHP+ep9F+ZiJ2gDHVBcw==} - engines: {node: '>=22'} - peerDependencies: - vite: ^7 - - '@solidjs/vite-plugin-nitro-2@0.1.0': - resolution: {integrity: sha512-gtT9GYhAdbfY2v3ISKYFXclxH/kK+mDhp5ENjiA1zJAfQq6XPWwIfIYm9MB73vfOp+MzVXWDHxYyUv+5pTpGTw==} - peerDependencies: - vite: ^7 - - '@speed-highlight/core@1.2.14': - resolution: {integrity: sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA==} - - '@tanstack/directive-functions-plugin@1.134.5': - resolution: {integrity: sha512-J3oawV8uBRBbPoLgMdyHt+LxzTNuWRKNJJuCLWsm/yq6v0IQSvIVCgfD2+liIiSnDPxGZ8ExduPXy8IzS70eXw==} - engines: {node: '>=12'} - peerDependencies: - vite: '>=6.0.0 || >=7.0.0' - - '@tanstack/router-utils@1.133.19': - resolution: {integrity: sha512-WEp5D2gPxvlLDRXwD/fV7RXjYtqaqJNXKB/L6OyZEbT+9BG/Ib2d7oG9GSUZNNMGPGYAlhBUOi3xutySsk6rxA==} - engines: {node: '>=12'} - - '@tanstack/server-functions-plugin@1.134.5': - resolution: {integrity: sha512-2sWxq70T+dOEUlE3sHlXjEPhaFZfdPYlWTSkHchWXrFGw2YOAa+hzD6L9wHMjGDQezYd03ue8tQlHG+9Jzbzgw==} - engines: {node: '>=12'} - - '@trpc/client@10.45.4': - resolution: {integrity: sha512-ItpH5FMIJFt862kbAgC8hf5NJnDLo0FwEvMEX6zSLCenzWD0UH6H/fvAX1suFo1e0wcAmMhiEU1Tl6xKk/Pd1g==} - peerDependencies: - '@trpc/server': 10.45.4 - - '@trpc/server@10.45.4': - resolution: {integrity: sha512-5MuyK5sDuFVGHOT9EMVHgRjP5I5j3RqGymcb5D47UOp8tzn6LH0g1lEgYrAsOZ12vmk1gpxMw/v/y4xHHK/Qdg==} - - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - - '@types/braces@3.0.5': - resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/hast@3.0.4': - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} - - '@types/mdast@4.0.4': - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} - - '@types/micromatch@4.0.10': - resolution: {integrity: sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==} - - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - - '@types/unist@3.0.3': - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - - '@typeschema/core@0.13.2': - resolution: {integrity: sha512-pAt0MK249/9szYaoPuvzhSfOd3smrLhhwCCpUNB4onX32mRx5F3lzDIveIYGQkLYRq58xOX5sjoW+n72f/MLLw==} - peerDependencies: - '@types/json-schema': ^7.0.15 - peerDependenciesMeta: - '@types/json-schema': - optional: true - - '@typeschema/valibot@0.13.5': - resolution: {integrity: sha512-9S3daaU7ShC1aRkZVmsW5N8uTPE7l4Af/c+rWTyFZi2/yp4Iiped8947WGX03lMd3UW6/UdbfOaiOh7HByQPiw==} - peerDependencies: - '@gcornut/valibot-json-schema': ^0.31.0 - valibot: ^0.31.0 - peerDependenciesMeta: - '@gcornut/valibot-json-schema': - optional: true - valibot: - optional: true - - '@ungap/structured-clone@1.3.0': - resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} - - '@vercel/nft@1.3.2': - resolution: {integrity: sha512-HC8venRc4Ya7vNeBsJneKHHMDDWpQie7VaKhAIOst3MKO+DES+Y/SbzSp8mFkD7OzwAE2HhHkeSuSmwS20mz3A==} - engines: {node: '>=20'} - hasBin: true - - abbrev@3.0.1: - resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} - engines: {node: ^18.17.0 || >=20.5.0} - - abort-controller@3.0.0: - resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} - engines: {node: '>=6.5'} - - acorn-import-attributes@1.9.5: - resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} - peerDependencies: - acorn: ^8 - - acorn@8.16.0: - resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} - engines: {node: '>=0.4.0'} - hasBin: true - - agent-base@7.1.4: - resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} - engines: {node: '>= 14'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-regex@6.2.2: - resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} - engines: {node: '>=12'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - ansi-styles@6.2.3: - resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} - engines: {node: '>=12'} - - ansis@4.2.0: - resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} - engines: {node: '>=14'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - archiver-utils@5.0.2: - resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} - engines: {node: '>= 14'} - - archiver@7.0.1: - resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} - engines: {node: '>= 14'} - - async-sema@3.1.1: - resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} - - async@3.2.6: - resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} - - b4a@1.8.0: - resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} - peerDependencies: - react-native-b4a: '*' - peerDependenciesMeta: - react-native-b4a: - optional: true - - babel-dead-code-elimination@1.0.12: - resolution: {integrity: sha512-GERT7L2TiYcYDtYk1IpD+ASAYXjKbLTDPhBtYj7X1NuRMDTMtAx9kyBenub1Ev41lo91OHCKdmP+egTDmfQ7Ig==} - - babel-plugin-jsx-dom-expressions@0.40.5: - resolution: {integrity: sha512-8TFKemVLDYezqqv4mWz+PhRrkryTzivTGu0twyLrOkVZ0P63COx2Y04eVsUjFlwSOXui1z3P3Pn209dokWnirg==} - peerDependencies: - '@babel/core': ^7.20.12 - - babel-preset-solid@1.9.10: - resolution: {integrity: sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==} - peerDependencies: - '@babel/core': ^7.0.0 - solid-js: ^1.9.10 - peerDependenciesMeta: - solid-js: - optional: true - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - balanced-match@4.0.4: - resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} - engines: {node: 18 || 20 || >=22} - - bare-events@2.8.2: - resolution: {integrity: sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==} - peerDependencies: - bare-abort-controller: '*' - peerDependenciesMeta: - bare-abort-controller: - optional: true - - bare-fs@4.5.5: - resolution: {integrity: sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==} - engines: {bare: '>=1.16.0'} - peerDependencies: - bare-buffer: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - - bare-os@3.7.1: - resolution: {integrity: sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==} - engines: {bare: '>=1.14.0'} - - bare-path@3.0.0: - resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} - - bare-stream@2.8.0: - resolution: {integrity: sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==} - peerDependencies: - bare-buffer: '*' - bare-events: '*' - peerDependenciesMeta: - bare-buffer: - optional: true - bare-events: - optional: true - - bare-url@2.3.2: - resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - baseline-browser-mapping@2.10.0: - resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} - engines: {node: '>=6.0.0'} - hasBin: true - - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - - brace-expansion@2.0.2: - resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - - brace-expansion@5.0.4: - resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} - engines: {node: 18 || 20 || >=22} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.28.1: - resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - buffer-crc32@1.0.0: - resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} - engines: {node: '>=8.0.0'} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - - c12@3.3.3: - resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} - peerDependencies: - magicast: '*' - peerDependenciesMeta: - magicast: - optional: true - - caniuse-lite@1.0.30001774: - resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} - - ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - - character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - - character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - - chokidar@5.0.0: - resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} - engines: {node: '>= 20.19.0'} - - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - - citty@0.1.6: - resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} - - citty@0.2.1: - resolution: {integrity: sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==} - - clipboardy@4.0.0: - resolution: {integrity: sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==} - engines: {node: '>=18'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - - compatx@0.2.0: - resolution: {integrity: sha512-6gLRNt4ygsi5NyMVhceOCFv14CIdDFN7fQjX1U4+47qVE/+kjPoXMK65KWK+dWxmFzMTuKazoQ9sch6pM0p5oA==} - - compress-commons@6.0.2: - resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} - engines: {node: '>= 14'} - - confbox@0.1.8: - resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - - confbox@0.2.4: - resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} - - consola@3.4.2: - resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} - engines: {node: ^14.18.0 || >=16.10.0} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cookie-es@1.2.2: - resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} - - cookie-es@2.0.0: - resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==} - - core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - - crc-32@1.2.2: - resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} - engines: {node: '>=0.8'} - hasBin: true - - crc32-stream@6.0.0: - resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} - engines: {node: '>= 14'} - - croner@9.1.0: - resolution: {integrity: sha512-p9nwwR4qyT5W996vBZhdvBCnMhicY5ytZkR4D1Xj0wuTDEiMnjwR57Q3RXYY/s0EpX6Ay3vgIcfaR+ewGHsi+g==} - engines: {node: '>=18.0'} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - crossws@0.3.5: - resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - - crossws@0.4.4: - resolution: {integrity: sha512-w6c4OdpRNnudVmcgr7brb/+/HmYjMQvYToO/oTrprTwxRUiom3LYWU1PMWuD006okbUWpII1Ea9/+kwpUfmyRg==} - peerDependencies: - srvx: '>=0.7.1' - peerDependenciesMeta: - srvx: - optional: true - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - db0@0.3.4: - resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==} - peerDependencies: - '@electric-sql/pglite': '*' - '@libsql/client': '*' - better-sqlite3: '*' - drizzle-orm: '*' - mysql2: '*' - sqlite3: '*' - peerDependenciesMeta: - '@electric-sql/pglite': - optional: true - '@libsql/client': - optional: true - better-sqlite3: - optional: true - drizzle-orm: - optional: true - mysql2: - optional: true - sqlite3: - optional: true - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - define-lazy-prop@2.0.0: - resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} - engines: {node: '>=8'} - - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - - denque@2.1.0: - resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} - engines: {node: '>=0.10'} - - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - - dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - - destr@2.0.5: - resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - - diff@8.0.3: - resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} - engines: {node: '>=0.3.1'} - - dot-prop@10.1.0: - resolution: {integrity: sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==} - engines: {node: '>=20'} - - dotenv@17.3.1: - resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} - engines: {node: '>=12'} - - duplexer@0.1.2: - resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} - - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - electron-to-chromium@1.5.302: - resolution: {integrity: sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==} - - emoji-regex-xs@1.0.0: - resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} - - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - - entities@6.0.1: - resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} - engines: {node: '>=0.12'} - - error-stack-parser-es@1.0.5: - resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} - - error-stack-parser@2.1.4: - resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} - - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - esbuild@0.25.12: - resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} - engines: {node: '>=18'} - hasBin: true - - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - - escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - event-target-shim@5.0.1: - resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} - engines: {node: '>=6'} - - events-universal@1.0.1: - resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} - - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - - execa@8.0.1: - resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} - engines: {node: '>=16.17'} - - exsolve@1.0.8: - resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} - - fast-fifo@1.3.2: - resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fastq@1.20.1: - resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - foreground-child@3.3.1: - resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} - engines: {node: '>=14'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - 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.*} - - get-port-please@3.2.0: - resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} - - get-stream@8.0.1: - resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} - engines: {node: '>=16'} - - giget@2.0.0: - resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} - hasBin: true - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob@10.5.0: - resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} - deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me - hasBin: true - - glob@13.0.6: - resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} - engines: {node: 18 || 20 || >=22} - - globby@16.1.1: - resolution: {integrity: sha512-dW7vl+yiAJSp6aCekaVnVJxurRv7DCOLyXqEG3RYMYUg7AuJ2jCqPkZTA8ooqC2vtnkaMcV5WfFBMuEnTu1OQg==} - engines: {node: '>=20'} - - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - gzip-size@7.0.0: - resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - h3@1.15.5: - resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} - - h3@2.0.1-rc.4: - resolution: {integrity: sha512-vZq8pEUp6THsXKXrUXX44eOqfChic2wVQ1GlSzQCBr7DeFBkfIZAo2WyNND4GSv54TAa0E4LYIK73WSPdgKUgw==} - engines: {node: '>=20.11.1'} - peerDependencies: - crossws: ^0.4.1 - peerDependenciesMeta: - crossws: - optional: true - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - hast-util-to-html@9.0.5: - resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} - - hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} - - hookable@5.5.3: - resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} - - html-entities@2.3.3: - resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} - - html-to-image@1.11.13: - resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} - - html-void-elements@3.0.0: - resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - - http-shutdown@1.2.2: - resolution: {integrity: sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==} - engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - - https-proxy-agent@7.0.6: - resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} - engines: {node: '>= 14'} - - httpxy@0.1.7: - resolution: {integrity: sha512-pXNx8gnANKAndgga5ahefxc++tJvNL87CXoRwxn1cJE2ZkWEojF3tNfQIEhZX/vfpt+wzeAzpUI4qkediX1MLQ==} - - human-signals@5.0.0: - resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} - engines: {node: '>=16.17.0'} - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} - engines: {node: '>= 4'} - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ioredis@5.10.0: - resolution: {integrity: sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==} - engines: {node: '>=12.22.0'} - - iron-webcrypto@1.2.1: - resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-docker@2.2.1: - resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} - engines: {node: '>=8'} - hasBin: true - - is-docker@3.0.0: - resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - hasBin: true - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-inside-container@1.0.0: - resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} - engines: {node: '>=14.16'} - hasBin: true - - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-path-inside@4.0.0: - resolution: {integrity: sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==} - engines: {node: '>=12'} - - is-reference@1.2.1: - resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - - is-stream@3.0.0: - resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - is-what@4.1.16: - resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} - engines: {node: '>=12.13'} - - is-wsl@2.2.0: - resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} - engines: {node: '>=8'} - - is-wsl@3.1.1: - resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} - engines: {node: '>=16'} - - is64bit@2.0.0: - resolution: {integrity: sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==} - engines: {node: '>=18'} - - isarray@1.0.0: - resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - jackspeak@3.4.3: - resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} - hasBin: true - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - - klona@2.0.6: - resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} - engines: {node: '>= 8'} - - knitwork@1.3.0: - resolution: {integrity: sha512-4LqMNoONzR43B1W0ek0fhXMsDNW/zxa1NdFAVMY+k28pgZLovR4G3PB5MrpTxCy1QaZCqNoiaKPr5w5qZHfSNw==} - - lazystream@1.0.1: - resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} - engines: {node: '>= 0.6.3'} - - listhen@1.9.0: - resolution: {integrity: sha512-I8oW2+QL5KJo8zXNWX046M134WchxsXC7SawLPvRQpogCbkyQIaFxPE89A2HiwR7vAK2Dm2ERBAmyjTYGYEpBg==} - hasBin: true - - local-pkg@1.1.2: - resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} - engines: {node: '>=14'} - - lodash.defaults@4.2.0: - resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} - - lodash.isarguments@3.1.0: - resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} - - lodash@4.17.23: - resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - lru-cache@11.2.6: - resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} - engines: {node: 20 || >=22} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - magicast@0.5.2: - resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} - - mdast-util-to-hast@13.2.1: - resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} - - merge-anything@5.1.7: - resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} - engines: {node: '>=12.13'} - - merge-stream@2.0.0: - resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromark-util-character@2.1.1: - resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} - - micromark-util-encode@2.0.1: - resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} - - micromark-util-sanitize-uri@2.0.1: - resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} - - micromark-util-symbol@2.0.1: - resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} - - micromark-util-types@2.0.2: - resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - - mime@4.1.0: - resolution: {integrity: sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==} - engines: {node: '>=16'} - hasBin: true - - mimic-fn@4.0.0: - resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} - engines: {node: '>=12'} - - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} - engines: {node: 18 || 20 || >=22} - - minimatch@5.1.9: - resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} - engines: {node: '>=10'} - - minimatch@9.0.9: - resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} - engines: {node: '>=16 || 14 >=14.17'} - - minipass@7.1.3: - resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} - engines: {node: '>=16 || 14 >=14.17'} - - minizlib@3.1.0: - resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} - engines: {node: '>= 18'} - - mlly@1.8.1: - resolution: {integrity: sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nitropack@2.13.1: - resolution: {integrity: sha512-2dDj89C4wC2uzG7guF3CnyG+zwkZosPEp7FFBGHB3AJo11AywOolWhyQJFHDzve8COvGxJaqscye9wW2IrUsNw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - xml2js: ^0.6.2 - peerDependenciesMeta: - xml2js: - optional: true - - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - - node-fetch-native@1.6.7: - resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} - - 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-forge@1.3.3: - resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} - engines: {node: '>= 6.13.0'} - - node-gyp-build@4.8.4: - resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} - hasBin: true - - node-mock-http@1.0.4: - resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} - - node-releases@2.0.27: - resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} - - nopt@8.1.0: - resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} - engines: {node: ^18.17.0 || >=20.5.0} - hasBin: true - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - npm-run-path@5.3.0: - resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - - nypm@0.6.5: - resolution: {integrity: sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==} - engines: {node: '>=18'} - hasBin: true - - ofetch@1.5.1: - resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} - - ohash@2.0.11: - resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - onetime@6.0.0: - resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} - engines: {node: '>=12'} - - oniguruma-to-es@2.3.0: - resolution: {integrity: sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==} - - open@8.4.2: - resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} - engines: {node: '>=12'} - - package-json-from-dist@1.0.1: - resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - - parse5@7.3.0: - resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} - - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-key@4.0.0: - resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} - engines: {node: '>=12'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - path-scurry@2.0.2: - resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} - engines: {node: 18 || 20 || >=22} - - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - - pathe@1.1.2: - resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - perfect-debounce@2.1.0: - resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - - pkg-types@1.3.1: - resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} - - pkg-types@2.3.0: - resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} - - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} - engines: {node: ^10 || ^12 || >=14} - - pretty-bytes@7.1.0: - resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} - engines: {node: '>=20'} - - process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} - - process@0.11.10: - resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} - engines: {node: '>= 0.6.0'} - - property-information@7.1.0: - resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - - quansync@0.2.11: - resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - radix3@1.1.2: - resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} - - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - rc9@2.1.2: - resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} - - readable-stream@2.3.8: - resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} - - readable-stream@4.7.0: - resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - readdir-glob@1.1.3: - resolution: {integrity: sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==} - - readdirp@5.0.0: - resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} - engines: {node: '>= 20.19.0'} - - redis-errors@1.2.0: - resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} - engines: {node: '>=4'} - - redis-parser@3.0.0: - resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} - engines: {node: '>=4'} - - regex-recursion@5.1.1: - resolution: {integrity: sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==} - - regex-utilities@2.3.0: - resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} - - regex@5.1.1: - resolution: {integrity: sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - resolve@1.22.11: - resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} - engines: {node: '>= 0.4'} - hasBin: true - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup-plugin-visualizer@6.0.11: - resolution: {integrity: sha512-TBwVHVY7buHjIKVLqr9scTVFwqZqMXINcCphPwIWKPDCOBIa+jCQfafvbjRJDZgXdq/A996Dy6yGJ/+/NtAXDQ==} - engines: {node: '>=18'} - hasBin: true - peerDependencies: - rolldown: 1.x || ^1.0.0-beta - rollup: 2.x || 3.x || 4.x - peerDependenciesMeta: - rolldown: - optional: true - rollup: - optional: true - - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - rou3@0.7.12: - resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - scule@1.3.0: - resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.7.4: - resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} - engines: {node: '>=10'} - hasBin: true - - send@1.2.1: - resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} - engines: {node: '>= 18'} - - serialize-javascript@6.0.2: - resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} - - seroval-plugins@1.5.0: - resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - - seroval@1.5.0: - resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} - engines: {node: '>=10'} - - serve-placeholder@2.0.2: - resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==} - - serve-static@2.2.1: - resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} - engines: {node: '>= 18'} - - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shiki@1.29.2: - resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==} - - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - - smob@1.6.1: - resolution: {integrity: sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==} - engines: {node: '>=20.0.0'} - - solid-js@1.9.11: - resolution: {integrity: sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q==} - - solid-refresh@0.6.3: - resolution: {integrity: sha512-F3aPsX6hVw9ttm5LYlth8Q15x6MlI/J3Dn+o3EQyRTtTxidepSTwAYdozt01/YA+7ObcciagGEyXIopGZzQtbA==} - peerDependencies: - solid-js: ^1.3 - - solid-use@0.9.1: - resolution: {integrity: sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw==} - engines: {node: '>=10'} - peerDependencies: - solid-js: ^1.7 - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - source-map@0.7.6: - resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} - engines: {node: '>= 12'} - - space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - - srvx@0.11.8: - resolution: {integrity: sha512-2n9t0YnAXPJjinytvxccNgs7rOA5gmE7Wowt/8Dy2dx2fDC6sBhfBpbrCvjYKALlVukPS/Uq3QwkolKNa7P/2Q==} - engines: {node: '>=20.16.0'} - hasBin: true - - srvx@0.9.8: - resolution: {integrity: sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ==} - engines: {node: '>=20.16.0'} - hasBin: true - - stackframe@1.3.4: - resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} - - standard-as-callback@2.1.0: - resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} - - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - - streamx@2.23.0: - resolution: {integrity: sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - - string_decoder@1.1.1: - resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-ansi@7.2.0: - resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} - engines: {node: '>=12'} - - strip-final-newline@3.0.0: - resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} - engines: {node: '>=12'} - - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - - supports-color@10.2.2: - resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} - engines: {node: '>=18'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - system-architecture@0.1.0: - resolution: {integrity: sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==} - engines: {node: '>=18'} - - tagged-tag@1.0.0: - resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} - engines: {node: '>=20'} - - tar-stream@3.1.8: - resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} - - tar@7.5.10: - resolution: {integrity: sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==} - engines: {node: '>=18'} - - teex@1.0.1: - resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} - - terracotta@1.1.0: - resolution: {integrity: sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww==} - engines: {node: '>=10'} - peerDependencies: - solid-js: ^1.8 - - terser@5.46.0: - resolution: {integrity: sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==} - engines: {node: '>=10'} - hasBin: true - - text-decoder@1.2.7: - resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} - - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} - engines: {node: '>=18'} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - - type-fest@5.4.4: - resolution: {integrity: sha512-JnTrzGu+zPV3aXIUhnyWJj4z/wigMsdYajGLIYakqyOW1nPllzXEJee0QQbHj+CTIQtXGlAjuK0UY+2xTyjVAw==} - engines: {node: '>=20'} - - ufo@1.6.3: - resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} - - ultrahtml@1.6.0: - resolution: {integrity: sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==} - - uncrypto@0.1.3: - resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} - - unctx@2.5.0: - resolution: {integrity: sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg==} - - unenv@2.0.0-rc.24: - resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} - - unicorn-magic@0.4.0: - resolution: {integrity: sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==} - engines: {node: '>=20'} - - unimport@5.7.0: - resolution: {integrity: sha512-njnL6sp8lEA8QQbZrt+52p/g4X0rw3bnGGmUcJnt1jeG8+iiqO779aGz0PirCtydAIVcuTBRlJ52F0u46z309Q==} - engines: {node: '>=18.12.0'} - - unist-util-is@6.0.1: - resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} - - unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} - - unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} - - unist-util-visit-parents@6.0.2: - resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} - - unist-util-visit@5.1.0: - resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} - - unplugin-utils@0.3.1: - resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} - engines: {node: '>=20.19.0'} - - unplugin@2.3.11: - resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} - engines: {node: '>=18.12.0'} - - unstorage@1.17.4: - resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} - peerDependencies: - '@azure/app-configuration': ^1.8.0 - '@azure/cosmos': ^4.2.0 - '@azure/data-tables': ^13.3.0 - '@azure/identity': ^4.6.0 - '@azure/keyvault-secrets': ^4.9.0 - '@azure/storage-blob': ^12.26.0 - '@capacitor/preferences': ^6 || ^7 || ^8 - '@deno/kv': '>=0.9.0' - '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 - '@planetscale/database': ^1.19.0 - '@upstash/redis': ^1.34.3 - '@vercel/blob': '>=0.27.1' - '@vercel/functions': ^2.2.12 || ^3.0.0 - '@vercel/kv': ^1 || ^2 || ^3 - aws4fetch: ^1.0.20 - db0: '>=0.2.1' - idb-keyval: ^6.2.1 - ioredis: ^5.4.2 - uploadthing: ^7.4.4 - peerDependenciesMeta: - '@azure/app-configuration': - optional: true - '@azure/cosmos': - optional: true - '@azure/data-tables': - optional: true - '@azure/identity': - optional: true - '@azure/keyvault-secrets': - optional: true - '@azure/storage-blob': - optional: true - '@capacitor/preferences': - optional: true - '@deno/kv': - optional: true - '@netlify/blobs': - optional: true - '@planetscale/database': - optional: true - '@upstash/redis': - optional: true - '@vercel/blob': - optional: true - '@vercel/functions': - optional: true - '@vercel/kv': - optional: true - aws4fetch: - optional: true - db0: - optional: true - idb-keyval: - optional: true - ioredis: - optional: true - uploadthing: - optional: true - - untun@0.1.3: - resolution: {integrity: sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==} - hasBin: true - - untyped@2.0.0: - resolution: {integrity: sha512-nwNCjxJTjNuLCgFr42fEak5OcLuB3ecca+9ksPFNvtfYSLpjf+iJqSIaSnIile6ZPbKYxI5k2AfXqeopGudK/g==} - hasBin: true - - unwasm@0.5.3: - resolution: {integrity: sha512-keBgTSfp3r6+s9ZcSma+0chwxQdmLbB5+dAD9vjtB21UTMYuKAxHXCU1K2CbCtnP09EaWeRvACnXk0EJtUx+hw==} - - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uqr@0.1.2: - resolution: {integrity: sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - valibot@0.29.0: - resolution: {integrity: sha512-JhZn08lwZPhAamOCfBwBkv/btQt4KeQhekULPH8crH053zUCLSOGEF2zKExu3bFf245tsj6J1dY0ysd/jUiMIQ==} - - vfile-message@4.0.3: - resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} - - vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - - vite-plugin-solid@2.11.10: - resolution: {integrity: sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==} - peerDependencies: - '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* - solid-js: ^1.7.2 - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - peerDependenciesMeta: - '@testing-library/jest-dom': - optional: true - - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitefu@1.1.2: - resolution: {integrity: sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0 - peerDependenciesMeta: - vite: - optional: true - - 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==} - - 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 - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - - y18n@5.0.8: - 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'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - - youch-core@0.3.3: - resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} - - youch@4.1.0: - resolution: {integrity: sha512-cYekNh2tUoU+voS11X0D0UQntVCSO6LQ1h10VriQGmfbpf0mnGTruwZICts23UUNiZCXm8H8hQBtRrdsbhuNNg==} - - zip-stream@6.0.1: - resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} - engines: {node: '>= 14'} - - zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - -snapshots: - - '@babel/code-frame@7.27.1': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.29.0': {} - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.29.1': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - jsesc: 3.1.0 - - '@babel/helper-annotate-as-pure@7.27.3': - dependencies: - '@babel/types': 7.29.0 - - '@babel/helper-compilation-targets@7.28.6': - dependencies: - '@babel/compat-data': 7.29.0 - '@babel/helper-validator-option': 7.27.1 - browserslist: 4.28.1 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.0 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/helper-globals@7.28.0': {} - - '@babel/helper-member-expression-to-functions@7.28.5': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-imports@7.18.6': - dependencies: - '@babel/types': 7.29.0 - - '@babel/helper-module-imports@7.28.6': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 - '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-optimise-call-expression@7.27.1': - dependencies: - '@babel/types': 7.29.0 - - '@babel/helper-plugin-utils@7.28.6': {} - - '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-member-expression-to-functions': 7.28.5 - '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': - dependencies: - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-string-parser@7.27.1': {} - - '@babel/helper-validator-identifier@7.28.5': {} - - '@babel/helper-validator-option@7.27.1': {} - - '@babel/helpers@7.28.6': - dependencies: - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - - '@babel/parser@7.29.0': - dependencies: - '@babel/types': 7.29.0 - - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - - '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - transitivePeerDependencies: - - supports-color - - '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0) - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) - transitivePeerDependencies: - - supports-color - - '@babel/template@7.28.6': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - - '@babel/traverse@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/types': 7.29.0 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.29.0': - dependencies: - '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.28.5 - - '@cloudflare/kv-asset-handler@0.4.2': {} - - '@esbuild/aix-ppc64@0.25.12': - optional: true - - '@esbuild/aix-ppc64@0.27.3': - optional: true - - '@esbuild/android-arm64@0.25.12': - optional: true - - '@esbuild/android-arm64@0.27.3': - optional: true - - '@esbuild/android-arm@0.25.12': - optional: true - - '@esbuild/android-arm@0.27.3': - optional: true - - '@esbuild/android-x64@0.25.12': - optional: true - - '@esbuild/android-x64@0.27.3': - optional: true - - '@esbuild/darwin-arm64@0.25.12': - optional: true - - '@esbuild/darwin-arm64@0.27.3': - optional: true - - '@esbuild/darwin-x64@0.25.12': - optional: true - - '@esbuild/darwin-x64@0.27.3': - optional: true - - '@esbuild/freebsd-arm64@0.25.12': - optional: true - - '@esbuild/freebsd-arm64@0.27.3': - optional: true - - '@esbuild/freebsd-x64@0.25.12': - optional: true - - '@esbuild/freebsd-x64@0.27.3': - optional: true - - '@esbuild/linux-arm64@0.25.12': - optional: true - - '@esbuild/linux-arm64@0.27.3': - optional: true - - '@esbuild/linux-arm@0.25.12': - optional: true - - '@esbuild/linux-arm@0.27.3': - optional: true - - '@esbuild/linux-ia32@0.25.12': - optional: true - - '@esbuild/linux-ia32@0.27.3': - optional: true - - '@esbuild/linux-loong64@0.25.12': - optional: true - - '@esbuild/linux-loong64@0.27.3': - optional: true - - '@esbuild/linux-mips64el@0.25.12': - optional: true - - '@esbuild/linux-mips64el@0.27.3': - optional: true - - '@esbuild/linux-ppc64@0.25.12': - optional: true - - '@esbuild/linux-ppc64@0.27.3': - optional: true - - '@esbuild/linux-riscv64@0.25.12': - optional: true - - '@esbuild/linux-riscv64@0.27.3': - optional: true - - '@esbuild/linux-s390x@0.25.12': - optional: true - - '@esbuild/linux-s390x@0.27.3': - optional: true - - '@esbuild/linux-x64@0.25.12': - optional: true - - '@esbuild/linux-x64@0.27.3': - optional: true - - '@esbuild/netbsd-arm64@0.25.12': - optional: true - - '@esbuild/netbsd-arm64@0.27.3': - optional: true - - '@esbuild/netbsd-x64@0.25.12': - optional: true - - '@esbuild/netbsd-x64@0.27.3': - optional: true - - '@esbuild/openbsd-arm64@0.25.12': - optional: true - - '@esbuild/openbsd-arm64@0.27.3': - optional: true - - '@esbuild/openbsd-x64@0.25.12': - optional: true - - '@esbuild/openbsd-x64@0.27.3': - optional: true - - '@esbuild/openharmony-arm64@0.25.12': - optional: true - - '@esbuild/openharmony-arm64@0.27.3': - optional: true - - '@esbuild/sunos-x64@0.25.12': - optional: true - - '@esbuild/sunos-x64@0.27.3': - optional: true - - '@esbuild/win32-arm64@0.25.12': - optional: true - - '@esbuild/win32-arm64@0.27.3': - optional: true - - '@esbuild/win32-ia32@0.25.12': - optional: true - - '@esbuild/win32-ia32@0.27.3': - optional: true - - '@esbuild/win32-x64@0.25.12': - optional: true - - '@esbuild/win32-x64@0.27.3': - optional: true - - '@ioredis/commands@1.5.1': {} - - '@isaacs/cliui@8.0.2': - dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.2.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 - - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.3 - - '@jridgewell/gen-mapping@0.3.13': - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/remapping@2.3.5': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/source-map@0.3.11': - dependencies: - '@jridgewell/gen-mapping': 0.3.13 - '@jridgewell/trace-mapping': 0.3.31 - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@jridgewell/trace-mapping@0.3.31': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.5 - - '@mapbox/node-pre-gyp@2.0.3': - dependencies: - consola: 3.4.2 - detect-libc: 2.1.2 - https-proxy-agent: 7.0.6 - node-fetch: 2.7.0 - nopt: 8.1.0 - semver: 7.7.4 - tar: 7.5.10 - transitivePeerDependencies: - - encoding - - supports-color - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.20.1 - - '@parcel/watcher-android-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.6': - optional: true - - '@parcel/watcher-darwin-x64@2.5.6': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.6': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.6': - optional: true - - '@parcel/watcher-wasm@2.5.6': - dependencies: - is-glob: 4.0.3 - picomatch: 4.0.3 - - '@parcel/watcher-win32-arm64@2.5.6': - optional: true - - '@parcel/watcher-win32-ia32@2.5.6': - optional: true - - '@parcel/watcher-win32-x64@2.5.6': - optional: true - - '@parcel/watcher@2.5.6': - dependencies: - detect-libc: 2.1.2 - is-glob: 4.0.3 - node-addon-api: 7.1.1 - picomatch: 4.0.3 - optionalDependencies: - '@parcel/watcher-android-arm64': 2.5.6 - '@parcel/watcher-darwin-arm64': 2.5.6 - '@parcel/watcher-darwin-x64': 2.5.6 - '@parcel/watcher-freebsd-x64': 2.5.6 - '@parcel/watcher-linux-arm-glibc': 2.5.6 - '@parcel/watcher-linux-arm-musl': 2.5.6 - '@parcel/watcher-linux-arm64-glibc': 2.5.6 - '@parcel/watcher-linux-arm64-musl': 2.5.6 - '@parcel/watcher-linux-x64-glibc': 2.5.6 - '@parcel/watcher-linux-x64-musl': 2.5.6 - '@parcel/watcher-win32-arm64': 2.5.6 - '@parcel/watcher-win32-ia32': 2.5.6 - '@parcel/watcher-win32-x64': 2.5.6 - - '@pkgjs/parseargs@0.11.0': - optional: true - - '@poppinss/colors@4.1.6': - dependencies: - kleur: 4.1.5 - - '@poppinss/dumper@0.7.0': - dependencies: - '@poppinss/colors': 4.1.6 - '@sindresorhus/is': 7.2.0 - supports-color: 10.2.2 - - '@poppinss/exception@1.2.3': {} - - '@rollup/plugin-alias@6.0.0(rollup@4.59.0)': - optionalDependencies: - rollup: 4.59.0 - - '@rollup/plugin-commonjs@29.0.2(rollup@4.59.0)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - commondir: 1.0.1 - estree-walker: 2.0.2 - fdir: 6.5.0(picomatch@4.0.3) - is-reference: 1.2.1 - magic-string: 0.30.21 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.59.0 - - '@rollup/plugin-inject@5.0.5(rollup@4.59.0)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - estree-walker: 2.0.2 - magic-string: 0.30.21 - optionalDependencies: - rollup: 4.59.0 - - '@rollup/plugin-json@6.1.0(rollup@4.59.0)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - optionalDependencies: - rollup: 4.59.0 - - '@rollup/plugin-node-resolve@16.0.3(rollup@4.59.0)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.11 - optionalDependencies: - rollup: 4.59.0 - - '@rollup/plugin-replace@6.0.3(rollup@4.59.0)': - dependencies: - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - magic-string: 0.30.21 - optionalDependencies: - rollup: 4.59.0 - - '@rollup/plugin-terser@0.4.4(rollup@4.59.0)': - dependencies: - serialize-javascript: 6.0.2 - smob: 1.6.1 - terser: 5.46.0 - optionalDependencies: - rollup: 4.59.0 - - '@rollup/pluginutils@5.3.0(rollup@4.59.0)': - dependencies: - '@types/estree': 1.0.8 - estree-walker: 2.0.2 - picomatch: 4.0.3 - optionalDependencies: - rollup: 4.59.0 - - '@rollup/rollup-android-arm-eabi@4.59.0': - optional: true - - '@rollup/rollup-android-arm64@4.59.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.59.0': - optional: true - - '@rollup/rollup-darwin-x64@4.59.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.59.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.59.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.59.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.59.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.59.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.59.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.59.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.59.0': - optional: true - - '@shikijs/core@1.29.2': - dependencies: - '@shikijs/engine-javascript': 1.29.2 - '@shikijs/engine-oniguruma': 1.29.2 - '@shikijs/types': 1.29.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - hast-util-to-html: 9.0.5 - - '@shikijs/engine-javascript@1.29.2': - dependencies: - '@shikijs/types': 1.29.2 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 2.3.0 - - '@shikijs/engine-oniguruma@1.29.2': - dependencies: - '@shikijs/types': 1.29.2 - '@shikijs/vscode-textmate': 10.0.2 - - '@shikijs/langs@1.29.2': - dependencies: - '@shikijs/types': 1.29.2 - - '@shikijs/themes@1.29.2': - dependencies: - '@shikijs/types': 1.29.2 - - '@shikijs/types@1.29.2': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - '@shikijs/vscode-textmate@10.0.2': {} - - '@sindresorhus/is@7.2.0': {} - - '@sindresorhus/merge-streams@4.0.0': {} - - '@solidjs/meta@0.29.4(solid-js@1.9.11)': - dependencies: - solid-js: 1.9.11 - - '@solidjs/router@0.15.4(solid-js@1.9.11)': - dependencies: - solid-js: 1.9.11 - - '@solidjs/start@2.0.0-alpha.2(crossws@0.4.4(srvx@0.11.8))(vite@7.3.1(jiti@2.6.1)(terser@5.46.0))': - dependencies: - '@babel/core': 7.29.0 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@solidjs/meta': 0.29.4(solid-js@1.9.11) - '@tanstack/server-functions-plugin': 1.134.5(vite@7.3.1(jiti@2.6.1)(terser@5.46.0)) - '@types/babel__traverse': 7.28.0 - '@types/micromatch': 4.0.10 - cookie-es: 2.0.0 - defu: 6.1.4 - error-stack-parser: 2.1.4 - es-module-lexer: 1.7.0 - esbuild: 0.25.12 - fast-glob: 3.3.3 - h3: 2.0.1-rc.4(crossws@0.4.4(srvx@0.11.8)) - html-to-image: 1.11.13 - micromatch: 4.0.8 - path-to-regexp: 8.3.0 - pathe: 2.0.3 - radix3: 1.1.2 - seroval: 1.5.0 - seroval-plugins: 1.5.0(seroval@1.5.0) - shiki: 1.29.2 - solid-js: 1.9.11 - source-map-js: 1.2.1 - srvx: 0.9.8 - terracotta: 1.1.0(solid-js@1.9.11) - vite: 7.3.1(jiti@2.6.1)(terser@5.46.0) - vite-plugin-solid: 2.11.10(solid-js@1.9.11)(vite@7.3.1(jiti@2.6.1)(terser@5.46.0)) - transitivePeerDependencies: - - '@testing-library/jest-dom' - - crossws - - supports-color - - '@solidjs/vite-plugin-nitro-2@0.1.0(vite@7.3.1(jiti@2.6.1)(terser@5.46.0))': - dependencies: - nitropack: 2.13.1 - vite: 7.3.1(jiti@2.6.1)(terser@5.46.0) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@libsql/client' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bare-abort-controller - - bare-buffer - - better-sqlite3 - - drizzle-orm - - encoding - - idb-keyval - - mysql2 - - react-native-b4a - - rolldown - - sqlite3 - - supports-color - - uploadthing - - xml2js - - '@speed-highlight/core@1.2.14': {} - - '@tanstack/directive-functions-plugin@1.134.5(vite@7.3.1(jiti@2.6.1)(terser@5.46.0))': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@tanstack/router-utils': 1.133.19 - babel-dead-code-elimination: 1.0.12 - pathe: 2.0.3 - tiny-invariant: 1.3.3 - vite: 7.3.1(jiti@2.6.1)(terser@5.46.0) - transitivePeerDependencies: - - supports-color - - '@tanstack/router-utils@1.133.19': - dependencies: - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/parser': 7.29.0 - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - ansis: 4.2.0 - diff: 8.0.3 - pathe: 2.0.3 - tinyglobby: 0.2.15 - transitivePeerDependencies: - - supports-color - - '@tanstack/server-functions-plugin@1.134.5(vite@7.3.1(jiti@2.6.1)(terser@5.46.0))': - dependencies: - '@babel/code-frame': 7.27.1 - '@babel/core': 7.29.0 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@tanstack/directive-functions-plugin': 1.134.5(vite@7.3.1(jiti@2.6.1)(terser@5.46.0)) - babel-dead-code-elimination: 1.0.12 - tiny-invariant: 1.3.3 - transitivePeerDependencies: - - supports-color - - vite - - '@trpc/client@10.45.4(@trpc/server@10.45.4)': - dependencies: - '@trpc/server': 10.45.4 - - '@trpc/server@10.45.4': {} - - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.29.0 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.29.0 - - '@types/braces@3.0.5': {} - - '@types/estree@1.0.8': {} - - '@types/hast@3.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/mdast@4.0.4': - dependencies: - '@types/unist': 3.0.3 - - '@types/micromatch@4.0.10': - dependencies: - '@types/braces': 3.0.5 - - '@types/resolve@1.20.2': {} - - '@types/unist@3.0.3': {} - - '@typeschema/core@0.13.2': {} - - '@typeschema/valibot@0.13.5(valibot@0.29.0)': - dependencies: - '@typeschema/core': 0.13.2 - optionalDependencies: - valibot: 0.29.0 - transitivePeerDependencies: - - '@types/json-schema' - - '@ungap/structured-clone@1.3.0': {} - - '@vercel/nft@1.3.2(rollup@4.59.0)': - dependencies: - '@mapbox/node-pre-gyp': 2.0.3 - '@rollup/pluginutils': 5.3.0(rollup@4.59.0) - acorn: 8.16.0 - acorn-import-attributes: 1.9.5(acorn@8.16.0) - async-sema: 3.1.1 - bindings: 1.5.0 - estree-walker: 2.0.2 - glob: 13.0.6 - graceful-fs: 4.2.11 - node-gyp-build: 4.8.4 - picomatch: 4.0.3 - resolve-from: 5.0.0 - transitivePeerDependencies: - - encoding - - rollup - - supports-color - - abbrev@3.0.1: {} - - abort-controller@3.0.0: - dependencies: - event-target-shim: 5.0.1 - - acorn-import-attributes@1.9.5(acorn@8.16.0): - dependencies: - acorn: 8.16.0 - - acorn@8.16.0: {} - - agent-base@7.1.4: {} - - ansi-regex@5.0.1: {} - - ansi-regex@6.2.2: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - ansi-styles@6.2.3: {} - - ansis@4.2.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - archiver-utils@5.0.2: - dependencies: - glob: 10.5.0 - graceful-fs: 4.2.11 - is-stream: 2.0.1 - lazystream: 1.0.1 - lodash: 4.17.23 - normalize-path: 3.0.0 - readable-stream: 4.7.0 - - archiver@7.0.1: - dependencies: - archiver-utils: 5.0.2 - async: 3.2.6 - buffer-crc32: 1.0.0 - readable-stream: 4.7.0 - readdir-glob: 1.1.3 - tar-stream: 3.1.8 - zip-stream: 6.0.1 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - async-sema@3.1.1: {} - - async@3.2.6: {} - - b4a@1.8.0: {} - - babel-dead-code-elimination@1.0.12: - dependencies: - '@babel/core': 7.29.0 - '@babel/parser': 7.29.0 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - transitivePeerDependencies: - - supports-color - - babel-plugin-jsx-dom-expressions@0.40.5(@babel/core@7.29.0): - dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.18.6 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/types': 7.29.0 - html-entities: 2.3.3 - parse5: 7.3.0 - - babel-preset-solid@1.9.10(@babel/core@7.29.0)(solid-js@1.9.11): - dependencies: - '@babel/core': 7.29.0 - babel-plugin-jsx-dom-expressions: 0.40.5(@babel/core@7.29.0) - optionalDependencies: - solid-js: 1.9.11 - - balanced-match@1.0.2: {} - - balanced-match@4.0.4: {} - - bare-events@2.8.2: {} - - bare-fs@4.5.5: - dependencies: - bare-events: 2.8.2 - bare-path: 3.0.0 - bare-stream: 2.8.0(bare-events@2.8.2) - bare-url: 2.3.2 - fast-fifo: 1.3.2 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - bare-os@3.7.1: {} - - bare-path@3.0.0: - dependencies: - bare-os: 3.7.1 - - bare-stream@2.8.0(bare-events@2.8.2): - dependencies: - streamx: 2.23.0 - teex: 1.0.1 - optionalDependencies: - bare-events: 2.8.2 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - bare-url@2.3.2: - dependencies: - bare-path: 3.0.0 - - base64-js@1.5.1: {} - - baseline-browser-mapping@2.10.0: {} - - bindings@1.5.0: - dependencies: - file-uri-to-path: 1.0.0 - - brace-expansion@2.0.2: - dependencies: - balanced-match: 1.0.2 - - brace-expansion@5.0.4: - dependencies: - balanced-match: 4.0.4 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.28.1: - dependencies: - baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001774 - electron-to-chromium: 1.5.302 - node-releases: 2.0.27 - update-browserslist-db: 1.2.3(browserslist@4.28.1) - - buffer-crc32@1.0.0: {} - - buffer-from@1.1.2: {} - - buffer@6.0.3: - dependencies: - base64-js: 1.5.1 - ieee754: 1.2.1 - - c12@3.3.3(magicast@0.5.2): - dependencies: - chokidar: 5.0.0 - confbox: 0.2.4 - defu: 6.1.4 - dotenv: 17.3.1 - exsolve: 1.0.8 - giget: 2.0.0 - jiti: 2.6.1 - ohash: 2.0.11 - pathe: 2.0.3 - perfect-debounce: 2.1.0 - pkg-types: 2.3.0 - rc9: 2.1.2 - optionalDependencies: - magicast: 0.5.2 - - caniuse-lite@1.0.30001774: {} - - ccount@2.0.1: {} - - character-entities-html4@2.1.0: {} - - character-entities-legacy@3.0.0: {} - - chokidar@5.0.0: - dependencies: - readdirp: 5.0.0 - - chownr@3.0.0: {} - - citty@0.1.6: - dependencies: - consola: 3.4.2 - - citty@0.2.1: {} - - clipboardy@4.0.0: - dependencies: - execa: 8.0.1 - is-wsl: 3.1.1 - is64bit: 2.0.0 - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - cluster-key-slot@1.1.2: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - comma-separated-tokens@2.0.3: {} - - commander@2.20.3: {} - - commondir@1.0.1: {} - - compatx@0.2.0: {} - - compress-commons@6.0.2: - dependencies: - crc-32: 1.2.2 - crc32-stream: 6.0.0 - is-stream: 2.0.1 - normalize-path: 3.0.0 - readable-stream: 4.7.0 - - confbox@0.1.8: {} - - confbox@0.2.4: {} - - consola@3.4.2: {} - - convert-source-map@2.0.0: {} - - cookie-es@1.2.2: {} - - cookie-es@2.0.0: {} - - core-util-is@1.0.3: {} - - crc-32@1.2.2: {} - - crc32-stream@6.0.0: - dependencies: - crc-32: 1.2.2 - readable-stream: 4.7.0 - - croner@9.1.0: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - crossws@0.3.5: - dependencies: - uncrypto: 0.1.3 - - crossws@0.4.4(srvx@0.11.8): - optionalDependencies: - srvx: 0.11.8 - optional: true - - csstype@3.2.3: {} - - db0@0.3.4: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deepmerge@4.3.1: {} - - define-lazy-prop@2.0.0: {} - - defu@6.1.4: {} - - denque@2.1.0: {} - - depd@2.0.0: {} - - dequal@2.0.3: {} - - destr@2.0.5: {} - - detect-libc@2.1.2: {} - - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - - diff@8.0.3: {} - - dot-prop@10.1.0: - dependencies: - type-fest: 5.4.4 - - dotenv@17.3.1: {} - - duplexer@0.1.2: {} - - eastasianwidth@0.2.0: {} - - ee-first@1.1.1: {} - - electron-to-chromium@1.5.302: {} - - emoji-regex-xs@1.0.0: {} - - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} - - encodeurl@2.0.0: {} - - entities@6.0.1: {} - - error-stack-parser-es@1.0.5: {} - - error-stack-parser@2.1.4: - dependencies: - stackframe: 1.3.4 - - es-module-lexer@1.7.0: {} - - esbuild@0.25.12: - optionalDependencies: - '@esbuild/aix-ppc64': 0.25.12 - '@esbuild/android-arm': 0.25.12 - '@esbuild/android-arm64': 0.25.12 - '@esbuild/android-x64': 0.25.12 - '@esbuild/darwin-arm64': 0.25.12 - '@esbuild/darwin-x64': 0.25.12 - '@esbuild/freebsd-arm64': 0.25.12 - '@esbuild/freebsd-x64': 0.25.12 - '@esbuild/linux-arm': 0.25.12 - '@esbuild/linux-arm64': 0.25.12 - '@esbuild/linux-ia32': 0.25.12 - '@esbuild/linux-loong64': 0.25.12 - '@esbuild/linux-mips64el': 0.25.12 - '@esbuild/linux-ppc64': 0.25.12 - '@esbuild/linux-riscv64': 0.25.12 - '@esbuild/linux-s390x': 0.25.12 - '@esbuild/linux-x64': 0.25.12 - '@esbuild/netbsd-arm64': 0.25.12 - '@esbuild/netbsd-x64': 0.25.12 - '@esbuild/openbsd-arm64': 0.25.12 - '@esbuild/openbsd-x64': 0.25.12 - '@esbuild/openharmony-arm64': 0.25.12 - '@esbuild/sunos-x64': 0.25.12 - '@esbuild/win32-arm64': 0.25.12 - '@esbuild/win32-ia32': 0.25.12 - '@esbuild/win32-x64': 0.25.12 - - esbuild@0.27.3: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 - - escalade@3.2.0: {} - - escape-html@1.0.3: {} - - escape-string-regexp@5.0.0: {} - - estree-walker@2.0.2: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - etag@1.8.1: {} - - event-target-shim@5.0.1: {} - - events-universal@1.0.1: - dependencies: - bare-events: 2.8.2 - transitivePeerDependencies: - - bare-abort-controller - - events@3.3.0: {} - - execa@8.0.1: - dependencies: - cross-spawn: 7.0.6 - get-stream: 8.0.1 - human-signals: 5.0.0 - is-stream: 3.0.0 - merge-stream: 2.0.0 - npm-run-path: 5.3.0 - onetime: 6.0.0 - signal-exit: 4.1.0 - strip-final-newline: 3.0.0 - - exsolve@1.0.8: {} - - fast-fifo@1.3.2: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fastq@1.20.1: - dependencies: - reusify: 1.1.0 - - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - - file-uri-to-path@1.0.0: {} - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - foreground-child@3.3.1: - dependencies: - cross-spawn: 7.0.6 - signal-exit: 4.1.0 - - fresh@2.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - gensync@1.0.0-beta.2: {} - - get-caller-file@2.0.5: {} - - get-port-please@3.2.0: {} - - get-stream@8.0.1: {} - - giget@2.0.0: - dependencies: - citty: 0.1.6 - consola: 3.4.2 - defu: 6.1.4 - node-fetch-native: 1.6.7 - nypm: 0.6.5 - pathe: 2.0.3 - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob@10.5.0: - dependencies: - foreground-child: 3.3.1 - jackspeak: 3.4.3 - minimatch: 9.0.9 - minipass: 7.1.3 - package-json-from-dist: 1.0.1 - path-scurry: 1.11.1 - - glob@13.0.6: - dependencies: - minimatch: 10.2.4 - minipass: 7.1.3 - path-scurry: 2.0.2 - - globby@16.1.1: - dependencies: - '@sindresorhus/merge-streams': 4.0.0 - fast-glob: 3.3.3 - ignore: 7.0.5 - is-path-inside: 4.0.0 - slash: 5.1.0 - unicorn-magic: 0.4.0 - - graceful-fs@4.2.11: {} - - gzip-size@7.0.0: - dependencies: - duplexer: 0.1.2 - - h3@1.15.5: - dependencies: - cookie-es: 1.2.2 - crossws: 0.3.5 - defu: 6.1.4 - destr: 2.0.5 - iron-webcrypto: 1.2.1 - node-mock-http: 1.0.4 - radix3: 1.1.2 - ufo: 1.6.3 - uncrypto: 0.1.3 - - h3@2.0.1-rc.4(crossws@0.4.4(srvx@0.11.8)): - dependencies: - rou3: 0.7.12 - srvx: 0.9.8 - optionalDependencies: - crossws: 0.4.4(srvx@0.11.8) - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - hast-util-to-html@9.0.5: - dependencies: - '@types/hast': 3.0.4 - '@types/unist': 3.0.3 - ccount: 2.0.1 - comma-separated-tokens: 2.0.3 - hast-util-whitespace: 3.0.0 - html-void-elements: 3.0.0 - mdast-util-to-hast: 13.2.1 - property-information: 7.1.0 - space-separated-tokens: 2.0.2 - stringify-entities: 4.0.4 - zwitch: 2.0.4 - - hast-util-whitespace@3.0.0: - dependencies: - '@types/hast': 3.0.4 - - hookable@5.5.3: {} - - html-entities@2.3.3: {} - - html-to-image@1.11.13: {} - - html-void-elements@3.0.0: {} - - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - http-shutdown@1.2.2: {} - - https-proxy-agent@7.0.6: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - - httpxy@0.1.7: {} - - human-signals@5.0.0: {} - - ieee754@1.2.1: {} - - ignore@7.0.5: {} - - inherits@2.0.4: {} - - ioredis@5.10.0: - dependencies: - '@ioredis/commands': 1.5.1 - cluster-key-slot: 1.1.2 - debug: 4.4.3 - denque: 2.1.0 - lodash.defaults: 4.2.0 - lodash.isarguments: 3.1.0 - redis-errors: 1.2.0 - redis-parser: 3.0.0 - standard-as-callback: 2.1.0 - transitivePeerDependencies: - - supports-color - - iron-webcrypto@1.2.1: {} - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-docker@2.2.1: {} - - is-docker@3.0.0: {} - - is-extglob@2.1.1: {} - - is-fullwidth-code-point@3.0.0: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-inside-container@1.0.0: - dependencies: - is-docker: 3.0.0 - - is-module@1.0.0: {} - - is-number@7.0.0: {} - - is-path-inside@4.0.0: {} - - is-reference@1.2.1: - dependencies: - '@types/estree': 1.0.8 - - is-stream@2.0.1: {} - - is-stream@3.0.0: {} - - is-what@4.1.16: {} - - is-wsl@2.2.0: - dependencies: - is-docker: 2.2.1 - - is-wsl@3.1.1: - dependencies: - is-inside-container: 1.0.0 - - is64bit@2.0.0: - dependencies: - system-architecture: 0.1.0 - - isarray@1.0.0: {} - - isexe@2.0.0: {} - - jackspeak@3.4.3: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 - - jiti@2.6.1: {} - - js-tokens@4.0.0: {} - - js-tokens@9.0.1: {} - - jsesc@3.1.0: {} - - json5@2.2.3: {} - - kleur@4.1.5: {} - - klona@2.0.6: {} - - knitwork@1.3.0: {} - - lazystream@1.0.1: - dependencies: - readable-stream: 2.3.8 - - listhen@1.9.0: - dependencies: - '@parcel/watcher': 2.5.6 - '@parcel/watcher-wasm': 2.5.6 - citty: 0.1.6 - clipboardy: 4.0.0 - consola: 3.4.2 - crossws: 0.3.5 - defu: 6.1.4 - get-port-please: 3.2.0 - h3: 1.15.5 - http-shutdown: 1.2.2 - jiti: 2.6.1 - mlly: 1.8.1 - node-forge: 1.3.3 - pathe: 1.1.2 - std-env: 3.10.0 - ufo: 1.6.3 - untun: 0.1.3 - uqr: 0.1.2 - - local-pkg@1.1.2: - dependencies: - mlly: 1.8.1 - pkg-types: 2.3.0 - quansync: 0.2.11 - - lodash.defaults@4.2.0: {} - - lodash.isarguments@3.1.0: {} - - lodash@4.17.23: {} - - lru-cache@10.4.3: {} - - lru-cache@11.2.6: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - magicast@0.5.2: - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - source-map-js: 1.2.1 - - mdast-util-to-hast@13.2.1: - dependencies: - '@types/hast': 3.0.4 - '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.3.0 - devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.1 - trim-lines: 3.0.1 - unist-util-position: 5.0.0 - unist-util-visit: 5.1.0 - vfile: 6.0.3 - - merge-anything@5.1.7: - dependencies: - is-what: 4.1.16 - - merge-stream@2.0.0: {} - - merge2@1.4.1: {} - - micromark-util-character@2.1.1: - dependencies: - micromark-util-symbol: 2.0.1 - micromark-util-types: 2.0.2 - - micromark-util-encode@2.0.1: {} - - micromark-util-sanitize-uri@2.0.1: - dependencies: - micromark-util-character: 2.1.1 - micromark-util-encode: 2.0.1 - micromark-util-symbol: 2.0.1 - - micromark-util-symbol@2.0.1: {} - - micromark-util-types@2.0.2: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mime-db@1.54.0: {} - - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - - mime@4.1.0: {} - - mimic-fn@4.0.0: {} - - minimatch@10.2.4: - dependencies: - brace-expansion: 5.0.4 - - minimatch@5.1.9: - dependencies: - brace-expansion: 2.0.2 - - minimatch@9.0.9: - dependencies: - brace-expansion: 2.0.2 - - minipass@7.1.3: {} - - minizlib@3.1.0: - dependencies: - minipass: 7.1.3 - - mlly@1.8.1: - dependencies: - acorn: 8.16.0 - pathe: 2.0.3 - pkg-types: 1.3.1 - ufo: 1.6.3 - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - nitropack@2.13.1: - dependencies: - '@cloudflare/kv-asset-handler': 0.4.2 - '@rollup/plugin-alias': 6.0.0(rollup@4.59.0) - '@rollup/plugin-commonjs': 29.0.2(rollup@4.59.0) - '@rollup/plugin-inject': 5.0.5(rollup@4.59.0) - '@rollup/plugin-json': 6.1.0(rollup@4.59.0) - '@rollup/plugin-node-resolve': 16.0.3(rollup@4.59.0) - '@rollup/plugin-replace': 6.0.3(rollup@4.59.0) - '@rollup/plugin-terser': 0.4.4(rollup@4.59.0) - '@vercel/nft': 1.3.2(rollup@4.59.0) - archiver: 7.0.1 - c12: 3.3.3(magicast@0.5.2) - chokidar: 5.0.0 - citty: 0.1.6 - compatx: 0.2.0 - confbox: 0.2.4 - consola: 3.4.2 - cookie-es: 2.0.0 - croner: 9.1.0 - crossws: 0.3.5 - db0: 0.3.4 - defu: 6.1.4 - destr: 2.0.5 - dot-prop: 10.1.0 - esbuild: 0.27.3 - escape-string-regexp: 5.0.0 - etag: 1.8.1 - exsolve: 1.0.8 - globby: 16.1.1 - gzip-size: 7.0.0 - h3: 1.15.5 - hookable: 5.5.3 - httpxy: 0.1.7 - ioredis: 5.10.0 - jiti: 2.6.1 - klona: 2.0.6 - knitwork: 1.3.0 - listhen: 1.9.0 - magic-string: 0.30.21 - magicast: 0.5.2 - mime: 4.1.0 - mlly: 1.8.1 - node-fetch-native: 1.6.7 - node-mock-http: 1.0.4 - ofetch: 1.5.1 - ohash: 2.0.11 - pathe: 2.0.3 - perfect-debounce: 2.1.0 - pkg-types: 2.3.0 - pretty-bytes: 7.1.0 - radix3: 1.1.2 - rollup: 4.59.0 - rollup-plugin-visualizer: 6.0.11(rollup@4.59.0) - scule: 1.3.0 - semver: 7.7.4 - serve-placeholder: 2.0.2 - serve-static: 2.2.1 - source-map: 0.7.6 - std-env: 3.10.0 - ufo: 1.6.3 - ultrahtml: 1.6.0 - uncrypto: 0.1.3 - unctx: 2.5.0 - unenv: 2.0.0-rc.24 - unimport: 5.7.0 - unplugin-utils: 0.3.1 - unstorage: 1.17.4(db0@0.3.4)(ioredis@5.10.0) - untyped: 2.0.0 - unwasm: 0.5.3 - youch: 4.1.0 - youch-core: 0.3.3 - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@libsql/client' - - '@netlify/blobs' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - bare-abort-controller - - bare-buffer - - better-sqlite3 - - drizzle-orm - - encoding - - idb-keyval - - mysql2 - - react-native-b4a - - rolldown - - sqlite3 - - supports-color - - uploadthing - - node-addon-api@7.1.1: {} - - node-fetch-native@1.6.7: {} - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - - node-forge@1.3.3: {} - - node-gyp-build@4.8.4: {} - - node-mock-http@1.0.4: {} - - node-releases@2.0.27: {} - - nopt@8.1.0: - dependencies: - abbrev: 3.0.1 - - normalize-path@3.0.0: {} - - npm-run-path@5.3.0: - dependencies: - path-key: 4.0.0 - - nypm@0.6.5: - dependencies: - citty: 0.2.1 - pathe: 2.0.3 - tinyexec: 1.0.2 - - ofetch@1.5.1: - dependencies: - destr: 2.0.5 - node-fetch-native: 1.6.7 - ufo: 1.6.3 - - ohash@2.0.11: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - onetime@6.0.0: - dependencies: - mimic-fn: 4.0.0 - - oniguruma-to-es@2.3.0: - dependencies: - emoji-regex-xs: 1.0.0 - regex: 5.1.1 - regex-recursion: 5.1.1 - - open@8.4.2: - dependencies: - define-lazy-prop: 2.0.0 - is-docker: 2.2.1 - is-wsl: 2.2.0 - - package-json-from-dist@1.0.1: {} - - parse5@7.3.0: - dependencies: - entities: 6.0.1 - - parseurl@1.3.3: {} - - path-key@3.1.1: {} - - path-key@4.0.0: {} - - path-parse@1.0.7: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.3 - - path-scurry@2.0.2: - dependencies: - lru-cache: 11.2.6 - minipass: 7.1.3 - - path-to-regexp@8.3.0: {} - - pathe@1.1.2: {} - - pathe@2.0.3: {} - - perfect-debounce@2.1.0: {} - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.3: {} - - pkg-types@1.3.1: - dependencies: - confbox: 0.1.8 - mlly: 1.8.1 - pathe: 2.0.3 - - pkg-types@2.3.0: - dependencies: - confbox: 0.2.4 - exsolve: 1.0.8 - pathe: 2.0.3 - - postcss@8.5.6: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - pretty-bytes@7.1.0: {} - - process-nextick-args@2.0.1: {} - - process@0.11.10: {} - - property-information@7.1.0: {} - - quansync@0.2.11: {} - - queue-microtask@1.2.3: {} - - radix3@1.1.2: {} - - randombytes@2.1.0: - dependencies: - safe-buffer: 5.2.1 - - range-parser@1.2.1: {} - - rc9@2.1.2: - dependencies: - defu: 6.1.4 - destr: 2.0.5 - - readable-stream@2.3.8: - dependencies: - core-util-is: 1.0.3 - inherits: 2.0.4 - isarray: 1.0.0 - process-nextick-args: 2.0.1 - safe-buffer: 5.1.2 - string_decoder: 1.1.1 - util-deprecate: 1.0.2 - - readable-stream@4.7.0: - dependencies: - abort-controller: 3.0.0 - buffer: 6.0.3 - events: 3.3.0 - process: 0.11.10 - string_decoder: 1.3.0 - - readdir-glob@1.1.3: - dependencies: - minimatch: 5.1.9 - - readdirp@5.0.0: {} - - redis-errors@1.2.0: {} - - redis-parser@3.0.0: - dependencies: - redis-errors: 1.2.0 - - regex-recursion@5.1.1: - dependencies: - regex: 5.1.1 - regex-utilities: 2.3.0 - - regex-utilities@2.3.0: {} - - regex@5.1.1: - dependencies: - regex-utilities: 2.3.0 - - require-directory@2.1.1: {} - - resolve-from@5.0.0: {} - - resolve@1.22.11: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.1.0: {} - - rollup-plugin-visualizer@6.0.11(rollup@4.59.0): - dependencies: - open: 8.4.2 - picomatch: 4.0.3 - source-map: 0.7.6 - yargs: 17.7.2 - optionalDependencies: - rollup: 4.59.0 - - rollup@4.59.0: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 - fsevents: 2.3.3 - - rou3@0.7.12: {} - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - safe-buffer@5.1.2: {} - - safe-buffer@5.2.1: {} - - scule@1.3.0: {} - - semver@6.3.1: {} - - semver@7.7.4: {} - - send@1.2.1: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serialize-javascript@6.0.2: - dependencies: - randombytes: 2.1.0 - - seroval-plugins@1.5.0(seroval@1.5.0): - dependencies: - seroval: 1.5.0 - - seroval@1.5.0: {} - - serve-placeholder@2.0.2: - dependencies: - defu: 6.1.4 - - serve-static@2.2.1: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.1 - transitivePeerDependencies: - - supports-color - - setprototypeof@1.2.0: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - shiki@1.29.2: - dependencies: - '@shikijs/core': 1.29.2 - '@shikijs/engine-javascript': 1.29.2 - '@shikijs/engine-oniguruma': 1.29.2 - '@shikijs/langs': 1.29.2 - '@shikijs/themes': 1.29.2 - '@shikijs/types': 1.29.2 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.4 - - signal-exit@4.1.0: {} - - slash@5.1.0: {} - - smob@1.6.1: {} - - solid-js@1.9.11: - dependencies: - csstype: 3.2.3 - seroval: 1.5.0 - seroval-plugins: 1.5.0(seroval@1.5.0) - - solid-refresh@0.6.3(solid-js@1.9.11): - dependencies: - '@babel/generator': 7.29.1 - '@babel/helper-module-imports': 7.28.6 - '@babel/types': 7.29.0 - solid-js: 1.9.11 - transitivePeerDependencies: - - supports-color - - solid-use@0.9.1(solid-js@1.9.11): - dependencies: - solid-js: 1.9.11 - - source-map-js@1.2.1: {} - - source-map-support@0.5.21: - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - - source-map@0.6.1: {} - - source-map@0.7.6: {} - - space-separated-tokens@2.0.2: {} - - srvx@0.11.8: - optional: true - - srvx@0.9.8: {} - - stackframe@1.3.4: {} - - standard-as-callback@2.1.0: {} - - statuses@2.0.2: {} - - std-env@3.10.0: {} - - streamx@2.23.0: - dependencies: - events-universal: 1.0.1 - fast-fifo: 1.3.2 - text-decoder: 1.2.7 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.2.0 - - string_decoder@1.1.1: - dependencies: - safe-buffer: 5.1.2 - - string_decoder@1.3.0: - dependencies: - safe-buffer: 5.2.1 - - stringify-entities@4.0.4: - dependencies: - character-entities-html4: 2.1.0 - character-entities-legacy: 3.0.0 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-ansi@7.2.0: - dependencies: - ansi-regex: 6.2.2 - - strip-final-newline@3.0.0: {} - - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - - supports-color@10.2.2: {} - - supports-preserve-symlinks-flag@1.0.0: {} - - system-architecture@0.1.0: {} - - tagged-tag@1.0.0: {} - - tar-stream@3.1.8: - dependencies: - b4a: 1.8.0 - bare-fs: 4.5.5 - fast-fifo: 1.3.2 - streamx: 2.23.0 - transitivePeerDependencies: - - bare-abort-controller - - bare-buffer - - react-native-b4a - - tar@7.5.10: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.3 - minizlib: 3.1.0 - yallist: 5.0.0 - - teex@1.0.1: - dependencies: - streamx: 2.23.0 - transitivePeerDependencies: - - bare-abort-controller - - react-native-b4a - - terracotta@1.1.0(solid-js@1.9.11): - dependencies: - solid-js: 1.9.11 - solid-use: 0.9.1(solid-js@1.9.11) - - terser@5.46.0: - dependencies: - '@jridgewell/source-map': 0.3.11 - acorn: 8.16.0 - commander: 2.20.3 - source-map-support: 0.5.21 - - text-decoder@1.2.7: - dependencies: - b4a: 1.8.0 - transitivePeerDependencies: - - react-native-b4a - - tiny-invariant@1.3.3: {} - - tinyexec@1.0.2: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toidentifier@1.0.1: {} - - tr46@0.0.3: {} - - trim-lines@3.0.1: {} - - type-fest@5.4.4: - dependencies: - tagged-tag: 1.0.0 - - ufo@1.6.3: {} - - ultrahtml@1.6.0: {} - - uncrypto@0.1.3: {} - - unctx@2.5.0: - dependencies: - acorn: 8.16.0 - estree-walker: 3.0.3 - magic-string: 0.30.21 - unplugin: 2.3.11 - - unenv@2.0.0-rc.24: - dependencies: - pathe: 2.0.3 - - unicorn-magic@0.4.0: {} - - unimport@5.7.0: - dependencies: - acorn: 8.16.0 - escape-string-regexp: 5.0.0 - estree-walker: 3.0.3 - local-pkg: 1.1.2 - magic-string: 0.30.21 - mlly: 1.8.1 - pathe: 2.0.3 - picomatch: 4.0.3 - pkg-types: 2.3.0 - scule: 1.3.0 - strip-literal: 3.1.0 - tinyglobby: 0.2.15 - unplugin: 2.3.11 - unplugin-utils: 0.3.1 - - unist-util-is@6.0.1: - dependencies: - '@types/unist': 3.0.3 - - unist-util-position@5.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-stringify-position@4.0.0: - dependencies: - '@types/unist': 3.0.3 - - unist-util-visit-parents@6.0.2: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - - unist-util-visit@5.1.0: - dependencies: - '@types/unist': 3.0.3 - unist-util-is: 6.0.1 - unist-util-visit-parents: 6.0.2 - - unplugin-utils@0.3.1: - dependencies: - pathe: 2.0.3 - picomatch: 4.0.3 - - unplugin@2.3.11: - dependencies: - '@jridgewell/remapping': 2.3.5 - acorn: 8.16.0 - picomatch: 4.0.3 - webpack-virtual-modules: 0.6.2 - - unstorage@1.17.4(db0@0.3.4)(ioredis@5.10.0): - dependencies: - anymatch: 3.1.3 - chokidar: 5.0.0 - destr: 2.0.5 - h3: 1.15.5 - lru-cache: 11.2.6 - node-fetch-native: 1.6.7 - ofetch: 1.5.1 - ufo: 1.6.3 - optionalDependencies: - db0: 0.3.4 - ioredis: 5.10.0 - - untun@0.1.3: - dependencies: - citty: 0.1.6 - consola: 3.4.2 - pathe: 1.1.2 - - untyped@2.0.0: - dependencies: - citty: 0.1.6 - defu: 6.1.4 - jiti: 2.6.1 - knitwork: 1.3.0 - scule: 1.3.0 - - unwasm@0.5.3: - dependencies: - exsolve: 1.0.8 - knitwork: 1.3.0 - magic-string: 0.30.21 - mlly: 1.8.1 - pathe: 2.0.3 - pkg-types: 2.3.0 - - update-browserslist-db@1.2.3(browserslist@4.28.1): - dependencies: - browserslist: 4.28.1 - escalade: 3.2.0 - picocolors: 1.1.1 - - uqr@0.1.2: {} - - util-deprecate@1.0.2: {} - - valibot@0.29.0: {} - - vfile-message@4.0.3: - dependencies: - '@types/unist': 3.0.3 - unist-util-stringify-position: 4.0.0 - - vfile@6.0.3: - dependencies: - '@types/unist': 3.0.3 - vfile-message: 4.0.3 - - vite-plugin-solid@2.11.10(solid-js@1.9.11)(vite@7.3.1(jiti@2.6.1)(terser@5.46.0)): - dependencies: - '@babel/core': 7.29.0 - '@types/babel__core': 7.20.5 - babel-preset-solid: 1.9.10(@babel/core@7.29.0)(solid-js@1.9.11) - merge-anything: 5.1.7 - solid-js: 1.9.11 - solid-refresh: 0.6.3(solid-js@1.9.11) - vite: 7.3.1(jiti@2.6.1)(terser@5.46.0) - vitefu: 1.1.2(vite@7.3.1(jiti@2.6.1)(terser@5.46.0)) - transitivePeerDependencies: - - supports-color - - vite@7.3.1(jiti@2.6.1)(terser@5.46.0): - dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.59.0 - tinyglobby: 0.2.15 - optionalDependencies: - fsevents: 2.3.3 - jiti: 2.6.1 - terser: 5.46.0 - - vitefu@1.1.2(vite@7.3.1(jiti@2.6.1)(terser@5.46.0)): - optionalDependencies: - vite: 7.3.1(jiti@2.6.1)(terser@5.46.0) - - webidl-conversions@3.0.1: {} - - webpack-virtual-modules@0.6.2: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.3 - string-width: 5.1.2 - strip-ansi: 7.2.0 - - y18n@5.0.8: {} - - yallist@3.1.1: {} - - yallist@5.0.0: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1 - - youch-core@0.3.3: - dependencies: - '@poppinss/exception': 1.2.3 - error-stack-parser-es: 1.0.5 - - youch@4.1.0: - dependencies: - '@poppinss/colors': 4.1.6 - '@poppinss/dumper': 0.7.0 - '@speed-highlight/core': 1.2.14 - cookie-es: 2.0.0 - youch-core: 0.3.3 - - zip-stream@6.0.1: - dependencies: - archiver-utils: 5.0.2 - compress-commons: 6.0.2 - readable-stream: 4.7.0 - - zwitch@2.0.4: {}