diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d45c2eb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: CI + +on: + push: + pull_request: + +permissions: + contents: read + +jobs: + crawler: + name: Crawler (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.11'] + defaults: + run: + working-directory: crawler + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: crawler/requirements.txt + - run: python -m pip install --upgrade pip + - run: python -m pip install -r requirements.txt + - run: python -m unittest discover -s tests -p 'test_*.py' -v + env: + PYTHONDONTWRITEBYTECODE: '1' + PYTHONIOENCODING: utf-8 + + api: + name: API (Python 3.11) + runs-on: ubuntu-latest + defaults: + run: + working-directory: api_server + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: pip + cache-dependency-path: api_server/requirements.txt + - run: python -m pip install --upgrade pip + - run: python -m pip install -r requirements.txt + - run: python -m unittest discover -s tests -p 'test_*.py' -v + env: + DEBUG: 'false' + PYTHONDONTWRITEBYTECODE: '1' + PYTHONIOENCODING: utf-8 + SUPABASE_KEY: test-key + SUPABASE_URL: http://localhost:54321 + + database: + name: Database migration + runs-on: ubuntu-latest + services: + postgres: + image: postgres:15 + env: + POSTGRES_PASSWORD: postgres + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v4 + - name: Run migration contract test + working-directory: supabase/tests + env: + PGPASSWORD: postgres + run: psql -h localhost -U postgres -d postgres -f stats_write_consistency_test.sql + + app: + name: App web build (Node 20) + runs-on: ubuntu-latest + defaults: + run: + working-directory: app + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: app/package-lock.json + - run: npm ci + - run: npm run test:ci + env: + CI: '1' diff --git a/.gitignore b/.gitignore index fe0ce4e..2e9d6f6 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ __pycache__/ .pytest_cache/ .venv/ venv/ +venv_win/ env/ # Database & Credentials @@ -41,4 +42,3 @@ scratch/ # Supabase CLI binaries supabase.exe supabase-go.exe - diff --git a/api_server/app/core/stats_storage.py b/api_server/app/core/stats_storage.py new file mode 100644 index 0000000..7db5f7c --- /dev/null +++ b/api_server/app/core/stats_storage.py @@ -0,0 +1,34 @@ +"""Shared database write helpers for derived generation statistics.""" + +from supabase import Client + + +def upsert_daily_stats( + db: Client, + records: list[dict], + *, + source: str, + allow_decrease: bool = False, +) -> list[dict]: + """Apply the database-owned daily conflict and monthly sync policy.""" + if not records: + return [] + + payload = [ + { + "plant_id": record["plant_id"], + "date": record["date"], + "total_generation": float(record["total_generation"]), + "peak_kw": float(record.get("peak_kw") or 0), + } + for record in records + ] + response = db.rpc( + "upsert_daily_stats", + { + "p_records": payload, + "p_source": source, + "p_allow_decrease": allow_decrease, + }, + ).execute() + return response.data or [] diff --git a/api_server/app/core/time_utils.py b/api_server/app/core/time_utils.py new file mode 100644 index 0000000..5418686 --- /dev/null +++ b/api_server/app/core/time_utils.py @@ -0,0 +1,32 @@ +"""API에서 사용하는 KST 기준 시간 및 UTC 조회 경계.""" + +from datetime import date, datetime, time, timedelta, timezone +from typing import Tuple + + +KST = timezone(timedelta(hours=9), name="KST") +UTC = timezone.utc + + +def now_kst() -> datetime: + """서버 OS 시간대와 무관한 현재 KST 시각을 반환한다.""" + return datetime.now(KST) + + +def today_kst() -> date: + return now_kst().date() + + +def kst_day_bounds_utc(target_date: date) -> Tuple[datetime, datetime]: + """KST 하루의 UTC 반개구간 [start, next_start)을 반환한다.""" + start_kst = datetime.combine(target_date, time.min, tzinfo=KST) + next_start_kst = start_kst + timedelta(days=1) + return start_kst.astimezone(UTC), next_start_kst.astimezone(UTC) + + +def parse_db_timestamp_kst(value: str) -> datetime: + """Supabase timestamptz 문자열을 KST aware datetime으로 변환한다.""" + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=UTC) + return parsed.astimezone(KST) diff --git a/api_server/app/main.py b/api_server/app/main.py index 3999c36..1696760 100644 --- a/api_server/app/main.py +++ b/api_server/app/main.py @@ -3,11 +3,14 @@ FastAPI 애플리케이션 진입점 - 태양광 발전 관제 시스템 미들웨어 서버 """ -from fastapi import FastAPI +from fastapi import Depends, FastAPI, HTTPException from fastapi.middleware.cors import CORSMiddleware +from supabase import Client from app.core.config import get_settings +from app.core.database import get_db from app.routers import plants, upload, stats +from app.schemas.health import DetailedHealthResponse, HealthResponse # 설정 로드 settings = get_settings() @@ -36,35 +39,45 @@ app.include_router(upload.router) # /plants/{plant_id}/upload app.include_router(plants.router) # /plants/{company_id} (가장 일반적인 경로) -@app.get("/", tags=["Health"]) -async def health_check() -> dict: +@app.get("/", tags=["Health"], response_model=HealthResponse) +def health_check() -> HealthResponse: """ 서버 상태 확인 (Health Check) Returns: 서버 상태 및 버전 정보 """ - return { - "status": "healthy", - "app_name": settings.APP_NAME, - "version": settings.APP_VERSION - } + return HealthResponse( + status="healthy", + app_name=settings.APP_NAME, + version=settings.APP_VERSION, + ) -@app.get("/health", tags=["Health"]) -async def detailed_health_check() -> dict: +@app.get("/health", tags=["Health"], response_model=DetailedHealthResponse) +def detailed_health_check( + db: Client = Depends(get_db), +) -> DetailedHealthResponse: """ 상세 서버 상태 확인 Returns: 서버 상태 및 연결 정보 """ - return { - "status": "healthy", - "app_name": settings.APP_NAME, - "version": settings.APP_VERSION, - "supabase_connected": bool(settings.SUPABASE_URL) - } + try: + db.table("plants").select("id").limit(1).execute() + except Exception as exc: + raise HTTPException( + status_code=503, + detail="데이터베이스 연결 상태를 확인할 수 없습니다.", + ) from exc + + return DetailedHealthResponse( + status="healthy", + app_name=settings.APP_NAME, + version=settings.APP_VERSION, + supabase_connected=True, + ) if __name__ == "__main__": diff --git a/api_server/app/routers/plants.py b/api_server/app/routers/plants.py index eb1569e..9078132 100644 --- a/api_server/app/routers/plants.py +++ b/api_server/app/routers/plants.py @@ -9,7 +9,14 @@ from supabase import Client from typing import List from app.core.database import get_db -from app.schemas.plant import PlantsListResponse, PlantWithLatestLog, SolarLogBase, PlantAlertUpdateRequest +from app.schemas.plant import ( + PlantAlertUpdateRequest, + PlantAlertUpdateResponse, + PlantDetailResponse, + PlantsListResponse, + PlantWithLatestLog, + SolarLogBase, +) router = APIRouter( prefix="/plants", @@ -18,7 +25,7 @@ router = APIRouter( @router.get("/{company_id}", response_model=PlantsListResponse) -async def get_plants_by_company( +def get_plants_by_company( company_id: int, db: Client = Depends(get_db) ) -> PlantsListResponse: @@ -32,10 +39,16 @@ async def get_plants_by_company( 발전소 목록 및 각 발전소의 최신 발전 로그 """ try: - # 1. 해당 업체의 모든 발전소 조회 + # 발전소와 각 발전소의 최신 로그 1건을 관계 중첩 조회로 한 번에 가져온다. plants_response = db.table("plants") \ - .select("*") \ + .select( + "*,solar_logs(" + "id,plant_id,current_kw,today_kwh,status,created_at" + ")" + ) \ .eq("company_id", company_id) \ + .order("created_at", desc=True, foreign_table="solar_logs") \ + .limit(1, foreign_table="solar_logs") \ .execute() plants = plants_response.data @@ -47,21 +60,14 @@ async def get_plants_by_company( total_count=0 ) - # 2. 각 발전소의 최신 발전 로그 조회 result: List[PlantWithLatestLog] = [] - for plant in plants: - # 해당 발전소의 최신 로그 1건 조회 - log_response = db.table("solar_logs") \ - .select("*") \ - .eq("plant_id", plant["id"]) \ - .order("created_at", desc=True) \ - .limit(1) \ - .execute() - + for plant_row in plants: + plant = dict(plant_row) + nested_logs = plant.pop("solar_logs", None) or [] latest_log = None - if log_response.data: - latest_log = SolarLogBase(**log_response.data[0]) + if nested_logs: + latest_log = SolarLogBase(**nested_logs[0]) plant_with_log = PlantWithLatestLog( **plant, @@ -82,10 +88,10 @@ async def get_plants_by_company( ) -@router.get("/{company_id}/{plant_id}", response_model=dict) -async def get_plant_detail( +@router.get("/{company_id}/{plant_id}", response_model=PlantDetailResponse) +def get_plant_detail( company_id: int, - plant_id: int, + plant_id: str, db: Client = Depends(get_db) ) -> dict: """ @@ -104,7 +110,7 @@ async def get_plant_detail( .select("*") \ .eq("id", plant_id) \ .eq("company_id", company_id) \ - .single() \ + .limit(1) \ .execute() if not plant_response.data: @@ -113,7 +119,7 @@ async def get_plant_detail( detail="발전소를 찾을 수 없습니다." ) - plant = plant_response.data + plant = plant_response.data[0] # 최근 발전 로그 10건 조회 logs_response = db.table("solar_logs") \ @@ -140,13 +146,16 @@ async def get_plant_detail( ) -@router.patch("/{company_id}/{plant_id}/alerts") -async def update_plant_alerts( +@router.patch( + "/{company_id}/{plant_id}/alerts", + response_model=PlantAlertUpdateResponse, +) +def update_plant_alerts( company_id: int, plant_id: str, alert_update: PlantAlertUpdateRequest, db: Client = Depends(get_db) -) -> dict: +) -> PlantAlertUpdateResponse: """ 특정 발전소의 알림 활성화 상태를 변경합니다. """ @@ -163,11 +172,11 @@ async def update_plant_alerts( detail="발전소를 찾을 수 없거나 업데이트에 실패했습니다." ) - return { - "status": "success", - "message": "알림 설정이 변경되었습니다.", - "data": response.data[0] - } + return PlantAlertUpdateResponse( + status="success", + message="알림 설정이 변경되었습니다.", + data=response.data[0], + ) except HTTPException: raise except Exception as e: diff --git a/api_server/app/routers/stats.py b/api_server/app/routers/stats.py index 39dc3e5..ead1cae 100644 --- a/api_server/app/routers/stats.py +++ b/api_server/app/routers/stats.py @@ -5,31 +5,76 @@ from fastapi import APIRouter, HTTPException, Depends, Query from supabase import Client -from typing import List, Literal, Optional -from datetime import datetime, timedelta, timezone +from typing import Literal, Optional +from datetime import date as date_type import calendar import re from app.core.database import get_db +from app.core.time_utils import ( + kst_day_bounds_utc, + parse_db_timestamp_kst, + today_kst, +) +from app.schemas.stats import ( + ComparisonStatsResponse, + HourlyStatsResponse, + PlantStatsResponse, +) router = APIRouter( prefix="/plants", tags=["Stats"] ) +MIN_STATS_YEAR = 2000 +MAX_STATS_YEAR = 2100 -@router.get("/stats/comparison") -async def get_all_plants_comparison( + +def parse_iso_date(value: str) -> date_type: + """YYYY-MM-DD만 허용하고 잘못된 날짜를 명확한 400으로 반환한다.""" + if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", value): + raise HTTPException( + status_code=400, + detail="날짜 형식은 YYYY-MM-DD여야 합니다.", + ) + try: + return date_type.fromisoformat(value) + except ValueError as exc: + raise HTTPException( + status_code=400, + detail="유효하지 않은 날짜입니다.", + ) from exc + + +def ensure_plant_exists(db: Client, plant_id: str) -> None: + response = db.table("plants") \ + .select("id") \ + .eq("id", plant_id) \ + .limit(1) \ + .execute() + if not response.data: + raise HTTPException(status_code=404, detail="발전소를 찾을 수 없습니다.") + + +@router.get("/stats/comparison", response_model=ComparisonStatsResponse) +def get_all_plants_comparison( period: Literal["day", "month", "year"] = Query("day", description="통계 기간"), date: Optional[str] = Query(None, description="날짜 (YYYY-MM-DD)"), - year: Optional[int] = Query(None, description="연도"), - month: Optional[int] = Query(None, description="월"), + year: Optional[int] = Query(None, ge=MIN_STATS_YEAR, le=MAX_STATS_YEAR, description="연도"), + month: Optional[int] = Query(None, ge=1, le=12, description="월"), db: Client = Depends(get_db) -) -> dict: +) -> ComparisonStatsResponse: """ 전체 발전소 발전량 비교 통계 조회 """ try: + # 날짜 파라미터 처리 (서버 OS 시간대와 무관하게 KST 기준) + today = today_kst() + target_date = parse_iso_date(date) if date else today + target_year = year if year is not None else target_date.year + target_month = month if month is not None else target_date.month + # 1. 모든 발전소 기본 정보 조회 (이름, 용량) plants_res = db.table("plants").select("id, name, capacity").execute() plants = {p['id']: p for p in plants_res.data} @@ -37,24 +82,6 @@ async def get_all_plants_comparison( # 결과 초기화 result_data = [] - # 날짜 파라미터 처리 - # KST 시간대 고려 - kst_timezone = timezone(timedelta(hours=9)) - now_kst = datetime.now(kst_timezone) - today = now_kst.date() - - target_date = None - if date: - try: - target_date = datetime.strptime(date, "%Y-%m-%d").date() - except ValueError: - target_date = today - else: - target_date = today - - target_year = year if year else target_date.year - target_month = month if month else target_date.month - # 데이터 조회 로직 stats_map = {} # plant_id -> generation @@ -64,13 +91,13 @@ async def get_all_plants_comparison( # (A) 오늘 날짜인 경우: solar_logs 최신값 (실시간) # 서버 시간대와 클라이언트 요청 날짜 일치 여부 확인 if target_date == today: - # 오늘 00:00:00 (KST) 이후 데이터 조회 - start_dt = f"{date_str}T00:00:00" + start_utc, next_start_utc = kst_day_bounds_utc(target_date) # solar_logs에서 오늘 생성된 데이터 조회 logs_res = db.table("solar_logs") \ .select("plant_id, today_kwh") \ - .gte("created_at", start_dt) \ + .gte("created_at", start_utc.isoformat()) \ + .lt("created_at", next_start_utc.isoformat()) \ .order("created_at", desc=True) \ .execute() @@ -170,8 +197,9 @@ async def get_all_plants_comparison( days_in_month = calendar.monthrange(target_year, target_month)[1] gen_hours = (gen / cap) / days_in_month elif period == "year": - # 연간 일평균 발전시간 (/365) - gen_hours = (gen / cap) / 365 + # 윤년을 포함한 연간 일평균 발전시간 + days_in_year = 366 if calendar.isleap(target_year) else 365 + gen_hours = (gen / cap) / days_in_year result_data.append({ "plant_id": pid, @@ -190,14 +218,16 @@ async def get_all_plants_comparison( result_data.sort(key=sort_key) - return { + return ComparisonStatsResponse(**{ "status": "success", "period": period, "target_date": target_date.isoformat(), "data": result_data, "count": len(result_data) - } + }) + except HTTPException: + raise except Exception as e: raise HTTPException( status_code=500, @@ -205,20 +235,21 @@ async def get_all_plants_comparison( ) -@router.get("/{plant_id}/stats") -async def get_plant_stats( +@router.get("/{plant_id}/stats", response_model=PlantStatsResponse) +def get_plant_stats( plant_id: str, period: Literal["day", "month", "year"] = Query("day", description="통계 기간"), - year: int = Query(None, description="특정 연도"), - month: int = Query(None, description="특정 월 (period='day' 시 필수)"), + year: Optional[int] = Query(None, ge=MIN_STATS_YEAR, le=MAX_STATS_YEAR, description="특정 연도"), + month: Optional[int] = Query(None, ge=1, le=12, description="특정 월"), db: Client = Depends(get_db) -) -> dict: +) -> PlantStatsResponse: """ 발전소 통계 조회 (Hybrid 방식) """ try: - today = datetime.now().date() + today = today_kst() today_str = today.isoformat() + ensure_plant_exists(db, plant_id) # 1. 과거 데이터 조회 (period에 따라 테이블 분기) stats_data_raw = [] @@ -259,10 +290,12 @@ async def get_plant_stats( # year 파라미터가 있으면 해당 연도 포함 최근 5년치 조회 if year: start_year = year - 4 + end_year = year else: start_year = today.year - 9 # 10년치 + end_year = today.year start_month = f"{start_year}-01" - end_month = f"{today.year}-12" + end_month = f"{end_year}-12" stats_query = db.table("monthly_stats") \ .select("month, total_generation") \ @@ -286,10 +319,12 @@ async def get_plant_stats( if period == "day": # 조회 중인 달이 이번 달인지 확인 if (not year or year == today.year) and (not month or month == today.month): + start_utc, next_start_utc = kst_day_bounds_utc(today) logs_result = db.table("solar_logs") \ .select("today_kwh") \ .eq("plant_id", plant_id) \ - .gte("created_at", f"{today_str}T00:00:00") \ + .gte("created_at", start_utc.isoformat()) \ + .lt("created_at", next_start_utc.isoformat()) \ .order("created_at", desc=True) \ .limit(1) \ .execute() @@ -363,13 +398,13 @@ async def get_plant_stats( "value": round(yearly.get(y_str, 0), 2) }) - return { + return PlantStatsResponse(**{ "status": "success", "plant_id": plant_id, "period": period, "data": data, "count": len(data) - } + }) except HTTPException: raise @@ -380,45 +415,33 @@ async def get_plant_stats( ) -@router.get("/{plant_id}/stats/today") -async def get_plant_hourly_stats( +@router.get("/{plant_id}/stats/today", response_model=HourlyStatsResponse) +def get_plant_hourly_stats( plant_id: str, date: str = Query(None, description="조회 날짜 (YYYY-MM-DD)"), db: Client = Depends(get_db) -) -> dict: +) -> HourlyStatsResponse: """ 특정 날짜의 시간별 발전 데이터 조회 (solar_logs 기반) Defaults to today if date is not provided. """ try: - # KST (UTC+9) 시간대 설정 - kst_timezone = timezone(timedelta(hours=9)) - if date: - try: - target_date = datetime.strptime(date, "%Y-%m-%d").date() - except ValueError: - raise HTTPException(status_code=400, detail="Invalid date format. Use YYYY-MM-DD") + target_date = parse_iso_date(date) else: - target_date = datetime.now(kst_timezone).date() - + target_date = today_kst() + + ensure_plant_exists(db, plant_id) target_date_str = target_date.isoformat() - # 조회 범위: 해당 일 00:00:00 ~ 23:59:59 (KST) - from_dt = datetime.combine(target_date, datetime.min.time()).replace(tzinfo=kst_timezone) - to_dt = datetime.combine(target_date, datetime.max.time()).replace(tzinfo=kst_timezone) - - # 전조치: UTC로 변환하여 쿼리 (Supabase DB가 UTC라고 가정) - # 하지만 solar_logs는 created_at이 timestamptz로 저장되어 있을 것임. - # 안전하게는 UTC 시간으로 필터링 - from_utc = from_dt.astimezone(timezone.utc) - to_utc = to_dt.astimezone(timezone.utc) + # 조회 범위: KST 하루를 UTC 반개구간으로 변환한다. + from_utc, next_start_utc = kst_day_bounds_utc(target_date) logs_result = db.table("solar_logs") \ .select("current_kw, today_kwh, created_at") \ .eq("plant_id", plant_id) \ .gte("created_at", from_utc.isoformat()) \ - .lte("created_at", to_utc.isoformat()) \ + .lt("created_at", next_start_utc.isoformat()) \ .order("created_at", desc=False) \ .execute() @@ -428,8 +451,7 @@ async def get_plant_hourly_stats( created_at = log.get("created_at", "") if created_at: try: - dt = datetime.fromisoformat(created_at.replace('Z', '+00:00')) - dt_kst = dt.astimezone(kst_timezone) + dt_kst = parse_db_timestamp_kst(created_at) if dt_kst.date() != target_date: continue @@ -439,7 +461,7 @@ async def get_plant_hourly_stats( "current_kw": log.get("current_kw", 0) or 0, "today_kwh": log.get("today_kwh", 0) or 0, } - except ValueError: + except (TypeError, ValueError): continue result = [] @@ -453,14 +475,16 @@ async def get_plant_hourly_stats( "has_data": hour in hourly_data }) - return { + return HourlyStatsResponse(**{ "status": "success", "plant_id": plant_id, "date": target_date_str, "data": result, "count": len([d for d in result if d["has_data"]]) - } + }) + except HTTPException: + raise except Exception as e: raise HTTPException( status_code=500, diff --git a/api_server/app/routers/upload.py b/api_server/app/routers/upload.py index ea569c2..34bfdca 100644 --- a/api_server/app/routers/upload.py +++ b/api_server/app/routers/upload.py @@ -3,20 +3,63 @@ - 과거 발전 데이터(Excel)를 업로드하여 daily_stats 테이블에 저장 """ +import calendar import io from fastapi import APIRouter, UploadFile, File, Form, HTTPException, Depends +from fastapi.concurrency import run_in_threadpool from supabase import Client import pandas as pd from app.core.database import get_db +from app.core.stats_storage import upsert_daily_stats +from app.core.time_utils import now_kst +from app.schemas.upload import UploadResponse router = APIRouter( tags=["Upload"] ) +ALLOWED_EXCEL_EXTENSIONS = ('.xlsx', '.xls') +MAX_UPLOAD_BYTES = 5 * 1024 * 1024 +MAX_UPLOAD_ROWS = 5000 -@router.post("/upload/history") + +async def read_excel_upload(file: UploadFile) -> pd.DataFrame: + """업로드 크기와 형식을 검증한 뒤 Excel DataFrame을 반환한다.""" + filename = (file.filename or '').strip() + if not filename.lower().endswith(ALLOWED_EXCEL_EXTENSIONS): + raise HTTPException( + status_code=400, + detail="엑셀 파일(.xlsx, .xls)만 업로드 가능합니다." + ) + + contents = await file.read(MAX_UPLOAD_BYTES + 1) + if not contents: + raise HTTPException(status_code=400, detail="업로드 파일이 비어있습니다.") + if len(contents) > MAX_UPLOAD_BYTES: + raise HTTPException( + status_code=413, + detail=f"파일 크기는 {MAX_UPLOAD_BYTES // (1024 * 1024)}MB 이하여야 합니다." + ) + + try: + df = await run_in_threadpool(pd.read_excel, io.BytesIO(contents)) + except Exception as e: + raise HTTPException(status_code=400, detail=f"엑셀 파일 읽기 실패: {str(e)}") from e + + if df.empty: + raise HTTPException(status_code=400, detail="엑셀 파일에 데이터가 없습니다.") + if len(df.index) > MAX_UPLOAD_ROWS: + raise HTTPException( + status_code=400, + detail=f"한 번에 최대 {MAX_UPLOAD_ROWS}행까지 업로드할 수 있습니다." + ) + + return df + + +@router.post("/upload/history", response_model=UploadResponse) async def upload_history( file: UploadFile = File(..., description="엑셀 파일 (.xlsx, .xls)"), plant_id: str = Form(..., description="발전소 ID (예: nrems-01)"), @@ -36,20 +79,15 @@ async def upload_history( Returns: 저장 결과 메시지 """ - # 1. 파일 확장자 검증 - if not file.filename.endswith(('.xlsx', '.xls')): - raise HTTPException( - status_code=400, - detail="엑셀 파일(.xlsx, .xls)만 업로드 가능합니다." - ) - # 2. 발전소 정보 조회 (capacity 필요) try: - plant_response = db.table("plants") \ - .select("id, capacity") \ - .eq("id", plant_id) \ - .single() \ + plant_response = await run_in_threadpool( + lambda: db.table("plants") + .select("id, capacity") + .eq("id", plant_id) + .limit(1) .execute() + ) if not plant_response.data: raise HTTPException( @@ -57,7 +95,7 @@ async def upload_history( detail=f"발전소 '{plant_id}'를 찾을 수 없습니다." ) - capacity = plant_response.data.get('capacity', 99.0) + capacity = plant_response.data[0].get('capacity', 99.0) if capacity <= 0: capacity = 99.0 # 기본값 @@ -71,8 +109,7 @@ async def upload_history( # 3. 엑셀 파일 읽기 try: - contents = await file.read() - df = pd.read_excel(io.BytesIO(contents)) + df = await read_excel_upload(file) # 컬럼 확인 required_columns = ['date', 'generation'] @@ -83,13 +120,6 @@ async def upload_history( detail=f"필수 컬럼이 없습니다: {missing_columns}. 엑셀에는 'date', 'generation' 컬럼이 필요합니다." ) - # 빈 데이터 체크 - if df.empty: - raise HTTPException( - status_code=400, - detail="엑셀 파일에 데이터가 없습니다." - ) - except HTTPException: raise except Exception as e: @@ -110,13 +140,14 @@ async def upload_history( errors.append(f"행 {idx+2}: 날짜가 비어있습니다.") continue - if isinstance(date_val, str): - date_str = date_val.strip() - else: - date_str = pd.to_datetime(date_val).strftime('%Y-%m-%d') + date_str = pd.to_datetime(date_val, errors='raise').strftime('%Y-%m-%d') # 발전량 - generation = float(row['generation']) if pd.notna(row['generation']) else 0.0 + if pd.isna(row['generation']): + raise ValueError("발전량이 비어있습니다.") + generation = float(row['generation']) + if generation < 0: + raise ValueError("발전량은 0 이상이어야 합니다.") # generation_hours 계산 generation_hours = round(generation / capacity, 2) if capacity > 0 else 0.0 @@ -141,10 +172,14 @@ async def upload_history( # 5. DB Upsert try: - result = db.table("daily_stats").upsert( - records, - on_conflict="plant_id,date" - ).execute() + await run_in_threadpool( + lambda: upsert_daily_stats( + db, + records, + source="excel_daily", + allow_decrease=True, + ) + ) response_msg = f"총 {len(records)}건의 데이터가 저장되었습니다." if errors: @@ -165,7 +200,7 @@ async def upload_history( ) -@router.post("/plants/{plant_id}/upload") +@router.post("/plants/{plant_id}/upload", response_model=UploadResponse) async def upload_plant_data( plant_id: str, file: UploadFile = File(..., description="엑셀 파일 (.xlsx, .xls)"), @@ -185,20 +220,15 @@ async def upload_plant_data( Returns: 저장 결과 메시지 """ - # 1. 파일 확장자 검증 - if not file.filename.endswith(('.xlsx', '.xls')): - raise HTTPException( - status_code=400, - detail="엑셀 파일(.xlsx, .xls)만 업로드 가능합니다." - ) - # 2. 발전소 정보 조회 try: - plant_response = db.table("plants") \ - .select("id, capacity, name") \ - .eq("id", plant_id) \ - .single() \ + plant_response = await run_in_threadpool( + lambda: db.table("plants") + .select("id, capacity, name") + .eq("id", plant_id) + .limit(1) .execute() + ) if not plant_response.data: raise HTTPException( @@ -206,8 +236,8 @@ async def upload_plant_data( detail=f"발전소 '{plant_id}'를 찾을 수 없습니다." ) - capacity = plant_response.data.get('capacity', 99.0) or 99.0 - plant_name = plant_response.data.get('name', plant_id) + capacity = plant_response.data[0].get('capacity', 99.0) or 99.0 + plant_name = plant_response.data[0].get('name', plant_id) except HTTPException: raise @@ -219,8 +249,7 @@ async def upload_plant_data( # 3. 엑셀 파일 읽기 try: - contents = await file.read() - df = pd.read_excel(io.BytesIO(contents)) + df = await read_excel_upload(file) # 컬럼명 소문자 변환 df.columns = [str(c).lower().strip() for c in df.columns] @@ -258,8 +287,8 @@ async def upload_plant_data( df.rename(columns=rename_dict, inplace=True) # 병합된 셀 처리 (ffill) - df['year'] = df['year'].fillna(method='ffill') - df['month'] = df['month'].fillna(method='ffill') + df['year'] = df['year'].ffill() + df['month'] = df['month'].ffill() # 날짜 생성 함수 def make_date(row): @@ -268,7 +297,7 @@ async def upload_plant_data( m = int(float(str(row['month']).replace('월','').strip())) d = int(float(str(row['day']).replace('일','').strip())) return f"{y:04d}-{m:02d}-{d:02d}" - except: + except (TypeError, ValueError): return None df['date'] = df.apply(make_date, axis=1) @@ -286,12 +315,6 @@ async def upload_plant_data( detail="날짜 정보를 파싱할 수 없습니다." ) - if df.empty: - raise HTTPException( - status_code=400, - detail="엑셀 파일에 데이터가 없습니다." - ) - except HTTPException: raise except Exception as e: @@ -311,7 +334,11 @@ async def upload_plant_data( continue date_str = pd.to_datetime(date_val).strftime('%Y-%m-%d') - generation = float(row['generation']) if pd.notna(row['generation']) else 0.0 + if pd.isna(row['generation']): + raise ValueError("발전량이 비어있습니다.") + generation = float(row['generation']) + if generation < 0: + raise ValueError("발전량은 0 이상이어야 합니다.") generation_hours = round(generation / capacity, 2) if capacity > 0 else 0.0 records.append({ @@ -333,10 +360,14 @@ async def upload_plant_data( # 5. DB Upsert try: - db.table("daily_stats").upsert( - records, - on_conflict="plant_id,date" - ).execute() + await run_in_threadpool( + lambda: upsert_daily_stats( + db, + records, + source="excel_daily", + allow_decrease=True, + ) + ) return { "status": "success", @@ -353,7 +384,7 @@ async def upload_plant_data( detail=f"DB 저장 실패: {str(e)}" ) -@router.post("/plants/{plant_id}/upload/monthly") +@router.post("/plants/{plant_id}/upload/monthly", response_model=UploadResponse) async def upload_plant_monthly_data( plant_id: str, file: UploadFile = File(..., description="월간 발전량 엑셀 파일 (year, month, kwh)"), @@ -373,30 +404,25 @@ async def upload_plant_monthly_data( - 'month', 'kwh'의 특수문자(월, 콤마 등) 제거 및 숫자 변환 - monthly_stats 테이블에 저장 """ - # 1. 파일 확장자 검증 - if not file.filename.endswith(('.xlsx', '.xls')): - raise HTTPException( - status_code=400, - detail="엑셀 파일(.xlsx, .xls)만 업로드 가능합니다." - ) - # 2. 발전소 존재 확인 try: - plant_check = db.table("plants").select("id").eq("id", plant_id).single().execute() + plant_check = await run_in_threadpool( + lambda: db.table("plants") + .select("id") + .eq("id", plant_id) + .limit(1) + .execute() + ) if not plant_check.data: raise HTTPException(status_code=404, detail="발전소를 찾을 수 없습니다.") + except HTTPException: + raise except Exception as e: raise HTTPException(status_code=500, detail=f"발전소 확인 실패: {e}") # 3. 엑셀 파싱 및 전처리 try: - contents = await file.read() - # engine='openpyxl' 명시 (xlsx) - try: - df = pd.read_excel(io.BytesIO(contents), engine='openpyxl') - except: - # xls fallback - df = pd.read_excel(io.BytesIO(contents)) + df = await read_excel_upload(file) # 컬럼명 정규화 (모두 소문자, 앞뒤 공백 제거) df.columns = [str(col).lower().strip() for col in df.columns] @@ -432,13 +458,11 @@ async def upload_plant_monthly_data( ) # A열(year) 병합된 셀 처리 (Forward Fill) - df['year'] = df['year'].fillna(method='ffill') + df['year'] = df['year'].ffill() records = [] errors = [] - from datetime import datetime - for idx, row in df.iterrows(): try: # 1. Year 파싱 @@ -449,7 +473,7 @@ async def upload_plant_monthly_data( try: year_val = int(float(y_raw)) - except: + except (TypeError, ValueError): continue # 2. Month 파싱 @@ -471,12 +495,17 @@ async def upload_plant_monthly_data( # 3. Kwh 파싱 k_raw = str(row['kwh']).replace(',', '').strip() if not k_raw or k_raw.lower() == 'nan': - kwh_val = 0.0 + errors.append(f"Row {idx+2}: 발전량이 비어있습니다.") + continue else: try: kwh_val = float(k_raw) - except: - kwh_val = 0.0 + except (TypeError, ValueError): + errors.append(f"Row {idx+2}: 발전량 형식이 올바르지 않습니다.") + continue + if kwh_val < 0: + errors.append(f"Row {idx+2}: 발전량은 0 이상이어야 합니다.") + continue # 포맷: YYYY-MM month_key = f"{year_val}-{month_val:02d}" @@ -485,7 +514,9 @@ async def upload_plant_monthly_data( "plant_id": plant_id, "month": month_key, "total_generation": kwh_val, - "updated_at": datetime.now().isoformat() + "last_date": f"{month_key}-{calendar.monthrange(year_val, month_val)[1]:02d}", + "updated_at": now_kst().isoformat(), + "source": "excel_monthly", }) except Exception as e: @@ -500,10 +531,12 @@ async def upload_plant_monthly_data( # 4. DB Upsert # monthly_stats 테이블 생성 여부 확인이 필요하지만, 이미 되어있다고 가정 - res = db.table("monthly_stats").upsert( - records, - on_conflict="plant_id, month" - ).execute() + await run_in_threadpool( + lambda: db.table("monthly_stats").upsert( + records, + on_conflict="plant_id, month" + ).execute() + ) return { "status": "success", diff --git a/api_server/app/schemas/__init__.py b/api_server/app/schemas/__init__.py index 3102422..4e7974d 100644 --- a/api_server/app/schemas/__init__.py +++ b/api_server/app/schemas/__init__.py @@ -1,2 +1,13 @@ # Pydantic Schemas module -from .plant import PlantBase, PlantResponse, PlantWithLatestLog, PlantsListResponse +from .health import DetailedHealthResponse, HealthResponse +from .plant import ( + PlantAlertUpdateRequest, + PlantAlertUpdateResponse, + PlantBase, + PlantDetailResponse, + PlantResponse, + PlantsListResponse, + PlantWithLatestLog, +) +from .stats import ComparisonStatsResponse, HourlyStatsResponse, PlantStatsResponse +from .upload import UploadResponse diff --git a/api_server/app/schemas/health.py b/api_server/app/schemas/health.py new file mode 100644 index 0000000..44565c2 --- /dev/null +++ b/api_server/app/schemas/health.py @@ -0,0 +1,15 @@ +"""서비스 상태 확인 응답 스키마.""" + +from typing import Literal + +from pydantic import BaseModel + + +class HealthResponse(BaseModel): + status: Literal["healthy"] + app_name: str + version: str + + +class DetailedHealthResponse(HealthResponse): + supabase_connected: bool diff --git a/api_server/app/schemas/plant.py b/api_server/app/schemas/plant.py index 4343e75..afb5d8b 100644 --- a/api_server/app/schemas/plant.py +++ b/api_server/app/schemas/plant.py @@ -48,6 +48,22 @@ class PlantResponse(BaseModel): data: PlantWithLatestLog +class PlantDetailData(BaseModel): + plant: PlantBase + recent_logs: List[SolarLogBase] + + +class PlantDetailResponse(BaseModel): + status: str = "success" + data: PlantDetailData + + +class PlantAlertUpdateResponse(BaseModel): + status: str = "success" + message: str + data: PlantBase + + class PlantsListResponse(BaseModel): """발전소 목록 응답 스키마""" status: str = "success" diff --git a/api_server/app/schemas/stats.py b/api_server/app/schemas/stats.py new file mode 100644 index 0000000..10041c8 --- /dev/null +++ b/api_server/app/schemas/stats.py @@ -0,0 +1,50 @@ +"""통계 API 응답 스키마.""" + +from typing import List, Literal + +from pydantic import BaseModel + + +class ComparisonStatItem(BaseModel): + plant_id: str + plant_name: str + capacity: float + generation: float + generation_hours: float + + +class ComparisonStatsResponse(BaseModel): + status: Literal["success"] + period: Literal["day", "month", "year"] + target_date: str + data: List[ComparisonStatItem] + count: int + + +class StatPoint(BaseModel): + label: str + value: float + + +class PlantStatsResponse(BaseModel): + status: Literal["success"] + plant_id: str + period: Literal["day", "month", "year"] + data: List[StatPoint] + count: int + + +class HourlyStatPoint(BaseModel): + hour: int + label: str + current_kw: float + today_kwh: float + has_data: bool + + +class HourlyStatsResponse(BaseModel): + status: Literal["success"] + plant_id: str + date: str + data: List[HourlyStatPoint] + count: int diff --git a/api_server/app/schemas/upload.py b/api_server/app/schemas/upload.py new file mode 100644 index 0000000..200324e --- /dev/null +++ b/api_server/app/schemas/upload.py @@ -0,0 +1,15 @@ +"""Excel 업로드 성공 응답 스키마.""" + +from typing import List, Literal, Optional + +from pydantic import BaseModel, Field + + +class UploadResponse(BaseModel): + status: Literal["success"] + message: str + saved_count: int + error_count: int = 0 + errors: List[str] = Field(default_factory=list) + plant_id: Optional[str] = None + plant_name: Optional[str] = None diff --git a/api_server/requirements.in b/api_server/requirements.in new file mode 100644 index 0000000..20924e4 --- /dev/null +++ b/api_server/requirements.in @@ -0,0 +1,8 @@ +# Direct API dependencies. api_server/requirements.txt is the deployed lock. +fastapi==0.128.0 +openpyxl==3.1.5 +pandas==2.3.3 +pydantic-settings==2.12.0 +python-multipart==0.0.21 +supabase==2.27.2 +uvicorn[standard]==0.40.0 diff --git a/api_server/requirements.txt b/api_server/requirements.txt index d2b4704..9576387 100644 --- a/api_server/requirements.txt +++ b/api_server/requirements.txt @@ -62,7 +62,7 @@ typing_extensions==4.15.0 tzdata==2025.3 urllib3==2.6.3 uvicorn==0.40.0 -uvloop==0.22.1 +uvloop==0.22.1; sys_platform != "win32" watchfiles==1.1.1 websockets==15.0.1 yarl==1.22.0 diff --git a/api_server/tests/test_api_contract.py b/api_server/tests/test_api_contract.py new file mode 100644 index 0000000..90b2aaa --- /dev/null +++ b/api_server/tests/test_api_contract.py @@ -0,0 +1,100 @@ +import unittest + +from fastapi.testclient import TestClient + +from app.core.database import get_db +from app.main import app +from tests.test_plants import FakeDb + + +class FailingDb: + def table(self, _table_name): + raise RuntimeError("database unavailable") + + +class ApiContractTest(unittest.TestCase): + def setUp(self): + self.db = FakeDb() + app.dependency_overrides[get_db] = lambda: self.db + self.client = TestClient(app, raise_server_exceptions=False) + + def tearDown(self): + app.dependency_overrides.clear() + + def test_health_checks_real_database_query(self): + response = self.client.get("/health") + + self.assertEqual(200, response.status_code) + self.assertTrue(response.json()["supabase_connected"]) + self.assertEqual(1, len(self.db.queries)) + self.assertEqual("plants", self.db.queries[0].table_name) + + def test_health_returns_503_when_database_query_fails(self): + app.dependency_overrides[get_db] = lambda: FailingDb() + + response = self.client.get("/health") + + self.assertEqual(503, response.status_code) + self.assertEqual( + "데이터베이스 연결 상태를 확인할 수 없습니다.", + response.json()["detail"], + ) + + def test_string_plant_detail_path_returns_200(self): + response = self.client.get("/plants/1/nrems-03") + + self.assertEqual(200, response.status_code) + self.assertEqual("nrems-03", response.json()["data"]["plant"]["id"]) + + def test_invalid_comparison_date_returns_400(self): + response = self.client.get( + "/plants/stats/comparison?period=day&date=2026-8-07" + ) + + self.assertEqual(400, response.status_code) + + def test_invalid_month_returns_422(self): + response = self.client.get( + "/plants/nrems-03/stats?period=day&year=2026&month=13" + ) + + self.assertEqual(422, response.status_code) + + def test_unknown_plant_stats_returns_404(self): + app.dependency_overrides[get_db] = lambda: FakeDb(plant_exists=False) + + response = self.client.get( + "/plants/missing/stats?period=day&year=2026&month=8" + ) + + self.assertEqual(404, response.status_code) + + def test_openapi_declares_string_plant_id_and_response_models(self): + schema = self.client.get("/openapi.json").json() + detail_operation = schema["paths"]["/plants/{company_id}/{plant_id}"]["get"] + plant_id_param = next( + item for item in detail_operation["parameters"] + if item["name"] == "plant_id" + ) + + self.assertEqual("string", plant_id_param["schema"]["type"]) + self.assertIn("200", detail_operation["responses"]) + self.assertIn( + "PlantDetailResponse", + str(detail_operation["responses"]["200"]), + ) + self.assertIn( + "DetailedHealthResponse", + str(schema["paths"]["/health"]["get"]["responses"]["200"]), + ) + self.assertIn( + "UploadResponse", + str( + schema["paths"]["/plants/{plant_id}/upload"] + ["post"]["responses"]["200"] + ), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/api_server/tests/test_plants.py b/api_server/tests/test_plants.py new file mode 100644 index 0000000..a8c255d --- /dev/null +++ b/api_server/tests/test_plants.py @@ -0,0 +1,153 @@ +import unittest +from types import SimpleNamespace + +from fastapi import HTTPException + +from app.routers import plants + + +PLANT = { + "id": "nrems-03", + "company_id": 1, + "name": "3호기", + "capacity": 99.0, + "location": None, + "alerts_enabled": True, + "created_at": "2026-01-01T00:00:00+00:00", +} + +LOG = { + "id": 10, + "plant_id": "nrems-03", + "current_kw": 20.0, + "today_kwh": 100.0, + "status": "정상", + "created_at": "2026-08-07T03:00:00+00:00", +} + + +class FakeQuery: + def __init__(self, db, table_name): + self.db = db + self.table_name = table_name + self.columns = None + self.filters = [] + self.action = "select" + self.payload = None + self.db.queries.append(self) + + def select(self, columns): + self.columns = columns + return self + + def eq(self, column, value): + self.filters.append((column, value)) + return self + + def order(self, _column, desc=False, foreign_table=None): + self.foreign_table = foreign_table + return self + + def limit(self, _count, foreign_table=None): + self.limit_foreign_table = foreign_table + return self + + def update(self, payload): + self.action = "update" + self.payload = payload + return self + + def execute(self): + if self.db.query_failure: + raise RuntimeError("database unavailable") + if self.table_name == "plants": + if not self.db.plant_exists: + return SimpleNamespace(data=[]) + if self.action == "update": + updated = dict(PLANT, **self.payload) + return SimpleNamespace(data=[updated]) + if "solar_logs(" in self.columns: + return SimpleNamespace(data=[dict(PLANT, solar_logs=[LOG])]) + return SimpleNamespace(data=[PLANT]) + if self.table_name == "solar_logs": + return SimpleNamespace(data=[LOG]) + raise AssertionError(f"unexpected table: {self.table_name}") + + +class FakeDb: + def __init__(self, plant_exists=True, query_failure=False): + self.plant_exists = plant_exists + self.query_failure = query_failure + self.queries = [] + + def table(self, table_name): + return FakeQuery(self, table_name) + + +class PlantsApiTest(unittest.TestCase): + def test_company_list_uses_one_nested_query(self): + db = FakeDb() + + result = plants.get_plants_by_company(company_id=1, db=db) + + self.assertEqual(1, len(db.queries)) + self.assertEqual("plants", db.queries[0].table_name) + self.assertEqual("solar_logs", db.queries[0].foreign_table) + self.assertEqual("solar_logs", db.queries[0].limit_foreign_table) + self.assertEqual("nrems-03", result.data[0].id) + self.assertEqual(10, result.data[0].latest_log.id) + + def test_company_without_plants_returns_empty_success(self): + result = plants.get_plants_by_company( + company_id=999, + db=FakeDb(plant_exists=False), + ) + + self.assertEqual("success", result.status) + self.assertEqual([], result.data) + self.assertEqual(0, result.total_count) + + def test_company_query_failure_returns_500(self): + with self.assertRaises(HTTPException) as raised: + plants.get_plants_by_company( + company_id=1, + db=FakeDb(query_failure=True), + ) + + self.assertEqual(500, raised.exception.status_code) + + def test_string_plant_id_is_used_for_detail_lookup(self): + db = FakeDb() + + result = plants.get_plant_detail( + company_id=1, plant_id="nrems-03", db=db + ) + + plant_query = db.queries[0] + self.assertIn(("id", "nrems-03"), plant_query.filters) + self.assertEqual("nrems-03", result["data"]["plant"]["id"]) + self.assertEqual(10, result["data"]["recent_logs"][0]["id"]) + + def test_missing_detail_returns_404(self): + with self.assertRaises(HTTPException) as raised: + plants.get_plant_detail( + company_id=1, + plant_id="missing", + db=FakeDb(plant_exists=False), + ) + + self.assertEqual(404, raised.exception.status_code) + + def test_alert_update_uses_typed_response(self): + result = plants.update_plant_alerts( + company_id=1, + plant_id="nrems-03", + alert_update=plants.PlantAlertUpdateRequest(alerts_enabled=False), + db=FakeDb(), + ) + + self.assertFalse(result.data.alerts_enabled) + + +if __name__ == "__main__": + unittest.main() diff --git a/api_server/tests/test_stats_timezone.py b/api_server/tests/test_stats_timezone.py new file mode 100644 index 0000000..d0c1fd6 --- /dev/null +++ b/api_server/tests/test_stats_timezone.py @@ -0,0 +1,170 @@ +import unittest +from datetime import date +from unittest.mock import patch + +from fastapi import HTTPException + +from app.routers import stats + + +class FakeResponse: + def __init__(self, data=None): + self.data = data or [] + + +class FakeQuery: + def __init__(self, db, table_name): + self.db = db + self.table_name = table_name + self.filters = [] + self.db.queries.append(self) + + def select(self, _columns): + return self + + def eq(self, column, value): + self.filters.append(("eq", column, value)) + return self + + def gte(self, column, value): + self.filters.append(("gte", column, value)) + return self + + def lte(self, column, value): + self.filters.append(("lte", column, value)) + return self + + def lt(self, column, value): + self.filters.append(("lt", column, value)) + return self + + def order(self, _column, desc=False): + return self + + def limit(self, _count): + return self + + def execute(self): + if self.table_name == "plants": + if not self.db.plant_exists: + return FakeResponse() + return FakeResponse([{"id": "plant-a", "name": "1호기", "capacity": 100}]) + if self.table_name == "solar_logs": + return FakeResponse(self.db.solar_logs) + if self.table_name == "monthly_stats": + return FakeResponse(self.db.monthly_stats) + return FakeResponse() + + +class FakeDb: + def __init__(self, solar_logs=None, monthly_stats=None, plant_exists=True): + self.solar_logs = solar_logs or [] + self.monthly_stats = monthly_stats or [] + self.plant_exists = plant_exists + self.queries = [] + + def table(self, table_name): + return FakeQuery(self, table_name) + + def latest_query(self, table_name): + return next(query for query in reversed(self.queries) if query.table_name == table_name) + + +class StatsTimezoneTest(unittest.TestCase): + def test_comparison_today_uses_kst_utc_half_open_bounds(self): + db = FakeDb([{"plant_id": "plant-a", "today_kwh": 123.4}]) + + with patch.object(stats, "today_kst", return_value=date(2026, 1, 1)): + result = stats.get_all_plants_comparison( + period="day", date=None, year=None, month=None, db=db + ) + + query = db.latest_query("solar_logs") + self.assertIn(("gte", "created_at", "2025-12-31T15:00:00+00:00"), query.filters) + self.assertIn(("lt", "created_at", "2026-01-01T15:00:00+00:00"), query.filters) + self.assertEqual(123.4, result.data[0].generation) + + def test_plant_today_query_has_upper_bound(self): + db = FakeDb([{"today_kwh": 77.0}]) + + with patch.object(stats, "today_kst", return_value=date(2026, 8, 6)): + result = stats.get_plant_stats( + plant_id="plant-a", period="day", year=2026, month=8, db=db + ) + + query = db.latest_query("solar_logs") + self.assertIn(("gte", "created_at", "2026-08-05T15:00:00+00:00"), query.filters) + self.assertIn(("lt", "created_at", "2026-08-06T15:00:00+00:00"), query.filters) + self.assertEqual(77.0, result.data[5].value) + + def test_hourly_stats_map_utc_edges_to_kst_hours(self): + db = FakeDb([ + {"created_at": "2026-12-30T15:00:00Z", "current_kw": 1, "today_kwh": 1}, + {"created_at": "2026-12-31T14:59:59Z", "current_kw": 2, "today_kwh": 2}, + {"created_at": "2026-12-31T15:00:00Z", "current_kw": 3, "today_kwh": 3}, + ]) + + result = stats.get_plant_hourly_stats( + plant_id="plant-a", date="2026-12-31", db=db + ) + + query = db.latest_query("solar_logs") + self.assertIn(("gte", "created_at", "2026-12-30T15:00:00+00:00"), query.filters) + self.assertIn(("lt", "created_at", "2026-12-31T15:00:00+00:00"), query.filters) + self.assertTrue(result.data[0].has_data) + self.assertTrue(result.data[23].has_data) + self.assertEqual(2, result.count) + + def test_invalid_hourly_date_remains_400(self): + with self.assertRaises(HTTPException) as raised: + stats.get_plant_hourly_stats( + plant_id="plant-a", date="2026-13-01", db=FakeDb() + ) + + self.assertEqual(400, raised.exception.status_code) + + def test_invalid_comparison_date_is_not_replaced_with_today(self): + db = FakeDb() + + with self.assertRaises(HTTPException) as raised: + stats.get_all_plants_comparison( + period="day", date="2026-8-07", year=None, month=None, db=db + ) + + self.assertEqual(400, raised.exception.status_code) + self.assertEqual([], db.queries) + + def test_unknown_plant_stats_returns_404(self): + with self.assertRaises(HTTPException) as raised: + stats.get_plant_stats( + plant_id="missing", period="day", year=2026, month=8, + db=FakeDb(plant_exists=False), + ) + + self.assertEqual(404, raised.exception.status_code) + + def test_leap_year_uses_366_days(self): + db = FakeDb(monthly_stats=[{ + "plant_id": "plant-a", + "total_generation": 36600, + }]) + + result = stats.get_all_plants_comparison( + period="year", date="2024-12-31", year=2024, month=None, db=db + ) + + self.assertEqual(1.0, result.data[0].generation_hours) + + def test_selected_year_is_used_as_yearly_query_end(self): + db = FakeDb() + + stats.get_plant_stats( + plant_id="plant-a", period="year", year=2024, month=None, db=db + ) + + query = db.latest_query("monthly_stats") + self.assertIn(("lte", "month", "2024-12"), query.filters) + + +if __name__ == "__main__": + unittest.main() diff --git a/api_server/tests/test_upload.py b/api_server/tests/test_upload.py new file mode 100644 index 0000000..4397963 --- /dev/null +++ b/api_server/tests/test_upload.py @@ -0,0 +1,180 @@ +import io +import unittest +from unittest.mock import patch + +import pandas as pd +from fastapi.testclient import TestClient + +from app.core.database import get_db +from app.main import app +from app.routers import upload + + +class FakeResponse: + def __init__(self, data=None): + self.data = data or [] + + +class FakeRpc: + def __init__(self, db, params): + self.db = db + self.params = params + + def execute(self): + self.db.saved["daily_stats"] = self.params["p_records"] + self.db.rpc_params = self.params + return FakeResponse(self.params["p_records"]) + + +class FakeQuery: + def __init__(self, db, table_name): + self.db = db + self.table_name = table_name + self.action = None + self.payload = None + self.filters = {} + + def select(self, _columns): + self.action = "select" + return self + + def eq(self, column, value): + self.filters[column] = value + return self + + def limit(self, _count): + return self + + def upsert(self, payload, on_conflict=None): + self.action = "upsert" + self.payload = payload + self.on_conflict = on_conflict + return self + + def execute(self): + if self.action == "select" and self.table_name == "plants": + if self.filters.get("id") != "plant-a": + return FakeResponse() + return FakeResponse([{ + "id": "plant-a", + "name": "테스트 발전소", + "capacity": 100.0, + }]) + + if self.action == "upsert": + self.db.saved[self.table_name] = self.payload + return FakeResponse(self.payload) + + raise AssertionError(f"unexpected query: {self.table_name} {self.action}") + + +class FakeDb: + def __init__(self): + self.saved = {} + self.rpc_params = None + + def table(self, table_name): + return FakeQuery(self, table_name) + + def rpc(self, function_name, params): + if function_name != "upsert_daily_stats": + raise AssertionError(function_name) + return FakeRpc(self, params) + + +def excel_bytes(rows): + output = io.BytesIO() + pd.DataFrame(rows).to_excel(output, index=False) + return output.getvalue() + + +class UploadApiTest(unittest.TestCase): + def setUp(self): + self.db = FakeDb() + app.dependency_overrides[get_db] = lambda: self.db + self.client = TestClient(app) + + def tearDown(self): + app.dependency_overrides.clear() + + def post_daily(self, content, filename="daily.xlsx", plant_id="plant-a"): + return self.client.post( + f"/plants/{plant_id}/upload", + files={"file": (filename, content, "application/octet-stream")}, + ) + + def post_monthly(self, content, filename="monthly.xlsx", plant_id="plant-a"): + return self.client.post( + f"/plants/{plant_id}/upload/monthly", + files={"file": (filename, content, "application/octet-stream")}, + ) + + def test_daily_sample_is_saved(self): + response = self.post_daily(excel_bytes([ + {"date": "2026-08-05", "generation": 123.4}, + ])) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["saved_count"], 1) + self.assertEqual(self.db.saved["daily_stats"][0]["total_generation"], 123.4) + self.assertEqual(self.db.rpc_params["p_source"], "excel_daily") + self.assertTrue(self.db.rpc_params["p_allow_decrease"]) + + def test_monthly_sample_with_merged_year_is_saved(self): + response = self.post_monthly(excel_bytes([ + {"year": 2026, "month": "1월", "kwh": "1,234.5"}, + {"year": None, "month": "2월", "kwh": 987.6}, + ])) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["saved_count"], 2) + self.assertEqual(self.db.saved["monthly_stats"][1]["month"], "2026-02") + self.assertEqual(self.db.saved["monthly_stats"][1]["source"], "excel_monthly") + self.assertEqual(self.db.saved["monthly_stats"][1]["last_date"], "2026-02-28") + + def test_wrong_extension_returns_400(self): + response = self.post_daily(b"text", filename="daily.csv") + self.assertEqual(response.status_code, 400) + + def test_empty_file_returns_400(self): + response = self.post_daily(b"") + self.assertEqual(response.status_code, 400) + + def test_missing_columns_returns_400(self): + response = self.post_daily(excel_bytes([{"wrong": 1}])) + self.assertEqual(response.status_code, 400) + + def test_invalid_date_returns_400(self): + response = self.post_daily(excel_bytes([ + {"date": "not-a-date", "generation": 1}, + ])) + self.assertEqual(response.status_code, 400) + + def test_negative_generation_returns_400(self): + response = self.post_daily(excel_bytes([ + {"date": "2026-08-05", "generation": -1}, + ])) + self.assertEqual(response.status_code, 400) + + def test_unknown_plant_returns_404(self): + response = self.post_daily( + excel_bytes([{"date": "2026-08-05", "generation": 1}]), + plant_id="missing", + ) + self.assertEqual(response.status_code, 404) + + def test_file_size_limit_returns_413(self): + response = self.post_daily(b"x" * (upload.MAX_UPLOAD_BYTES + 1)) + self.assertEqual(response.status_code, 413) + + def test_row_limit_returns_400(self): + with patch.object(upload, "MAX_UPLOAD_ROWS", 1): + response = self.post_daily(excel_bytes([ + {"date": "2026-08-05", "generation": 1}, + {"date": "2026-08-06", "generation": 2}, + ])) + self.assertEqual(response.status_code, 400) + + +if __name__ == "__main__": + unittest.main() diff --git a/app/.gitignore b/app/.gitignore index 06e699f..7281aad 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -11,7 +11,6 @@ node_modules/ # npm npm-debug.log -package-lock.json yarn-error.log # Mac diff --git a/app/components/UploadModal.js b/app/components/UploadModal.js index 4b221a6..28fe989 100644 --- a/app/components/UploadModal.js +++ b/app/components/UploadModal.js @@ -58,12 +58,17 @@ export default function UploadModal({ visible, onClose, plantId, onUploadSuccess try { const formData = new FormData(); - // 파일 추가 - formData.append('file', { - uri: selectedFile.uri, - name: selectedFile.name, - type: selectedFile.mimeType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - }); + if (selectedFile.file) { + // Web: 브라우저가 multipart boundary와 파일명을 설정한다. + formData.append('file', selectedFile.file, selectedFile.name); + } else { + // Native: React Native FormData가 요구하는 URI 객체를 사용한다. + formData.append('file', { + uri: selectedFile.uri, + name: selectedFile.name, + type: selectedFile.mimeType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + } // uploadType에 따라 엔드포인트 분기 const endpoint = uploadType === 'daily' @@ -76,9 +81,6 @@ export default function UploadModal({ visible, onClose, plantId, onUploadSuccess const response = await fetch(`${API_URL}${endpoint}`, { method: 'POST', body: formData, - headers: { - 'Content-Type': 'multipart/form-data', - }, }); const result = await response.json(); @@ -180,7 +182,7 @@ export default function UploadModal({ visible, onClose, plantId, onUploadSuccess {/* 선택된 파일 정보 */} - {selectedFile && ( + {selectedFile?.size != null && ( 크기: {(selectedFile.size / 1024).toFixed(1)} KB diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..b41b8d1 --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,11802 @@ +{ + "name": "solorpower-dashboard", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "solorpower-dashboard", + "version": "1.0.0", + "dependencies": { + "@expo/metro-runtime": "~4.0.1", + "@react-navigation/native": "^7.1.28", + "@react-navigation/native-stack": "^7.10.1", + "expo": "~52.0.0", + "expo-asset": "~11.0.5", + "expo-document-picker": "~13.0.3", + "expo-linear-gradient": "~14.0.2", + "expo-status-bar": "~2.0.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-native": "0.76.5", + "react-native-gifted-charts": "^1.4.70", + "react-native-safe-area-context": "^5.6.2", + "react-native-screens": "^4.20.0", + "react-native-svg": "^15.15.1", + "react-native-web": "~0.19.13" + }, + "devDependencies": { + "@babel/core": "^7.25.2", + "supabase": "^2.106.0" + } + }, + "node_modules/@0no-co/graphql.web": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.3.3.tgz", + "integrity": "sha512-4gFGBdyaFmQ6n9euhp5JtIGS4ZeivwDr1tCPENUxTvy5wyv532yOtFCr9zzYAJh1s6uibgC+TRXUcay+mxzCoQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/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.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "license": "MIT", + "dependencies": { + "@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/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "license": "MIT", + "dependencies": { + "@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.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", + "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", + "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.", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.29.7.tgz", + "integrity": "sha512-p+G5BNXDcy3bOXplhY4HybQ1GxH3i2Tppmdm/3epyRu2VgJJZuUlZ61MqRTg582Q7ZLBdP7fePYvsumSEkMxcQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", + "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", + "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.", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", + "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", + "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.", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.29.7.tgz", + "integrity": "sha512-foag0BB37ROhdeIX9O8G0jX7hw0UekJc04cHMrYLOnrErsnBKqJGHJ8eDRpoCFZBvEPPygmmtw4qyU97qa4oOw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz", + "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.29.7.tgz", + "integrity": "sha512-+1wdDMGNb4UPeY3Q4L5yLiYe6TXPXubs4NjrgRFw13hPRLJfEMw2Q5OXkee6/IfdqePIeW4Jjwe3aBh7SdKz4Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.29.7.tgz", + "integrity": "sha512-WsZulLVBUHXVj2cUcPVx6UE21TpalB6bHbSFErKT0Ib++ax24jjXe73FqlWvdylFOjiuPHYi6VCcgRad1ItN+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.29.7.tgz", + "integrity": "sha512-Xfy3UVMF04+ypnFbkhvfqtmvwfe92qwQdbGZVonhE+6v35GzlofmOnA1szaZqzb9xYWr0nl1e5EMmzi0DNON1g==", + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.29.7.tgz", + "integrity": "sha512-H5E+HBgDpr6Q5t+Aj11tL7XkIui1jhbIoArVQnqjgXo5/3YxkN7ZEBcWF4RQlB0T4rrxJQbXS6kiFV6B7XTqUA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.7.tgz", + "integrity": "sha512-xmAscdE/AsqRW7vutbPNoUmu/nF5SrLKPs7aoJgEjo35lLKA/Bc0i2rMv/hr1+Y0o1bQCiVtith3u2vdgRL39Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz", + "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^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" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.29.7.tgz", + "integrity": "sha512-KYIRV0BuaN68CDdsqFkAD7MU7yipUqQNuNElwATdxaIdpTjhvtY82QvkBJs7zV3Evxj2jFAAZ1iO8nyy0nhjqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-flow-strip-types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.29.7.tgz", + "integrity": "sha512-C+PV1TFUPTmBQGoPBL8j2QmLpZ117YTCwxIZeJOM96GbYMFSc7/pOXU5lVykwnZxyTqQxRsvoRk6f2FktZgGHA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-react-display-name": "^7.29.7", + "@babel/plugin-transform-react-jsx": "^7.29.7", + "@babel/plugin-transform-react-jsx-development": "^7.29.7", + "@babel/plugin-transform-react-pure-annotations": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.7.tgz", + "integrity": "sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map": { + "name": "@babel/traverse", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ecies/ciphers": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", + "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", + "dev": true, + "license": "MIT", + "engines": { + "bun": ">=1", + "deno": ">=2.7.10", + "node": ">=16" + }, + "peerDependencies": { + "@noble/ciphers": "^1.0.0" + } + }, + "node_modules/@expo/bunyan": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@expo/bunyan/-/bunyan-4.0.1.tgz", + "integrity": "sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==", + "license": "MIT", + "dependencies": { + "uuid": "^8.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@expo/cli": { + "version": "0.22.28", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.22.28.tgz", + "integrity": "sha512-lvt72KNitGuixYD2l3SZmRKVu2G4zJpmg5V7WfUBNpmUU5oODBw/6qmiJ6kSLAlfDozscUk+BBGknBBzxUrwrA==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.0.8", + "@babel/runtime": "^7.20.0", + "@expo/code-signing-certificates": "^0.0.6", + "@expo/config": "~10.0.11", + "@expo/config-plugins": "~9.0.17", + "@expo/devcert": "^1.1.2", + "@expo/env": "~0.4.2", + "@expo/image-utils": "^0.6.5", + "@expo/json-file": "^9.0.2", + "@expo/metro-config": "~0.19.12", + "@expo/osascript": "^2.1.6", + "@expo/package-manager": "^1.7.2", + "@expo/plist": "^0.2.2", + "@expo/prebuild-config": "~8.2.0", + "@expo/rudder-sdk-node": "^1.1.1", + "@expo/spawn-async": "^1.7.2", + "@expo/ws-tunnel": "^1.0.1", + "@expo/xcpretty": "^4.3.0", + "@react-native/dev-middleware": "0.76.9", + "@urql/core": "^5.0.6", + "@urql/exchange-retry": "^1.3.0", + "accepts": "^1.3.8", + "arg": "^5.0.2", + "better-opn": "~3.0.2", + "bplist-creator": "0.0.7", + "bplist-parser": "^0.3.1", + "cacache": "^18.0.2", + "chalk": "^4.0.0", + "ci-info": "^3.3.0", + "compression": "^1.7.4", + "connect": "^3.7.0", + "debug": "^4.3.4", + "env-editor": "^0.4.1", + "fast-glob": "^3.3.2", + "form-data": "^3.0.1", + "freeport-async": "^2.0.0", + "fs-extra": "~8.1.0", + "getenv": "^1.0.0", + "glob": "^10.4.2", + "internal-ip": "^4.3.0", + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1", + "lodash.debounce": "^4.0.8", + "minimatch": "^3.0.4", + "node-forge": "^1.3.3", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "picomatch": "^3.0.1", + "pretty-bytes": "^5.6.0", + "pretty-format": "^29.7.0", + "progress": "^2.0.3", + "prompts": "^2.3.2", + "qrcode-terminal": "0.11.0", + "require-from-string": "^2.0.2", + "requireg": "^0.2.2", + "resolve": "^1.22.2", + "resolve-from": "^5.0.0", + "resolve.exports": "^2.0.3", + "semver": "^7.6.0", + "send": "^0.19.0", + "slugify": "^1.3.4", + "source-map-support": "~0.5.21", + "stacktrace-parser": "^0.1.10", + "structured-headers": "^0.4.1", + "tar": "^6.2.1", + "temp-dir": "^2.0.0", + "tempy": "^0.7.1", + "terminal-link": "^2.1.1", + "undici": "^6.18.2", + "unique-string": "~2.0.0", + "wrap-ansi": "^7.0.0", + "ws": "^8.12.1" + }, + "bin": { + "expo-internal": "build/bin/cli" + } + }, + "node_modules/@expo/cli/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/code-signing-certificates": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", + "integrity": "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==", + "license": "MIT", + "dependencies": { + "node-forge": "^1.3.3" + } + }, + "node_modules/@expo/config": { + "version": "10.0.11", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-10.0.11.tgz", + "integrity": "sha512-nociJ4zr/NmbVfMNe9j/+zRlt7wz/siISu7PjdWE4WE+elEGxWWxsGzltdJG0llzrM+khx8qUiFK5aiVcdMBww==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "@expo/config-plugins": "~9.0.17", + "@expo/config-types": "^52.0.5", + "@expo/json-file": "^9.0.2", + "deepmerge": "^4.3.1", + "getenv": "^1.0.0", + "glob": "^10.4.2", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0", + "resolve-workspace-root": "^2.0.0", + "semver": "^7.6.0", + "slugify": "^1.3.4", + "sucrase": "3.35.0" + } + }, + "node_modules/@expo/config-plugins": { + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-9.0.17.tgz", + "integrity": "sha512-m24F1COquwOm7PBl5wRbkT9P9DviCXe0D7S7nQsolfbhdCWuvMkfXeoWmgjtdhy7sDlOyIgBrAdnB6MfsWKqIg==", + "license": "MIT", + "dependencies": { + "@expo/config-types": "^52.0.5", + "@expo/json-file": "~9.0.2", + "@expo/plist": "^0.2.2", + "@expo/sdk-runtime-versions": "^1.0.0", + "chalk": "^4.1.2", + "debug": "^4.3.5", + "getenv": "^1.0.0", + "glob": "^10.4.2", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "slash": "^3.0.0", + "slugify": "^1.6.6", + "xcode": "^3.0.1", + "xml2js": "0.6.0" + } + }, + "node_modules/@expo/config-plugins/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@expo/config-plugins/node_modules/@expo/json-file": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.0.2.tgz", + "integrity": "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3", + "write-file-atomic": "^2.3.0" + } + }, + "node_modules/@expo/config-plugins/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/config-types": { + "version": "52.0.5", + "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-52.0.5.tgz", + "integrity": "sha512-AMDeuDLHXXqd8W+0zSjIt7f37vUd/BP8p43k68NHpyAvQO+z8mbQZm3cNQVAMySeayK2XoPigAFB1JF2NFajaA==", + "license": "MIT" + }, + "node_modules/@expo/config/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@expo/config/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/devcert": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.1.tgz", + "integrity": "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==", + "license": "MIT", + "dependencies": { + "@expo/sudo-prompt": "^9.3.1", + "debug": "^3.1.0" + } + }, + "node_modules/@expo/devcert/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@expo/env": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-0.4.2.tgz", + "integrity": "sha512-TgbCgvSk0Kq0e2fLoqHwEBL4M0ztFjnBEz0YCDm5boc1nvkV1VMuIMteVdeBwnTh8Z0oPJTwHCD49vhMEt1I6A==", + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^1.0.0" + } + }, + "node_modules/@expo/fingerprint": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.11.11.tgz", + "integrity": "sha512-gNyn1KnAOpEa8gSNsYqXMTcq0fSwqU/vit6fP5863vLSKxHm/dNt/gm/uZJxrRZxKq71KUJWF6I7d3z8qIfq5g==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "arg": "^5.0.2", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "find-up": "^5.0.0", + "getenv": "^1.0.0", + "minimatch": "^3.0.4", + "p-limit": "^3.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0" + }, + "bin": { + "fingerprint": "bin/cli.js" + } + }, + "node_modules/@expo/fingerprint/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/image-utils": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.6.5.tgz", + "integrity": "sha512-RsS/1CwJYzccvlprYktD42KjyfWZECH6PPIEowvoSmXfGLfdViwcUEI4RvBfKX5Jli6P67H+6YmHvPTbGOboew==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "fs-extra": "9.0.0", + "getenv": "^1.0.0", + "jimp-compact": "0.16.1", + "parse-png": "^2.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "temp-dir": "~2.0.0", + "unique-string": "~2.0.0" + } + }, + "node_modules/@expo/image-utils/node_modules/fs-extra": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.0.tgz", + "integrity": "sha512-pmEYSk3vYsG/bF651KPUXZ+hvjpgWYw/Gc7W9NFUe3ZVLczKKWIij3IKpOrQcdw4TILtibFslZ0UmR8Vvzig4g==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^1.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/image-utils/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@expo/image-utils/node_modules/jsonfile/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@expo/image-utils/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/image-utils/node_modules/universalify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz", + "integrity": "sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@expo/json-file": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.1.5.tgz", + "integrity": "sha512-prWBhLUlmcQtvN6Y7BpW2k9zXGd3ySa3R6rAguMJkp1z22nunLN64KYTUWfijFlprFoxm9r2VNnGkcbndAlgKA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3" + } + }, + "node_modules/@expo/json-file/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@expo/metro-config": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.19.12.tgz", + "integrity": "sha512-fhT3x1ikQWHpZgw7VrEghBdscFPz1laRYa8WcVRB18nTTqorF6S8qPYslkJu1faEziHZS7c2uyDzTYnrg/CKbg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@babel/parser": "^7.20.0", + "@babel/types": "^7.20.0", + "@expo/config": "~10.0.11", + "@expo/env": "~0.4.2", + "@expo/json-file": "~9.0.2", + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "fs-extra": "^9.1.0", + "getenv": "^1.0.0", + "glob": "^10.4.2", + "jsc-safe-url": "^0.2.4", + "lightningcss": "~1.27.0", + "minimatch": "^3.0.4", + "postcss": "~8.4.32", + "resolve-from": "^5.0.0" + } + }, + "node_modules/@expo/metro-config/node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@expo/metro-config/node_modules/@expo/json-file": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-9.0.2.tgz", + "integrity": "sha512-yAznIUrybOIWp3Uax7yRflB0xsEpvIwIEqIjao9SGi2Gaa+N0OamWfe0fnXBSWF+2zzF4VvqwT4W5zwelchfgw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3", + "write-file-atomic": "^2.3.0" + } + }, + "node_modules/@expo/metro-config/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/metro-config/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@expo/metro-config/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@expo/metro-runtime": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-4.0.1.tgz", + "integrity": "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react-native": "*" + } + }, + "node_modules/@expo/osascript": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.7.1.tgz", + "integrity": "sha512-Zn03EX6In7ts2lPUW2ESUSkEhEWQN1qqsiXjadtZMJOuZRkMiAg1ZQHuvz9DjByDWNJ2pBwAGyrts9lj9k389g==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/package-manager": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.13.1.tgz", + "integrity": "sha512-y/K+CaYYpZpNGZhSX4HyLT/vyIunFjNfyoxNysPBCefeLKI/VCx6f9LNPzrxayr3rCYO5bl9O8H+HRQK265Nkg==", + "license": "MIT", + "dependencies": { + "@expo/json-file": "^11.0.1", + "@expo/spawn-async": "^1.8.0", + "chalk": "^4.0.0", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "resolve-workspace-root": "^2.0.0" + } + }, + "node_modules/@expo/package-manager/node_modules/@expo/json-file": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-11.0.1.tgz", + "integrity": "sha512-zxHWj4MKKMAL29ZQSY/Fssx4Thluk40JmuGNaeS078wy/NhlFhnVi+rHHunulE3xJAJ0CM73m8VK2+GkF9eRwQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "json5": "^2.2.3" + } + }, + "node_modules/@expo/plist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.2.2.tgz", + "integrity": "sha512-ZZGvTO6vEWq02UAPs3LIdja+HRO18+LRI5QuDl6Hs3Ps7KX7xU6Y6kjahWKY37Rx2YjNpX07dGpBFzzC+vKa2g==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "~0.7.7", + "base64-js": "^1.2.3", + "xmlbuilder": "^14.0.0" + } + }, + "node_modules/@expo/prebuild-config": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-8.2.0.tgz", + "integrity": "sha512-CxiPpd980s0jyxi7eyN3i/7YKu3XL+8qPjBZUCYtc0+axpGweqIkq2CslyLSKHyqVyH/zlPkbVgWdyiYavFS5Q==", + "license": "MIT", + "dependencies": { + "@expo/config": "~10.0.11", + "@expo/config-plugins": "~9.0.17", + "@expo/config-types": "^52.0.5", + "@expo/image-utils": "^0.6.5", + "@expo/json-file": "^9.0.2", + "@react-native/normalize-colors": "0.76.9", + "debug": "^4.3.1", + "fs-extra": "^9.0.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "xml2js": "0.6.0" + } + }, + "node_modules/@expo/prebuild-config/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/prebuild-config/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/@expo/prebuild-config/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@expo/prebuild-config/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@expo/rudder-sdk-node": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@expo/rudder-sdk-node/-/rudder-sdk-node-1.1.1.tgz", + "integrity": "sha512-uy/hS/awclDJ1S88w9UGpc6Nm9XnNUjzOAAib1A3PVAnGQIwebg8DpFqOthFBTlZxeuV/BKbZ5jmTbtNZkp1WQ==", + "license": "MIT", + "dependencies": { + "@expo/bunyan": "^4.0.0", + "@segment/loosely-validate-event": "^2.0.0", + "fetch-retry": "^4.1.1", + "md5": "^2.2.1", + "node-fetch": "^2.6.1", + "remove-trailing-slash": "^0.1.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/sdk-runtime-versions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz", + "integrity": "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==", + "license": "MIT" + }, + "node_modules/@expo/spawn-async": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.8.0.tgz", + "integrity": "sha512-eb9xxd/LbuEGSdua4NumCu/McVB9EM+F/JxB9pWgnERw4HQ9XyTNH1KapG6oqLWR8TuRK2LQfzJlmNi94CVobw==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/sudo-prompt": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz", + "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==", + "license": "MIT" + }, + "node_modules/@expo/vector-icons": { + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-14.0.4.tgz", + "integrity": "sha512-+yKshcbpDfbV4zoXOgHxCwh7lkE9VVTT5T03OUlBsqfze1PLy6Hi4jp1vSb1GVbY6eskvMIivGVc9SKzIv0oEQ==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.8.1" + } + }, + "node_modules/@expo/ws-tunnel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz", + "integrity": "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==", + "license": "MIT" + }, + "node_modules/@expo/xcpretty": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.4.tgz", + "integrity": "sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "chalk": "^4.1.0", + "js-yaml": "^4.1.0" + }, + "bin": { + "excpretty": "build/cli.js" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@jest/transform/node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.5.tgz", + "integrity": "sha512-MN5dasWo37MirVcKWuysRkRr4BjNc81SXwUtJYstwbn8oEkfnwR9DaqdDTo/hHOnTdhafffLIa2xOOHcjDIGEw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.76.9", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.9.tgz", + "integrity": "sha512-vxL/vtDEIYHfWKm5oTaEmwcnNGsua/i9OjIxBDBFiJDu5i5RU3bpmDiXQm/bJxrJNPRp5lW0I0kpGihVhnMAIQ==", + "license": "MIT", + "dependencies": { + "@react-native/codegen": "0.76.9" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.76.9", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.9.tgz", + "integrity": "sha512-TbSeCplCM6WhL3hR2MjC/E1a9cRnMLz7i767T7mP90oWkklEjyPxWl+0GGoVGnJ8FC/jLUupg/HvREKjjif6lw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.76.9", + "babel-plugin-syntax-hermes-parser": "^0.25.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.76.9", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.9.tgz", + "integrity": "sha512-AzlCHMTKrAVC2709V4ZGtBXmGVtWTpWm3Ruv5vXcd3/anH4mGucfJ4rjbWKdaYQJMpXa3ytGomQrsIsT/s8kgA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.23.1", + "invariant": "^2.2.4", + "jscodeshift": "^0.14.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/@react-native/codegen/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "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", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.76.5.tgz", + "integrity": "sha512-3MKMnlU0cZOWlMhz5UG6WqACJiWUrE3XwBEumzbMmZw3Iw3h+fIsn+7kLLE5EhzqLt0hg5Y4cgYFi4kOaNgq+g==", + "license": "MIT", + "dependencies": { + "@react-native/dev-middleware": "0.76.5", + "@react-native/metro-babel-transformer": "0.76.5", + "chalk": "^4.0.0", + "execa": "^5.1.1", + "invariant": "^2.2.4", + "metro": "^0.81.0", + "metro-config": "^0.81.0", + "metro-core": "^0.81.0", + "node-fetch": "^2.2.0", + "readline": "^1.3.0", + "semver": "^7.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@react-native-community/cli-server-api": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli-server-api": { + "optional": true + } + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/debugger-frontend": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.5.tgz", + "integrity": "sha512-5gtsLfBaSoa9WP8ToDb/8NnDBLZjv4sybQQj7rDKytKOdsXm3Pr2y4D7x7GQQtP1ZQRqzU0X0OZrhRz9xNnOqA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/@react-native/dev-middleware": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.5.tgz", + "integrity": "sha512-f8eimsxpkvMgJia7POKoUu9uqjGF6KgkxX4zqr/a6eoR1qdEAWUd6PonSAqtag3PAqvEaJpB99gLH2ZJI1nDGg==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.76.5", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "selfsigned": "^2.4.1", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@react-native/community-cli-plugin/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@react-native/community-cli-plugin/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/@react-native/community-cli-plugin/node_modules/ws": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.6.tgz", + "integrity": "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.76.9", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.76.9.tgz", + "integrity": "sha512-0Ru72Bm066xmxFuOXhhvrryxvb57uI79yDSFf+hxRpktkC98NMuRenlJhslMrbJ6WjCu1vOe/9UjWNYyxXTRTA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.76.9", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.76.9.tgz", + "integrity": "sha512-xkd3C3dRcmZLjFTEAOvC14q3apMLouIvJViCZY/p1EfCMrNND31dgE1dYrLTiI045WAWMt5bD15i6f7dE2/QWA==", + "license": "MIT", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.76.9", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "selfsigned": "^2.4.1", + "serve-static": "^1.13.1", + "ws": "^6.2.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/@react-native/dev-middleware/node_modules/ws": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.6.tgz", + "integrity": "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.76.5.tgz", + "integrity": "sha512-7KSyD0g0KhbngITduC8OABn0MAlJfwjIdze7nA4Oe1q3R7qmAv+wQzW+UEXvPah8m1WqFjYTkQwz/4mK3XrQGw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.76.5.tgz", + "integrity": "sha512-ggM8tcKTcaqyKQcXMIvcB0vVfqr9ZRhWVxWIdiFO1mPvJyS6n+a+lLGkgQAyO8pfH0R1qw6K9D0nqbbDo865WQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/metro-babel-transformer": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/metro-babel-transformer/-/metro-babel-transformer-0.76.5.tgz", + "integrity": "sha512-Cm9G5Sg5BDty3/MKa3vbCAJtT3YHhlEaPlQALLykju7qBS+pHZV9bE9hocfyyvc5N/osTIGWxG5YOfqTeMu1oQ==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@react-native/babel-preset": "0.76.5", + "hermes-parser": "0.23.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-plugin-codegen": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.76.5.tgz", + "integrity": "sha512-xe7HSQGop4bnOLMaXt0aU+rIatMNEQbz242SDl8V9vx5oOTI0VbZV9yLy6yBc6poUlYbcboF20YVjoRsxX4yww==", + "license": "MIT", + "dependencies": { + "@react-native/codegen": "0.76.5" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/babel-preset": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.76.5.tgz", + "integrity": "sha512-1Nu5Um4EogOdppBLI4pfupkteTjWfmI0hqW8ezWTg7Bezw0FtBj8yS8UYVd3wTnDFT9A5mA2VNoNUqomJnvj2A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.76.5", + "babel-plugin-syntax-hermes-parser": "^0.25.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/@react-native/codegen": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.5.tgz", + "integrity": "sha512-FoZ9VRQ5MpgtDAnVo1rT9nNRfjnWpE40o1GeJSDlpUMttd36bVXvsDm8W/NhX8BKTWXSX+CPQJsRcvN1UPYGKg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.23.1", + "invariant": "^2.2.4", + "jscodeshift": "^0.14.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/@react-native/metro-babel-transformer/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "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", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.76.9", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.9.tgz", + "integrity": "sha512-TUdMG2JGk72M9d8DYbubdOlrzTYjw+YMe/xOnLU4viDgWRHsCbtRS9x0IAxRjs3amj/7zmK3Atm8jUPvdAc8qw==", + "license": "MIT" + }, + "node_modules/@react-native/virtualized-lists": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.76.5.tgz", + "integrity": "sha512-M/fW1fTwxrHbcx0OiVOIxzG6rKC0j9cR9Csf80o77y1Xry0yrNPpAlf8D1ev3LvHsiAUiRNFlauoPtodrs2J1A==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@react-navigation/core": { + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.21.11.tgz", + "integrity": "sha512-bCW1PsLA/eOXDOukcJFEzlcL3Zpy8DJuDCfkDDwAQlAgoSZ/J9+ZeDRUMmCUi6xbnFgvFEEIMertaLeErOFP0Q==", + "license": "MIT", + "dependencies": { + "@react-navigation/routers": "^7.6.4", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/elements": { + "version": "2.9.37", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.37.tgz", + "integrity": "sha512-M67E3ca9xTvx121SacdK/IN/HOUpZ17zAMx7nEBRQDYPMmcxlFRO9MX6xoVYcfvhZPPoLkrG/qqWUt6DUDxwMg==", + "license": "MIT", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.3.15", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.3.15", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.3.15.tgz", + "integrity": "sha512-qgYZJOZ0VLJHcA2svxsPcctk4vVmw5jDW07UXNTe06pPgkwxx08aUAOzRY/HzhmcE2SK5Cw1w8i1AwoyDaiyug==", + "license": "MIT", + "peer": true, + "dependencies": { + "@react-navigation/core": "^7.21.11", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "standard-navigation": "^0.0.8", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.18.7", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.18.7.tgz", + "integrity": "sha512-FXjyAKZhr7N4yMFi/abLR0I6Fe5jB2acfz1z3MlEM+QWlw5LLNpF2/25fnDOqogriYbsZglurSFx7gSdUzciZQ==", + "license": "MIT", + "dependencies": { + "@react-navigation/elements": "^2.9.37", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.3.15", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.6.4.tgz", + "integrity": "sha512-GI7eJm8/KsZUQaYcXvEExikKurRZRgEsSzyZ7faENfi65yqJBCXjDMwyN1pF6pNW1MoLH1ErDwDivFxY6BzD3w==", + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11" + } + }, + "node_modules/@segment/loosely-validate-event": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz", + "integrity": "sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw==", + "dependencies": { + "component-type": "^1.2.1", + "join-component": "^1.1.0" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.12", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.12.tgz", + "integrity": "sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==", + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@supabase/cli-darwin-arm64": { + "version": "2.111.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-darwin-arm64/-/cli-darwin-arm64-2.111.0.tgz", + "integrity": "sha512-H1ucZ+9Z37Ha7uqYrKHfAy1vXWMVsN4gNlKaOpjKUYoHwDEbueEHVIDA1/PBIUd4HX+usJfpq+R+gWqzM/FKqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@supabase/cli-darwin-x64": { + "version": "2.111.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-darwin-x64/-/cli-darwin-x64-2.111.0.tgz", + "integrity": "sha512-4iMYm/XaAZJ8YzdJ2HBRVc7i9SRIwE6VQrnSt968WTt83M1y6knC7UENCKjMtO+As4QeZkICpUk50CeathqxbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@supabase/cli-linux-arm64": { + "version": "2.111.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64/-/cli-linux-arm64-2.111.0.tgz", + "integrity": "sha512-2KSHITFMXe2u5yALupBWHGeQ1IY4C8GkcIWPWgNFdtAPo03pgs1hLWgL1eeqSh/a0wB/6rgUlM1fQR/hAgCyCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-linux-arm64-musl": { + "version": "2.111.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.111.0.tgz", + "integrity": "sha512-RnvbVlJ4TX/UIwLiglBVQ2eL4GAl9SfWZmM5LENXyIaohBcRZHDva5t2OyK+BPGBtngjVGpb3QyfLdvkt9yJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-linux-x64": { + "version": "2.111.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64/-/cli-linux-x64-2.111.0.tgz", + "integrity": "sha512-NwwNhiZZT4WYEPDXAbgZL+l77z/hoJ+w5t+52WBN1VlmoxwDmf2NP+UERM8wuCGFe/tmBlUuFE1eJYZfab0/qA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-linux-x64-musl": { + "version": "2.111.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-linux-x64-musl/-/cli-linux-x64-musl-2.111.0.tgz", + "integrity": "sha512-2QVpsy/3v+TzqE5GgTCPruoxTlF3d8QPGirjcnah8hP66nxXStB9yTvxmJq+LtO3gwMt1Kpm7is0roZVkV55Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@supabase/cli-windows-arm64": { + "version": "2.111.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-windows-arm64/-/cli-windows-arm64-2.111.0.tgz", + "integrity": "sha512-twawKY5xfU2dNOnZrKDmrudxRG/XIKcBxOb6X2lMGJU3N1Hd+8oWpI9TwM5Qv+eXehtlsKDmUvbitStXvJr/xQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@supabase/cli-windows-x64": { + "version": "2.111.0", + "resolved": "https://registry.npmjs.org/@supabase/cli-windows-x64/-/cli-windows-x64-2.111.0.tgz", + "integrity": "sha512-1F+X5tAYxAGx93ZZoIQBnIQ2Q2NnplKqjpigAS/zpTsNaWAjNi7EnxmuQKaEoAdqOHKbLhegVVDiw1+us3ZVpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", + "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "license": "MIT" + }, + "node_modules/@urql/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz", + "integrity": "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==", + "license": "MIT", + "dependencies": { + "@0no-co/graphql.web": "^1.0.13", + "wonka": "^6.3.2" + } + }, + "node_modules/@urql/exchange-retry": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-1.3.2.tgz", + "integrity": "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg==", + "license": "MIT", + "dependencies": { + "@urql/core": "^5.1.2", + "wonka": "^6.3.2" + }, + "peerDependencies": { + "@urql/core": "^5.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", + "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "deprecated": "this version has critical issues, please update to the latest version", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==", + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/babel-core": { + "version": "7.0.0-bridge.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-7.0.0-bridge.0.tgz", + "integrity": "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==", + "license": "MIT", + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-react-native-web": { + "version": "0.19.13", + "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.19.13.tgz", + "integrity": "sha512-4hHoto6xaN23LCyZgL9LJZc3olmAxd7b6jDzlZnKXAh4rRAbZRKNBJoOOdp46OBqgy+K0t0guTj5/mhA8inymQ==", + "license": "MIT" + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.25.1.tgz", + "integrity": "sha512-IVNpGzboFLfXZUAwkLFcI/bnqVbwky0jP3eBno4HKtqvQJAHBLdgxiG6lQ4to0+Q/YCN3PO0od5NZwIKyY4REQ==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.25.1" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-expo": { + "version": "12.0.12", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-12.0.12.tgz", + "integrity": "sha512-qAuaGGZIN//DyQVackP7Czr1SMq5dYb5tpu/uQqL/f1bRARb74r+kBWQRLJGxQ3QujsEw13SyAodCZIOUoD6KQ==", + "license": "MIT", + "dependencies": { + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-transform-export-namespace-from": "^7.22.11", + "@babel/plugin-transform-object-rest-spread": "^7.12.13", + "@babel/plugin-transform-parameters": "^7.22.15", + "@babel/preset-react": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-preset": "0.76.9", + "babel-plugin-react-native-web": "~0.19.13", + "react-refresh": "^0.14.2" + }, + "peerDependencies": { + "babel-plugin-react-compiler": "^19.0.0-beta-9ee70a1-20241017", + "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020" + }, + "peerDependenciesMeta": { + "babel-plugin-react-compiler": { + "optional": true + }, + "react-compiler-runtime": { + "optional": true + } + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/baseline-browser-mapping": { + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "license": "MIT", + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/better-opn/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/bplist-creator": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", + "integrity": "sha512-xp/tcaV3T5PCiaY04mXga7o/TE+t95gqeLmADeBI1CvZtdWTbgBt3uLpvh4UWtenKeBhCV6oVxGk38yZr2uYEA==", + "license": "MIT", + "dependencies": { + "stream-buffers": "~2.2.0" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz", + "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "license": "MIT" + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "license": "MIT" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", + "license": "MIT", + "dependencies": { + "callsites": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", + "license": "MIT", + "dependencies": { + "caller-callsite": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001807", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001807.tgz", + "integrity": "sha512-daRXJ9EB/rdRgu7kV+TTl1YUKtlsMWblPl2sLnpg9DZae16QCegol6A1SmCE31Lm9mXC1sRWGt/krouH+/dl7Q==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/chromium-edge-launcher/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, + "node_modules/component-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/component-type/-/component-type-1.2.2.tgz", + "integrity": "sha512-99VUHREHiN5cLeHm3YLq312p6v+HUEcwtLCAtelvUDI6+SH5g5Cr85oNR2S1o6ywzL0ykMbuwLzM2ANocjEOIA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", + "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^1.0.0", + "ip-regex": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/del": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", + "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", + "license": "MIT", + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/eciesjs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.5.0.tgz", + "integrity": "sha512-s0J9SEVYAEPg7J63GFMApLYzPH9VNIQIyC6s15JpnqVc0TqcKWdbgFlnAweEBRyMmko2dcs2sfC83Hj4J43tuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ecies/ciphers": "^0.2.6", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "^1.9.7", + "@noble/hashes": "^1.8.0" + }, + "engines": { + "bun": ">=1", + "deno": ">=2.7.10", + "node": ">=16" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.402", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz", + "integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-editor": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", + "integrity": "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/expo": { + "version": "52.0.49", + "resolved": "https://registry.npmjs.org/expo/-/expo-52.0.49.tgz", + "integrity": "sha512-ge3gUnuyGEePWWKzPY7TQ7FsvtFTdmsdYDHeBVUjMr9KIoQig/gf8A03oH26p3UtTL6sUJcyOIg9vwIHGNPSUw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.20.0", + "@expo/cli": "0.22.28", + "@expo/config": "~10.0.11", + "@expo/config-plugins": "~9.0.17", + "@expo/fingerprint": "0.11.11", + "@expo/metro-config": "0.19.12", + "@expo/vector-icons": "~14.0.4", + "babel-preset-expo": "~12.0.12", + "expo-asset": "~11.0.5", + "expo-constants": "~17.0.8", + "expo-file-system": "~18.0.12", + "expo-font": "~13.0.4", + "expo-keep-awake": "~14.0.3", + "expo-modules-autolinking": "2.0.8", + "expo-modules-core": "2.2.3", + "fbemitter": "^3.0.0", + "web-streams-polyfill": "^3.3.2", + "whatwg-url-without-unicode": "8.0.0-3" + }, + "bin": { + "expo": "bin/cli", + "expo-modules-autolinking": "bin/autolinking", + "fingerprint": "bin/fingerprint" + }, + "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 + } + } + }, + "node_modules/expo-asset": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-11.0.5.tgz", + "integrity": "sha512-TL60LmMBGVzs3NQcO8ylWqBumMh4sx0lmeJsn7+9C88fylGDhyyVnKZ1PyTXo9CVDBkndutZx2JUEQWM9BaiXw==", + "license": "MIT", + "dependencies": { + "@expo/image-utils": "^0.6.5", + "expo-constants": "~17.0.8", + "invariant": "^2.2.4", + "md5-file": "^3.2.3" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-constants": { + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-17.0.8.tgz", + "integrity": "sha512-XfWRyQAf1yUNgWZ1TnE8pFBMqGmFP5Gb+SFSgszxDdOoheB/NI5D4p7q86kI2fvGyfTrxAe+D+74nZkfsGvUlg==", + "license": "MIT", + "dependencies": { + "@expo/config": "~10.0.11", + "@expo/env": "~0.4.2" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo-document-picker": { + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-13.0.3.tgz", + "integrity": "sha512-348xcsiA/YhgWm1SuJNNdb5cUDpRJYCyIk8MhOU2MEDxbVRR+Q1TiUBTCIMVqaWHcxsFQzP56Wwv9n24qjeILg==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-linear-gradient": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz", + "integrity": "sha512-nvac1sPUfFFJ4mY25UkvubpUV/olrBH+uQw5k+beqSvQaVQiUfFtYzfRr+6HhYBNb4AEsOtpsCRkpDww3M2iGQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-modules-autolinking": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.0.8.tgz", + "integrity": "sha512-DezgnEYFQYic8hKGhkbztBA3QUmSftjaNDIKNAtS2iGJmzCcNIkatjN2slFDSWjSTNo8gOvPQyMKfyHWFvLpOQ==", + "license": "MIT", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "fast-glob": "^3.2.5", + "find-up": "^5.0.0", + "fs-extra": "^9.1.0", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "expo-modules-autolinking": "bin/expo-modules-autolinking.js" + } + }, + "node_modules/expo-modules-autolinking/node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/expo-modules-autolinking/node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/expo-modules-autolinking/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/expo-modules-core": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-2.2.3.tgz", + "integrity": "sha512-01QqZzpP/wWlxnNly4G06MsOBUTbMDj02DQigZoXfDh80vd/rk3/uVXqnZgOdLSggTs6DnvOgAUy0H2q30XdUg==", + "license": "MIT", + "dependencies": { + "invariant": "^2.2.4" + } + }, + "node_modules/expo-status-bar": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-2.0.1.tgz", + "integrity": "sha512-AkIPX7jWHRPp83UBZ1iXtVvyr0g+DgBVvIXTtlmPtmUsm8Vq9Bb5IGj86PW8osuFlgoTVAg7HI/+Ok7yEYwiRg==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/expo-file-system": { + "version": "18.0.12", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-18.0.12.tgz", + "integrity": "sha512-HAkrd/mb8r+G3lJ9MzmGeuW2B+BxQR1joKfeCyY4deLl1zoZ48FrAWjgZjHK9aHUVhJ0ehzInu/NQtikKytaeg==", + "license": "MIT", + "dependencies": { + "web-streams-polyfill": "^3.3.2" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/expo-font": { + "version": "13.0.4", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-13.0.4.tgz", + "integrity": "sha512-eAP5hyBgC8gafFtprsz0HMaB795qZfgJWqTmU0NfbSin1wUuVySFMEPMOrTkTgmazU73v4Cb4x7p86jY1XXYUw==", + "license": "MIT", + "dependencies": { + "fontfaceobserver": "^2.1.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo/node_modules/expo-keep-awake": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-14.0.3.tgz", + "integrity": "sha512-6Jh94G6NvTZfuLnm2vwIpKe3GdOiVBuISl7FI8GqN0/9UOg9E0WXXp5cDcfAG8bn80RfgLJS8P7EPUGTZyOvhg==", + "license": "MIT", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-loops": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-loops/-/fast-loops-1.1.4.tgz", + "integrity": "sha512-8dbd3XWoKCTms18ize6JmQF1SFnnfj5s0B7rRry22EofgMu7B6LKHVh+XfFqFGsqnbH54xgeO83PzpKI+ODhlg==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fbemitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fbemitter/-/fbemitter-3.0.0.tgz", + "integrity": "sha512-KWKaceCwKQU0+HPoop6gn4eOHk50bBv/VxjJtGMfwmJt3D29JpN4H4eisCtIPA+a8GVBam+ldMMpMjJUvpDyHw==", + "license": "BSD-3-Clause", + "dependencies": { + "fbjs": "^3.0.0" + } + }, + "node_modules/fbjs": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-3.0.5.tgz", + "integrity": "sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==", + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.1.5", + "fbjs-css-vars": "^1.0.0", + "loose-envify": "^1.0.0", + "object-assign": "^4.1.0", + "promise": "^7.1.1", + "setimmediate": "^1.0.5", + "ua-parser-js": "^1.0.35" + } + }, + "node_modules/fbjs-css-vars": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/fbjs-css-vars/-/fbjs-css-vars-1.0.2.tgz", + "integrity": "sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==", + "license": "MIT" + }, + "node_modules/fetch-retry": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-4.1.1.tgz", + "integrity": "sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA==", + "license": "MIT" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==", + "license": "MIT" + }, + "node_modules/flow-estree": { + "version": "0.326.0", + "resolved": "https://registry.npmjs.org/flow-estree/-/flow-estree-0.326.0.tgz", + "integrity": "sha512-43Qv+Ei9qfabhLx8JGEJku4frHGD5zI2EuL73Hs9HrWqPMbTT/DS18Az7bcodGwdQEv1DVIEI2WowkEbMIk4BQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/flow-parser": { + "version": "0.326.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.326.0.tgz", + "integrity": "sha512-H/Wqt2SDkQ8GH8wpyAj434zN40zomipZWBbKYlAwQYyNdMIQMKqpXTx82FMnITt2iDQ5zrV80NvSPAgIAafwGA==", + "license": "MIT", + "dependencies": { + "flow-estree": "0.326.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fontfaceobserver": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", + "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==", + "license": "BSD-2-Clause" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz", + "integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/freeport-async": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", + "integrity": "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/getenv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz", + "integrity": "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/gifted-charts-core": { + "version": "0.1.81", + "resolved": "https://registry.npmjs.org/gifted-charts-core/-/gifted-charts-core-0.1.81.tgz", + "integrity": "sha512-plgJSbKB0Lxp2KQ/Fvj1qbOhiy6wxPiZ0Av60iFHpSSu6YlJjYhwczx5w2/iJQdZYb851OFMHgN/pgTNVLv6dA==", + "license": "MIT", + "peerDependencies": { + "react": "*", + "react-native": "*", + "react-native-svg": "*" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "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", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.23.1.tgz", + "integrity": "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg==", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.23.1.tgz", + "integrity": "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.23.1" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", + "license": "MIT", + "dependencies": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/inline-style-prefixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-6.0.4.tgz", + "integrity": "sha512-FwXmZC2zbeeS7NzGjJ6pAiqRhXR0ugUShSNb6GApMl6da0/XGc4MOJsoWAywia52EEWbXNSy0pzkwz/+Y+swSg==", + "license": "MIT", + "dependencies": { + "css-in-js-utils": "^3.1.0", + "fast-loops": "^1.1.3" + } + }, + "node_modules/internal-ip": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", + "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "license": "MIT", + "dependencies": { + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/ip-regex": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", + "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jimp-compact": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/jimp-compact/-/jimp-compact-0.16.1.tgz", + "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==", + "license": "MIT" + }, + "node_modules/join-component": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz", + "integrity": "sha512-bF7vcQxbODoGK1imE2P9GS9aw4zD0Sd+Hni68IMZLj7zRnquH7dXUmMw9hDI5S/Jzt7q+IyTXN0rSg2GI0IKhQ==", + "license": "MIT" + }, + "node_modules/jose": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", + "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-android": { + "version": "250231.0.0", + "resolved": "https://registry.npmjs.org/jsc-android/-/jsc-android-250231.0.0.tgz", + "integrity": "sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw==", + "license": "BSD-2-Clause" + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==", + "license": "0BSD" + }, + "node_modules/jscodeshift": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-0.14.0.tgz", + "integrity": "sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.13.16", + "@babel/parser": "^7.13.16", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-optional-chaining": "^7.13.12", + "@babel/plugin-transform-modules-commonjs": "^7.13.8", + "@babel/preset-flow": "^7.13.13", + "@babel/preset-typescript": "^7.13.0", + "@babel/register": "^7.13.16", + "babel-core": "^7.0.0-bridge.0", + "chalk": "^4.1.2", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.4", + "neo-async": "^2.5.0", + "node-dir": "^0.1.17", + "recast": "^0.21.0", + "temp": "^0.8.4", + "write-file-atomic": "^2.3.0" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.27.0.tgz", + "integrity": "sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.27.0", + "lightningcss-darwin-x64": "1.27.0", + "lightningcss-freebsd-x64": "1.27.0", + "lightningcss-linux-arm-gnueabihf": "1.27.0", + "lightningcss-linux-arm64-gnu": "1.27.0", + "lightningcss-linux-arm64-musl": "1.27.0", + "lightningcss-linux-x64-gnu": "1.27.0", + "lightningcss-linux-x64-musl": "1.27.0", + "lightningcss-win32-arm64-msvc": "1.27.0", + "lightningcss-win32-x64-msvc": "1.27.0" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.27.0.tgz", + "integrity": "sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.27.0.tgz", + "integrity": "sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.27.0.tgz", + "integrity": "sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.27.0.tgz", + "integrity": "sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.27.0.tgz", + "integrity": "sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.27.0.tgz", + "integrity": "sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.27.0.tgz", + "integrity": "sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.27.0.tgz", + "integrity": "sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.27.0.tgz", + "integrity": "sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.27.0.tgz", + "integrity": "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "license": "MIT", + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/log-symbols/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/md5-file": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/md5-file/-/md5-file-3.2.3.tgz", + "integrity": "sha512-3Tkp1piAHaworfcCgH0jKbTvj1jWWFgbvh2cXaNCgHwyTCBxxvD1Y04rmfpvdPm1P4oXMOpm6+2H7sr7v9v8Fw==", + "license": "MIT", + "dependencies": { + "buffer-alloc": "^1.1.0" + }, + "bin": { + "md5-file": "cli.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/metro": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.81.5.tgz", + "integrity": "sha512-YpFF0DDDpDVygeca2mAn7K0+us+XKmiGk4rIYMz/CRdjFoCGqAei/IQSpV0UrGfQbToSugpMQeQJveaWSH88Hg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "@babel/types": "^7.25.2", + "accepts": "^1.3.7", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^2.2.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.25.1", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.81.5", + "metro-cache": "0.81.5", + "metro-cache-key": "0.81.5", + "metro-config": "0.81.5", + "metro-core": "0.81.5", + "metro-file-map": "0.81.5", + "metro-resolver": "0.81.5", + "metro-runtime": "0.81.5", + "metro-source-map": "0.81.5", + "metro-symbolicate": "0.81.5", + "metro-transform-plugins": "0.81.5", + "metro-transform-worker": "0.81.5", + "mime-types": "^2.1.27", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.81.5.tgz", + "integrity": "sha512-oKCQuajU5srm+ZdDcFg86pG/U8hkSjBlkyFjz380SZ4TTIiI5F+OQB830i53D8hmqmcosa4wR/pnKv8y4Q3dLw==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.25.1", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-babel-transformer/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/metro-babel-transformer/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/metro-cache": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.81.5.tgz", + "integrity": "sha512-wOsXuEgmZMZ5DMPoz1pEDerjJ11AuMy9JifH4yNW7NmWS0ghCRqvDxk13LsElzLshey8C+my/tmXauXZ3OqZgg==", + "license": "MIT", + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "metro-core": "0.81.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-cache-key": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.81.5.tgz", + "integrity": "sha512-lGWnGVm1UwO8faRZ+LXQUesZSmP1LOg14OVR+KNPBip8kbMECbQJ8c10nGesw28uQT7AE0lwQThZPXlxDyCLKQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-config": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.81.5.tgz", + "integrity": "sha512-oDRAzUvj6RNRxratFdcVAqtAsg+T3qcKrGdqGZFUdwzlFJdHGR9Z413sW583uD2ynsuOjA2QB6US8FdwiBdNKg==", + "license": "MIT", + "dependencies": { + "connect": "^3.6.5", + "cosmiconfig": "^5.0.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.81.5", + "metro-cache": "0.81.5", + "metro-core": "0.81.5", + "metro-runtime": "0.81.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-core": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.81.5.tgz", + "integrity": "sha512-+2R0c8ByfV2N7CH5wpdIajCWa8escUFd8TukfoXyBq/vb6yTCsznoA25FhNXJ+MC/cz1L447Zj3vdUfCXIZBwg==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.81.5" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-file-map": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.81.5.tgz", + "integrity": "sha512-mW1PKyiO3qZvjeeVjj1brhkmIotObA3/9jdbY1fQQYvEWM6Ml7bN/oJCRDGn2+bJRlG+J8pwyJ+DgdrM4BsKyg==", + "license": "MIT", + "dependencies": { + "debug": "^2.2.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-file-map/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/metro-file-map/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/metro-minify-terser": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.81.5.tgz", + "integrity": "sha512-/mn4AxjANnsSS3/Bb+zA1G5yIS5xygbbz/OuPaJYs0CPcZCaWt66D+65j4Ft/nJkffUxcwE9mk4ubpkl3rjgtw==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-resolver": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.81.5.tgz", + "integrity": "sha512-6BX8Nq3g3go3FxcyXkVbWe7IgctjDTk6D9flq+P201DfHHQ28J+DWFpVelFcrNTn4tIfbP/Bw7u/0g2BGmeXfQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-runtime": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.81.5.tgz", + "integrity": "sha512-M/Gf71ictUKP9+77dV/y8XlAWg7xl76uhU7ggYFUwEdOHHWPG6gLBr1iiK0BmTjPFH8yRo/xyqMli4s3oGorPQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-source-map": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.81.5.tgz", + "integrity": "sha512-Jz+CjvCKLNbJZYJTBeN3Kq9kIJf6b61MoLBdaOQZJ5Ajhw6Pf95Nn21XwA8BwfUYgajsi6IXsp/dTZsYJbN00Q==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.81.5", + "nullthrows": "^1.1.1", + "ob1": "0.81.5", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.81.5.tgz", + "integrity": "sha512-X3HV3n3D6FuTE11UWFICqHbFMdTavfO48nXsSpnNGFkUZBexffu0Xd+fYKp+DJLNaQr3S+lAs8q9CgtDTlRRuA==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.81.5", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.81.5.tgz", + "integrity": "sha512-MmHhVx/1dJC94FN7m3oHgv5uOjKH8EX8pBeu1pnPMxbJrx6ZuIejO0k84zTSaQTZ8RxX1wqwzWBpXAWPjEX8mA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.81.5.tgz", + "integrity": "sha512-lUFyWVHa7lZFRSLJEv+m4jH8WrR5gU7VIjUlg4XmxQfV8ngY4V10ARKynLhMYPeQGl7Qvf+Ayg0eCZ272YZ4Mg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "metro": "0.81.5", + "metro-babel-transformer": "0.81.5", + "metro-cache": "0.81.5", + "metro-cache-key": "0.81.5", + "metro-minify-terser": "0.81.5", + "metro-source-map": "0.81.5", + "metro-transform-plugins": "0.81.5", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/metro/node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "license": "MIT" + }, + "node_modules/metro/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/metro/node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/metro/node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/metro/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/metro/node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/nested-error-stacks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", + "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==", + "license": "MIT" + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "license": "MIT" + }, + "node_modules/node-dir": { + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", + "integrity": "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==", + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.10.5" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", + "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "license": "ISC", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", + "license": "MIT" + }, + "node_modules/ob1": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.81.5.tgz", + "integrity": "sha512-iNpbeXPLmaiT9I5g16gFFFjsF3sGxLpYG2EGP3dfFB4z+l9X60mp/yRzStHhMtuNt8qmf7Ww80nOPQHngHhnIQ==", + "license": "MIT", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=18.18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/ora/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/ora/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "license": "MIT", + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/parse-png": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz", + "integrity": "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==", + "license": "MIT", + "dependencies": { + "pngjs": "^3.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.2.tgz", + "integrity": "sha512-cfDHL6LStTEKlNilboNtobT/kEa30PtAf2Q1OgszfrG/rpVl1xaFWT9ktfkS306GmHgmnad1Sw4wabhlvFtsTw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/plist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz", + "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==", + "license": "MIT", + "dependencies": { + "@xmldom/xmldom": "^0.9.10", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/plist/node_modules/@xmldom/xmldom": { + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", + "license": "MIT", + "engines": { + "node": ">=14.6" + } + }, + "node_modules/plist/node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode-terminal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", + "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-devtools-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-5.3.2.tgz", + "integrity": "sha512-crr9HkVrDiJ0A4zot89oS0Cgv0Oa4OG1Em4jit3P3ZxZSKPMYyMjfwMqgcJna9o625g8oN87rBm8SWWrSTBZxg==", + "license": "MIT", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-devtools-core/node_modules/ws": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/react-native": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.5.tgz", + "integrity": "sha512-op2p2kB+lqMF1D7AdX4+wvaR0OPFbvWYs+VBE7bwsb99Cn9xISrLRLAgFflZedQsa5HvnOGrULhtnmItbIKVVw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/create-cache-key-function": "^29.6.3", + "@react-native/assets-registry": "0.76.5", + "@react-native/codegen": "0.76.5", + "@react-native/community-cli-plugin": "0.76.5", + "@react-native/gradle-plugin": "0.76.5", + "@react-native/js-polyfills": "0.76.5", + "@react-native/normalize-colors": "0.76.5", + "@react-native/virtualized-lists": "0.76.5", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "^0.23.1", + "base64-js": "^1.5.1", + "chalk": "^4.0.0", + "commander": "^12.0.0", + "event-target-shim": "^5.0.1", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.6.3", + "jsc-android": "^250231.0.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.81.0", + "metro-source-map": "^0.81.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^5.3.1", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.24.0-canary-efb381bbf-20230505", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/react": "^18.2.6", + "react": "^18.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-native-gifted-charts": { + "version": "1.4.77", + "resolved": "https://registry.npmjs.org/react-native-gifted-charts/-/react-native-gifted-charts-1.4.77.tgz", + "integrity": "sha512-Ul4juHO0Gicng139i61AzQ3h4kLM25dzid1rU+d3d7PuHsI4UypgeChz/luaHMZaWgXkALjq6DLqaM2Y2AEhrA==", + "license": "MIT", + "dependencies": { + "gifted-charts-core": "0.1.81" + }, + "peerDependencies": { + "expo-linear-gradient": "*", + "react": "*", + "react-native": "*", + "react-native-linear-gradient": "*", + "react-native-svg": "*" + }, + "peerDependenciesMeta": { + "expo-linear-gradient": { + "optional": true + }, + "react-native-linear-gradient": { + "optional": true + } + } + }, + "node_modules/react-native-safe-area-context": { + "version": "5.8.1", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.8.1.tgz", + "integrity": "sha512-dDYVAJcW5LJlXAqTUFAaWvYJovqxrh6HdJlOkdTQ7dXCcKWhCq5vKoDXJc3n1q5rYcAUaIwmToKNPKibnogC4g==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.26.2.tgz", + "integrity": "sha512-2XnWsZToKj76trGtEZzx5ELD/qOICFEprEeUntImmitQFVUkea27fiWdUSITArI356Y1qynpXZINW+Unbhky/A==", + "license": "MIT", + "peer": true, + "dependencies": { + "react-freeze": "^1.0.0", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-svg": { + "version": "15.15.5", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.5.tgz", + "integrity": "sha512-L4go5jA+GWutdJ/JucuN20cjAbMg1HmMtAP+wZ+3JLCf6Jd0bhXQHxciRP/AQm/FlrIEZwkMcHNZP+FXAiic0w==", + "license": "MIT", + "peer": true, + "dependencies": { + "css-select": "^5.1.0", + "css-tree": "^1.1.3" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-web": { + "version": "0.19.13", + "resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz", + "integrity": "sha512-etv3bN8rJglrRCp/uL4p7l8QvUNUC++QwDbdZ8CB7BvZiMvsxfFIRM1j04vxNldG3uo2puRd6OSWR3ibtmc29A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@react-native/normalize-colors": "^0.74.1", + "fbjs": "^3.0.4", + "inline-style-prefixer": "^6.0.1", + "memoize-one": "^6.0.0", + "nullthrows": "^1.1.1", + "postcss-value-parser": "^4.2.0", + "styleq": "^0.1.3" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/react-native-web/node_modules/@react-native/normalize-colors": { + "version": "0.74.89", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.74.89.tgz", + "integrity": "sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==", + "license": "MIT" + }, + "node_modules/react-native-web/node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/react-native/node_modules/@react-native/codegen": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.76.5.tgz", + "integrity": "sha512-FoZ9VRQ5MpgtDAnVo1rT9nNRfjnWpE40o1GeJSDlpUMttd36bVXvsDm8W/NhX8BKTWXSX+CPQJsRcvN1UPYGKg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.23.1", + "invariant": "^2.2.4", + "jscodeshift": "^0.14.0", + "mkdirp": "^0.5.1", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + } + }, + "node_modules/react-native/node_modules/@react-native/normalize-colors": { + "version": "0.76.5", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.76.5.tgz", + "integrity": "sha512-6QRLEok1r55gLqj+94mEWUENuU5A6wsr2OoXpyq/CgQ7THWowbHtru/kRGRr6o3AQXrVnZheR60JNgFcpNYIug==", + "license": "MIT" + }, + "node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.23.1.tgz", + "integrity": "sha512-uNLD0tk2tLUjGFdmCk+u/3FEw2o+BAwW4g+z2QVlxJrzZYOOPADroEcNtTPt5lNiScctaUmnsTkVEnOwZUOLhA==", + "license": "MIT", + "dependencies": { + "hermes-parser": "0.23.1" + } + }, + "node_modules/react-native/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "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", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-native/node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/react-native/node_modules/scheduler": { + "version": "0.24.0-canary-efb381bbf-20230505", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.24.0-canary-efb381bbf-20230505.tgz", + "integrity": "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/react-native/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/react-native/node_modules/ws": { + "version": "6.2.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.6.tgz", + "integrity": "sha512-XTrf1gv7kXoVf1hbC3PAyAiPgR8Wz1blcrYIjEsUmr08BLksT41R8KbjmS9408C2ERx7v1JDLD/BkpLEttjfKA==", + "license": "MIT", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readline": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", + "integrity": "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==", + "license": "BSD" + }, + "node_modules/recast": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.21.5.tgz", + "integrity": "sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==", + "license": "MIT", + "dependencies": { + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/remove-trailing-slash": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz", + "integrity": "sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireg": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz", + "integrity": "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==", + "dependencies": { + "nested-error-stacks": "~2.0.1", + "rc": "~1.2.7", + "resolve": "~1.7.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/requireg/node_modules/resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "license": "MIT", + "dependencies": { + "path-parse": "^1.0.5" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-workspace-root": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz", + "integrity": "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==", + "license": "MIT" + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "license": "MIT", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "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", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "license": "MIT", + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sf-symbols-typescript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz", + "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "license": "MIT", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-plist/node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "license": "MIT", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/simple-plist/node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "license": "MIT", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slugify": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.9.tgz", + "integrity": "sha512-vZ7rfeehZui7wQs438JXBckYLkIIdfHOXsaVEUMyS5fHo1483l1bMdo0EDSWYclY0yZKFOipDy4KHuKs6ssvdg==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "license": "MIT", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/standard-navigation": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/standard-navigation/-/standard-navigation-0.0.8.tgz", + "integrity": "sha512-TyVbo7INUDWtsUWDFn8RR7kwR87U0S4xHfLfbbnyeC581TmmyqQ+eM+nPw8rQTSD8QitRVcYfPaSHr/QJiUy1g==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "license": "Unlicense", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-headers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", + "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==", + "license": "MIT" + }, + "node_modules/styleq": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz", + "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==", + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supabase": { + "version": "2.111.0", + "resolved": "https://registry.npmjs.org/supabase/-/supabase-2.111.0.tgz", + "integrity": "sha512-0cjCRdYNV1h2XXa0wm04mdct7QuDU7sMul/NwETRJmN3+HCsdEu6u5n0oygL97fdf6sDrGmnAdBh8E4wsv1ayg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eciesjs": "^0.5.0", + "jose": "^6.2.3" + }, + "bin": { + "supabase": "dist/supabase.js" + }, + "optionalDependencies": { + "@supabase/cli-darwin-arm64": "2.111.0", + "@supabase/cli-darwin-x64": "2.111.0", + "@supabase/cli-linux-arm64": "2.111.0", + "@supabase/cli-linux-arm64-musl": "2.111.0", + "@supabase/cli-linux-x64": "2.111.0", + "@supabase/cli-linux-x64-musl": "2.111.0", + "@supabase/cli-windows-arm64": "2.111.0", + "@supabase/cli-windows-x64": "2.111.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/temp": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.8.4.tgz", + "integrity": "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg==", + "license": "MIT", + "dependencies": { + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/temp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "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", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/tempy": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.7.1.tgz", + "integrity": "sha512-vXPxwOyaNVi9nyczO16mxmHGpl6ASC5/TVhRRHpqeYHvKQm58EaWNvZXxAhR0lYYnBOQFjXjhzeLsaXdjxLjRg==", + "license": "MIT", + "dependencies": { + "del": "^6.0.0", + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.49.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.2.tgz", + "integrity": "sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "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", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "license": "MIT" + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=8" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici": { + "version": "6.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", + "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "license": "MIT" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "license": "ISC", + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz", + "integrity": "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", + "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "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).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==", + "license": "MIT" + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==", + "license": "MIT" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/whatwg-url-without-unicode": { + "version": "8.0.0-3", + "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", + "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", + "license": "MIT", + "dependencies": { + "buffer": "^5.4.3", + "punycode": "^2.1.1", + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wonka": { + "version": "6.3.6", + "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.6.tgz", + "integrity": "sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", + "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "node_modules/write-file-atomic/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.21.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.2.tgz", + "integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==", + "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 + } + } + }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "license": "Apache-2.0", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xcode/node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "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).", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/xml2js": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", + "integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-14.0.0.tgz", + "integrity": "sha512-ts+B2rSe4fIckR6iquDjsKbQFK2NlUk6iG5nf14mDEyldgoc2nEKZ3jZWMPTxGQwVgToSjt6VGIho1H8/fNFTg==", + "license": "MIT", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/app/package.json b/app/package.json index 098239c..59b9f08 100644 --- a/app/package.json +++ b/app/package.json @@ -6,13 +6,16 @@ "start": "expo start", "android": "expo run:android", "ios": "expo run:ios", - "web": "expo start --web" + "web": "expo start --web", + "build:web": "expo export -p web", + "test:ci": "npm run build:web" }, "dependencies": { "@expo/metro-runtime": "~4.0.1", "@react-navigation/native": "^7.1.28", "@react-navigation/native-stack": "^7.10.1", "expo": "~52.0.0", + "expo-asset": "~11.0.5", "expo-document-picker": "~13.0.3", "expo-linear-gradient": "~14.0.2", "expo-status-bar": "~2.0.0", diff --git a/app/screens/PlantDetailScreen.js b/app/screens/PlantDetailScreen.js index debf2e2..324b444 100644 --- a/app/screens/PlantDetailScreen.js +++ b/app/screens/PlantDetailScreen.js @@ -8,11 +8,10 @@ import { ScrollView, Alert, useWindowDimensions, - Modal, } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { BarChart } from 'react-native-gifted-charts'; -import * as DocumentPicker from 'expo-document-picker'; +import UploadModal from '../components/UploadModal'; const API_BASE_URL = 'https://solorpower.dadot.net'; @@ -21,7 +20,7 @@ export default function PlantDetailScreen({ route, navigation }) { const [period, setPeriod] = useState('today'); const [chartData, setChartData] = useState([]); const [loading, setLoading] = useState(true); - const [uploading, setUploading] = useState(false); + const [uploadVisible, setUploadVisible] = useState(false); const [error, setError] = useState(null); const [todayData, setTodayData] = useState(null); const [tooltip, setTooltip] = useState({ visible: false, x: 0, y: 0, label: '', value: 0 }); @@ -270,59 +269,6 @@ export default function PlantDetailScreen({ route, navigation }) { return chartData.reduce((sum, item) => sum + (item.value || 0), 0); }; - // 엑셀 업로드 핸들러 - const handleUpload = async () => { - try { - const result = await DocumentPicker.getDocumentAsync({ - type: [ - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.ms-excel', - ], - copyToCacheDirectory: true, - }); - - if (result.canceled) return; - - const file = result.assets[0]; - setUploading(true); - - const formData = new FormData(); - - if (file.file) { - // Web: 실제 File 객체 사용 - formData.append('file', file.file); - } else { - // Native: URI 객체 사용 - formData.append('file', { - uri: file.uri, - type: file.mimeType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - name: file.name, - }); - } - - const response = await fetch(`${API_BASE_URL}/plants/${plant.id}/upload/monthly`, { - method: 'POST', - body: formData, - }); - - const responseData = await response.json(); - - if (!response.ok) throw new Error(responseData.detail || '업로드 실패'); - - Alert.alert( - '업로드 완료', - responseData.message || `${responseData.saved_count}건의 데이터가 저장되었습니다.`, - [{ text: '확인' }] - ); - - fetchStats(); - } catch (err) { - Alert.alert('업로드 실패', err.message, [{ text: '확인' }]); - } finally { - setUploading(false); - } - }; - // 탭 렌더링 const renderTabs = () => ( @@ -497,21 +443,22 @@ export default function PlantDetailScreen({ route, navigation }) { {/* 업로드 버튼 */} setUploadVisible(true)} > - {uploading ? ( - - ) : ( - 📂 과거 엑셀 데이터 업로드 - )} + 📂 과거 엑셀 데이터 업로드 - * 엑셀 파일에 'date', 'generation' 컬럼이 필요합니다 + * 일간 또는 월간 형식을 선택할 수 있습니다 + setUploadVisible(false)} + plantId={plant.id} + onUploadSuccess={fetchStats} + /> ); } @@ -775,9 +722,6 @@ const styles = StyleSheet.create({ shadowRadius: 8, elevation: 6, }, - uploadButtonDisabled: { - backgroundColor: '#9CA3AF', - }, uploadButtonText: { color: '#FFFFFF', fontSize: 16, diff --git a/crawler/alert_manager.py b/crawler/alert_manager.py index f2956e5..96af123 100644 --- a/crawler/alert_manager.py +++ b/crawler/alert_manager.py @@ -1,9 +1,11 @@ import sqlite3 +from contextlib import closing + import requests -from datetime import datetime from pathlib import Path from config import TELEGRAM_BOT_TOKEN +from time_utils import ensure_kst, now_kst class AlertManager: """ @@ -11,7 +13,7 @@ class AlertManager: - 상태(정상/이상)를 DB에 저장하여 중복 알림 방지 """ - def __init__(self, db_path: str = None): + def __init__(self, db_path: str = None, now_provider=None): """ DB 연결 및 테이블 초기화 """ @@ -20,11 +22,15 @@ class AlertManager: db_path = Path(__file__).parent / "crawler_manager.db" self.db_path = str(db_path) + self.zero_threshold = 3 + self.today_growth_threshold = 0.5 + self.now_provider = now_provider or now_kst + self._alerts_enabled_cache = {} self._init_db() def _init_db(self): """알림 히스토리 테이블 생성""" - with sqlite3.connect(self.db_path) as conn: + with closing(sqlite3.connect(self.db_path)) as conn: cursor = conn.cursor() # site_id: 발전소 ID # alert_status: 'NORMAL' (정상), 'ALERT' (이상 발생 및 알림 전송됨) @@ -33,10 +39,63 @@ class AlertManager: CREATE TABLE IF NOT EXISTS alert_history ( site_id TEXT PRIMARY KEY, alert_status TEXT DEFAULT 'NORMAL', - last_alert_time TEXT + last_alert_time TEXT, + zero_count INTEGER DEFAULT 0, + first_zero_today_kwh REAL ) """) + cursor.execute("PRAGMA table_info(alert_history)") + columns = {row[1] for row in cursor.fetchall()} + if 'zero_count' not in columns: + cursor.execute("ALTER TABLE alert_history ADD COLUMN zero_count INTEGER DEFAULT 0") + if 'first_zero_today_kwh' not in columns: + cursor.execute("ALTER TABLE alert_history ADD COLUMN first_zero_today_kwh REAL") conn.commit() + + def _reset_pending_zero(self, site_id: str): + """수집 실패가 실제 0kW 연속 판정에 섞이지 않도록 의심 카운트를 초기화한다.""" + with closing(sqlite3.connect(self.db_path)) as conn: + cursor = conn.cursor() + cursor.execute(""" + UPDATE alert_history + SET zero_count = 0, + first_zero_today_kwh = NULL + WHERE site_id = ? + AND (zero_count != 0 OR first_zero_today_kwh IS NOT NULL) + """, (site_id,)) + conn.commit() + + def _is_alert_enabled(self, site_id: str, plant_name: str) -> bool: + """ + 발전소별 알림 활성화 설정을 조회한다. + + plants.id가 전역 고유 기본키이므로 타입이 다른 crawler company_id는 조회 조건에 + 사용하지 않는다. 일시적인 DB 조회 실패 시에는 마지막 성공 값을 사용하고, + 캐시가 없으면 기존 동작과 동일하게 알림 활성 상태로 간주한다. + """ + try: + from database import get_supabase_client + + client = get_supabase_client() + if not client: + return self._alerts_enabled_cache.get(site_id, True) + + response = client.table("plants") \ + .select("alerts_enabled") \ + .eq("id", site_id) \ + .limit(1) \ + .execute() + + if response.data: + enabled = response.data[0].get('alerts_enabled') is not False + self._alerts_enabled_cache[site_id] = enabled + return enabled + + print(f" ⚠️ [Alert] {plant_name}: 발전소 알림 설정을 찾지 못했습니다.") + except Exception as e: + print(f" ⚠️ [Alert] {plant_name}: 알림 설정 조회 실패, 마지막 설정을 사용합니다: {e}") + + return self._alerts_enabled_cache.get(site_id, True) def send_telegram_message(self, chat_id, message): """텔레그램 메시지 전송""" @@ -63,17 +122,20 @@ class AlertManager: print(f" ❌ 텔레그램 전송 중 에러: {e}") return False - def check_and_alert(self, plant_info: dict, current_kw: float): + def check_and_alert( + self, + plant_info: dict, + current_kw: float, + today_kwh: float = None, + data_valid: bool = True + ): """ 발전량을 체크하고 필요 시 알림 전송 - 오전 10시 ~ 오후 5시에만 동작 + - current_kw 0이 3회 연속이고 누적 발전량도 정체된 경우만 알림 - 상태 변경 시에만 알림 (중복 방지) """ - # 1. 시간 체크 (오전 10시 ~ 오후 5시) - now = datetime.now() - if not (10 <= now.hour <= 17): - return - + now = ensure_kst(self.now_provider()) site_id = plant_info.get('id') plant_name = plant_info.get('display_name', plant_info.get('name')) chat_id = plant_info.get('telegram_chat_id') @@ -81,28 +143,51 @@ class AlertManager: if not site_id: return - # 1.5. DB에서 알림 활성화 상태 확인 + # 수집 오류/미수집은 발전소의 실제 0kW가 아니다. + # 이전 정상 응답에서 시작된 의심 카운트도 끊어 연속 판정에 섞이지 않게 한다. + if not data_valid: + self._reset_pending_zero(site_id) + print(f" ℹ️ [Alert] {plant_name}: 수집 실패 데이터는 0kW 판정에서 제외합니다.") + return + + # 알림 전송 시간대: 오전 10시 ~ 오후 5시 + if not (10 <= now.hour <= 17): + return + + if not self._is_alert_enabled(site_id, plant_name): + print(f" 🔇 [Alert] {plant_name}: 알림이 비활성화되어 있습니다.") + return + try: - from database import get_supabase_client - client = get_supabase_client() - if client: - company_id = plant_info.get('company_id', 1) - resp = client.table("plants").select("alerts_enabled").eq("id", site_id).eq("company_id", company_id).execute() - if resp.data and resp.data[0].get('alerts_enabled') is False: - print(f" 🔇 [Alert] {plant_name}: 알림이 비활성화되어 있습니다.") - return - except Exception as e: - print(f" ⚠️ 알림 설정 확인 중 오류: {e}") + current_kw = float(current_kw or 0) + except (TypeError, ValueError): + current_kw = 0.0 + + if today_kwh is not None: + try: + today_kwh = float(today_kwh) + except (TypeError, ValueError): + today_kwh = None # 2. 현재 DB 상태 확인 current_status = 'NORMAL' - with sqlite3.connect(self.db_path) as conn: + current_last_alert_time = None + zero_count = 0 + first_zero_today_kwh = None + with closing(sqlite3.connect(self.db_path)) as conn: cursor = conn.cursor() - cursor.execute("SELECT alert_status FROM alert_history WHERE site_id = ?", (site_id,)) + cursor.execute(""" + SELECT alert_status, last_alert_time, zero_count, first_zero_today_kwh + FROM alert_history + WHERE site_id = ? + """, (site_id,)) row = cursor.fetchone() if row: current_status = row[0] + current_last_alert_time = row[1] + zero_count = row[2] or 0 + first_zero_today_kwh = row[3] else: # 초기값 생성 cursor.execute("INSERT INTO alert_history (site_id, alert_status) VALUES (?, ?)", (site_id, 'NORMAL')) @@ -110,48 +195,80 @@ class AlertManager: # 3. 상태 전이 로직 new_status = current_status + new_last_alert_time = current_last_alert_time + new_zero_count = zero_count + new_first_zero_today_kwh = first_zero_today_kwh # [Case A] 발전량 0 (이상 감지) if current_kw == 0: - if current_status == 'NORMAL': + new_zero_count += 1 + if new_zero_count == 1 or new_first_zero_today_kwh is None: + new_first_zero_today_kwh = today_kwh + + today_delta = None + if today_kwh is not None and new_first_zero_today_kwh is not None: + today_delta = today_kwh - new_first_zero_today_kwh + + if today_delta is not None and today_delta > self.today_growth_threshold: + print( + f" ✅ [Alert] {plant_name}: 0kW 감지됐지만 누적 발전량 증가 " + f"({today_delta:.1f}kWh)로 오탐 처리" + ) + new_zero_count = 0 + new_first_zero_today_kwh = None + if current_status == 'ALERT': + new_status = 'NORMAL' + elif new_zero_count < self.zero_threshold: + print( + f" 🕒 [Alert] {plant_name}: 0kW 의심 " + f"{new_zero_count}/{self.zero_threshold}회, 알림 보류" + ) + elif current_status == 'NORMAL': # NORMAL -> ALERT: 알림 전송 - print(f" 🚨 [Alert] {plant_name} 발전량 0kW 감지! 알림 전송 시도...") - + print(f" 🚨 [Alert] {plant_name} 발전량 0kW {new_zero_count}회 연속 감지! 알림 전송 시도...") + if chat_id: message = ( f"🚨 [긴급] 발전소 이상 감지!\n\n" f"- 발전소: {plant_name}\n" - f"- 상태: 발전량 0kW\n" + f"- 상태: 발전량 0kW {new_zero_count}회 연속\n" f"- 시간: {now.strftime('%Y-%m-%d %H:%M:%S')}" ) if self.send_telegram_message(chat_id, message): new_status = 'ALERT' + new_last_alert_time = now.isoformat() else: print(f" ⚠️ {plant_name}: Chat ID 오류로 알림 실패") # 전송 실패해도 상태를 ALERT로 할 것인가? # 실패했다면 다음에 다시 시도해야 하므로 NORMAL 유지 else: print(f" ⚠️ {plant_name}: 설정된 Chat ID가 없습니다. (config.py 확인)") - - else: - # 이미 ALERT 상태: 중복 알림 생략 - pass # [Case B] 발전량 > 0 (정상 복구) else: + new_zero_count = 0 + new_first_zero_today_kwh = None if current_status == 'ALERT': # ALERT -> NORMAL: 상태 리셋 print(f" ✅ [Alert] {plant_name} 정상 복구됨 ({current_kw}kW)") # 복구 알림은 옵션 (현재는 생략) new_status = 'NORMAL' + new_last_alert_time = now.isoformat() - # 4. 상태 변경 시 DB 업데이트 - if new_status != current_status: - with sqlite3.connect(self.db_path) as conn: + # 4. 상태/의심 카운트 변경 시 DB 업데이트 + if ( + new_status != current_status + or new_zero_count != zero_count + or new_first_zero_today_kwh != first_zero_today_kwh + ): + with closing(sqlite3.connect(self.db_path)) as conn: cursor = conn.cursor() cursor.execute(""" UPDATE alert_history - SET alert_status = ?, last_alert_time = ? + SET alert_status = ?, + last_alert_time = ?, + zero_count = ?, + first_zero_today_kwh = ? WHERE site_id = ? - """, (new_status, now.isoformat(), site_id)) + """, (new_status, new_last_alert_time, new_zero_count, new_first_zero_today_kwh, site_id)) conn.commit() diff --git a/crawler/backward_backfill.py b/crawler/backward_backfill.py index 06c17a5..7985f01 100644 --- a/crawler/backward_backfill.py +++ b/crawler/backward_backfill.py @@ -13,6 +13,7 @@ import time import sqlite3 import argparse import importlib +from contextlib import closing from datetime import datetime, timedelta from dotenv import load_dotenv @@ -30,20 +31,28 @@ sys.path.append(current_dir) from config import get_all_plants from database import save_history, get_supabase_client +from time_utils import yesterday_kst # DB 경로 설정 DB_PATH = os.path.join(current_dir, "crawler_manager.db") def get_db_connection(): - """SQLite 연결 반환""" - return sqlite3.connect(DB_PATH, timeout=10.0) + """사용 후 실제 close되는 SQLite 컨텍스트를 반환한다.""" + return closing(sqlite3.connect(DB_PATH, timeout=10.0)) -def init_backfill_states(yesterday_str: str): +def get_initial_cursor_date(first_target_date_str: str) -> str: + """첫 수집 대상의 다음 날을 초기 커서로 반환한다.""" + first_target_date = datetime.strptime(first_target_date_str, "%Y-%m-%d") + return (first_target_date + timedelta(days=1)).strftime("%Y-%m-%d") + + +def init_backfill_states(first_target_date_str: str): """ 각 발전소의 백필 상태를 SQLite에 초기화합니다. 이미 존재하면 건너뛰고 없으면 신규 등록합니다. """ plants = get_all_plants() + initial_cursor_date = get_initial_cursor_date(first_target_date_str) with get_db_connection() as conn: cursor = conn.cursor() @@ -75,8 +84,11 @@ def init_backfill_states(yesterday_str: str): cursor.execute(""" INSERT INTO backfill_state (site_id, last_backfilled_date, consecutive_zero_count, status) VALUES (?, ?, 0, 'RUNNING') - """, (site_id, yesterday_str)) - print(f" 📝 [Backfill Init] {site_id} 상태 등록 완료 (시작일: {yesterday_str})") + """, (site_id, initial_cursor_date)) + print( + f" 📝 [Backfill Init] {site_id} 상태 등록 완료 " + f"(첫 수집일: {first_target_date_str}, 커서: {initial_cursor_date})" + ) conn.commit() def update_backfill_state(site_id: str, last_date: str, zero_count: int, status: str): @@ -178,15 +190,16 @@ def process_backfill(max_days_per_run: int = 7, delay_sec: float = 2.0, dry_run: current_dt = last_dt while days_processed < max_days_per_run: - # 하루 이전 날짜 계산 - current_dt = current_dt - timedelta(days=1) - target_date_str = current_dt.strftime("%Y-%m-%d") + # last_backfilled_date는 마지막으로 처리가 끝난 날짜를 뜻한다. + # 따라서 다음 수집 대상은 커서의 하루 전이다. + target_dt = current_dt - timedelta(days=1) + target_date_str = target_dt.strftime("%Y-%m-%d") # 가동개시일 이전이면 종료 - if current_dt < start_dt: + if target_dt < start_dt: print(f" 🏁 가동개시일({start_date_str}) 이전 날짜에 도달했습니다. 백필을 마감합니다.") if not dry_run: - update_backfill_state(site_id, target_date_str, zero_count, 'COMPLETED') + update_backfill_state(site_id, last_date_str, zero_count, 'COMPLETED') break print(f" 📅 [{target_date_str}] 수집 시도 중...") @@ -196,6 +209,7 @@ def process_backfill(max_days_per_run: int = 7, delay_sec: float = 2.0, dry_run: print(f" [Dry-Run] 크롤링 호출 시뮬레이션: {site_id} @ {target_date_str}") days_processed += 1 last_date_str = target_date_str + current_dt = target_dt time.sleep(0.1) continue @@ -204,6 +218,10 @@ def process_backfill(max_days_per_run: int = 7, delay_sec: float = 2.0, dry_run: # fetch_history_daily는 리스트 반환: [{'plant_id': '...', 'date': '...', 'generation_kwh': ...}] history_data = crawler_fn(plant_config, target_date_str, target_date_str) time.sleep(delay_sec) # Rate Limit 딜레이 + + if history_data is None: + print(" ➔ ❌ 수집 요청 또는 파싱 실패. 진행 상태를 유지하고 다음 실행에서 재시도합니다.") + break matched_val = 0.0 has_data = False @@ -217,42 +235,48 @@ def process_backfill(max_days_per_run: int = 7, delay_sec: float = 2.0, dry_run: matched = history_data if matched: - matched_val = matched[0].get('generation_kwh', 0.0) + matched_val = float(matched[0]['generation_kwh']) has_data = True if has_data: print(f" ➔ 🟢 수집 완료: {matched_val:.2f} kWh") + + # DB 저장에 성공한 날짜만 진행 상태에 반영한다. + saved = save_history([{ + 'plant_id': site_id, + 'date': target_date_str, + 'generation_kwh': matched_val + }], 'daily') + if not saved: + print(" ➔ ❌ DB 저장 실패. 진행 상태를 유지하고 다음 실행에서 재시도합니다.") + break + if matched_val <= 0.0: zero_count += 1 print(f" ➔ ⚠️ 발전량이 0입니다. (연속 {zero_count}일)") else: zero_count = 0 # 발전 확인 시 카운트 리셋 - - # DB 저장 (Supabase) - # matched list 형태로 save_history에 전달 - save_history([{ - 'plant_id': site_id, - 'date': target_date_str, - 'generation_kwh': matched_val - }], 'daily') else: - zero_count += 1 - print(f" ➔ ⚠️ 원본 데이터가 존재하지 않습니다. (연속 {zero_count}일)") + # 성공적으로 조회했지만 해당 날짜 레코드가 없는 경우다. + # 실제 0kWh와 구분하며 연속 무발전 종료 조건에는 포함하지 않는다. + zero_count = 0 + print(" ➔ ⚪ 원본에 해당 날짜 데이터가 없습니다. 무발전 일수에는 포함하지 않습니다.") except Exception as e: - zero_count += 1 - print(f" ➔ ❌ 크롤링 오류 발생: {e} (연속 {zero_count}일)") + print(f" ➔ ❌ 크롤링 오류 발생: {e}. 진행 상태를 유지하고 다음 실행에서 재시도합니다.") time.sleep(delay_sec) + break days_processed += 1 last_date_str = target_date_str + current_dt = target_dt # 상태 업데이트 (매 날짜 진행 마다 유실 방지 위해 업데이트) update_backfill_state(site_id, last_date_str, zero_count, 'RUNNING') - # 연속 무발전/데이터 누락 30일 조건 확인 + # 성공적으로 조회된 실제 0kWh만 연속 종료 조건에 포함한다. if zero_count >= 30: - print(f" 🏁 연속 30일 동안 데이터가 없거나 발전량이 0입니다. 백필을 마감합니다.") + print(" 🏁 실제 발전량이 연속 30일 동안 0kWh입니다. 백필을 마감합니다.") update_backfill_state(site_id, last_date_str, zero_count, 'COMPLETED') break @@ -273,7 +297,7 @@ def main(): if args.yesterday: yesterday_str = args.yesterday else: - yesterday_str = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") + yesterday_str = yesterday_kst().strftime("%Y-%m-%d") # 2. 백필 상태 초기화 init_backfill_states(yesterday_str) @@ -286,9 +310,12 @@ def main(): UPDATE backfill_state SET last_backfilled_date = ?, consecutive_zero_count = 0, status = 'RUNNING' WHERE site_id = ? - """, (yesterday_str, args.reset_site)) + """, (get_initial_cursor_date(yesterday_str), args.reset_site)) conn.commit() - print(f"🔄 [{args.reset_site}] 백필 상태가 {yesterday_str} 기준 'RUNNING'으로 재설정되었습니다.") + print( + f"🔄 [{args.reset_site}] 백필 상태가 첫 수집일 {yesterday_str} 기준 " + f"'RUNNING'으로 재설정되었습니다." + ) # 4. 백필 작업 진행 process_backfill(max_days_per_run=args.days, delay_sec=args.delay, dry_run=args.dry_run) diff --git a/crawler/config.py b/crawler/config.py index a50888a..fefaf77 100644 --- a/crawler/config.py +++ b/crawler/config.py @@ -2,11 +2,13 @@ # config.py - 다중 업체(Multi-Tenant) 설정 관리 # ========================================== +import os + # --------------------------------------------------------- # [프록시 설정 - 클라우드 이전용] # 오라클 서버 등 외부 망에서 접속할 때 NAS의 인터넷을 빌려 쓰기 위한 설정입니다. # --------------------------------------------------------- -USE_PROXY = False # True로 변경하면 모든 크롤링이 아래 프록시를 경유합니다. +USE_PROXY = os.getenv('USE_PROXY', 'False').lower() in ('true', '1', 't') # True로 변경하면 모든 크롤링이 아래 프록시를 경유합니다. PROXY_URL = "http://100.83.7.81:3128" PROXIES = { "http": PROXY_URL, diff --git a/crawler/crawler_manager.py b/crawler/crawler_manager.py index 595a842..5ac6c7d 100644 --- a/crawler/crawler_manager.py +++ b/crawler/crawler_manager.py @@ -12,9 +12,12 @@ # - 업데이트 패턴 학습은 부가 기능 (로깅용) import sqlite3 -from datetime import datetime, timedelta +from contextlib import closing +from datetime import timedelta from pathlib import Path +from time_utils import ensure_kst, now_kst, parse_legacy_kst_datetime + class CrawlerManager: """ @@ -25,7 +28,7 @@ class CrawlerManager: - analyze_and_optimize: 업데이트 패턴 학습 (로깅/모니터링 목적) """ - def __init__(self, db_path: str = None): + def __init__(self, db_path: str = None, now_provider=None): """ DB 연결 및 테이블 초기화 @@ -36,11 +39,16 @@ class CrawlerManager: db_path = Path(__file__).parent / "crawler_manager.db" self.db_path = str(db_path) + self.now_provider = now_provider or now_kst self._init_db() + def _now(self): + """테스트 주입값과 운영 시각을 모두 KST aware 값으로 정규화한다.""" + return ensure_kst(self.now_provider()) + def _init_db(self): """테이블이 없으면 생성""" - with sqlite3.connect(self.db_path) as conn: + with closing(sqlite3.connect(self.db_path)) as conn: cursor = conn.cursor() cursor.executescript(""" CREATE TABLE IF NOT EXISTS site_rules ( @@ -74,16 +82,16 @@ class CrawlerManager: """) conn.commit() - def _get_connection(self) -> sqlite3.Connection: - """SQLite 연결 반환 (타임아웃 설정 추가)""" - return sqlite3.connect(self.db_path, timeout=10.0) + def _get_connection(self): + """사용 후 실제 close되는 SQLite 컨텍스트를 반환한다.""" + return closing(sqlite3.connect(self.db_path, timeout=10.0)) def _cleanup_old_history(self): """오래된 히스토리 정리 (30일 이상 지난 데이터 삭제)""" try: with self._get_connection() as conn: cursor = conn.cursor() - limit_date = (datetime.now() - timedelta(days=30)).isoformat() + limit_date = (self._now() - timedelta(days=30)).isoformat() cursor.execute("DELETE FROM update_history WHERE detected_at < ?", (limit_date,)) conn.commit() except Exception as e: @@ -106,7 +114,7 @@ class CrawlerManager: if cursor.fetchone(): return False - today = datetime.now().strftime("%Y-%m-%d") + today = self._now().strftime("%Y-%m-%d") cursor.execute(""" INSERT INTO site_rules (site_id, status, target_minute, start_date, last_run) VALUES (?, 'LEARNING', -1, ?, NULL) @@ -132,7 +140,7 @@ class CrawlerManager: Returns: bool: 크롤링 실행 여부 (야간이면 False) """ - now = datetime.now() + now = self._now() current_hour = now.hour current_minute = now.minute @@ -172,7 +180,7 @@ class CrawlerManager: """ new_kw = float(current_data.get('kw', 0)) new_today = float(current_data.get('today', 0)) - now = datetime.now() + now = self._now() with self._get_connection() as conn: cursor = conn.cursor() @@ -203,7 +211,7 @@ class CrawlerManager: # 3. 1시간 이상 저장 없었으면 강제 heartbeat 저장 elif last_updated_at: try: - last_dt = datetime.fromisoformat(last_updated_at) + last_dt = parse_legacy_kst_datetime(last_updated_at) if now - last_dt >= timedelta(hours=1): should_save = True except (ValueError, TypeError): @@ -238,7 +246,7 @@ class CrawlerManager: 이 정보는 현재 크롤링 스케줄 제어에는 사용하지 않으며, 향후 분석이나 시각화를 위한 참고 데이터로만 활용. """ - now = datetime.now() + now = self._now() current_minute = now.minute # 히스토리 기록 @@ -319,7 +327,7 @@ class CrawlerManager: Args: site_id: 사이트 식별자 """ - now_str = datetime.now().isoformat() + now_str = self._now().isoformat() with self._get_connection() as conn: cursor = conn.cursor() diff --git a/crawler/crawlers/base.py b/crawler/crawlers/base.py index 4d18ea0..2bbf38c 100644 --- a/crawler/crawlers/base.py +++ b/crawler/crawlers/base.py @@ -63,6 +63,16 @@ def format_result(name, kw, today, plant_id, status=None): 'status': status } + +def is_data_valid(item): + """크롤러 결과가 실제 발전 상태를 나타내는 유효한 관측값인지 판단한다.""" + explicit_value = item.get('data_valid') + if explicit_value is not None: + return bool(explicit_value) + + status = str(item.get('status', '')) + return '오류' not in status and 'error' not in status.lower() + def validate_data_quality(data_list, value_key='generation_kwh'): """ 데이터 품질 검증 @@ -112,4 +122,4 @@ def validate_data_quality(data_list, value_key='generation_kwh'): 'warnings': warnings, 'all_zero': all_zero, 'duplicate_ratio': duplicate_ratio - } \ No newline at end of file + } diff --git a/crawler/crawlers/cmsolar.py b/crawler/crawlers/cmsolar.py index a53e19e..680fa03 100644 --- a/crawler/crawlers/cmsolar.py +++ b/crawler/crawlers/cmsolar.py @@ -238,6 +238,7 @@ def fetch_history_daily(plant_info, start_date, end_date): from dateutil.relativedelta import relativedelta results = [] + fetch_failed = False plant_id = plant_info.get('id', 'cmsolar-10') auth = plant_info.get('auth', {}) system = plant_info.get('system', {}) @@ -275,10 +276,10 @@ def fetch_history_daily(plant_info, start_date, end_date): print(" ✓ Login successful") else: print(" ✗ Login failed") - return results + return None except Exception as e: print(f" ✗ Login error: {e}") - return results + return None # 사이트 선택 (필수!) try: @@ -287,7 +288,7 @@ def fetch_history_daily(plant_info, start_date, end_date): print(" ✓ Site selected") except Exception as e: print(f" ✗ Site selection error: {e}") - return results + return None # 월 단위로 반복 (type=month는 한 달 치 일별 데이터 반환) current_date = datetime.strptime(start_date, '%Y-%m-%d') @@ -346,17 +347,20 @@ def fetch_history_daily(plant_info, start_date, end_date): print(f" ✓ {date_str}: {generation_kwh:.2f}kWh") else: print(f" ⚠ No tbody found for {month_start[:7]}") + fetch_failed = True else: print(f" ✗ HTTP {res.status_code} for {month_start[:7]}") - + fetch_failed = True + except Exception as e: print(f" ✗ Error for {month_start[:7]}: {e}") + fetch_failed = True # 다음 달로 이동 current_date = (current_date.replace(day=1) + relativedelta(months=1)) print(f"[Total] Collected {len(results)} daily records\n") - return results + return None if fetch_failed else results def fetch_history_monthly(plant_info, start_month, end_month): diff --git a/crawler/crawlers/hyundai.py b/crawler/crawlers/hyundai.py index a1fb2ee..d247d7c 100644 --- a/crawler/crawlers/hyundai.py +++ b/crawler/crawlers/hyundai.py @@ -277,6 +277,7 @@ def fetch_history_daily(plant_info, start_date, end_date): import calendar results = [] + fetch_failed = False plant_id = plant_info.get('id', 'hyundai-08') auth = plant_info.get('auth', {}) system = plant_info.get('system', {}) @@ -314,14 +315,14 @@ def fetch_history_daily(plant_info, start_date, end_date): if not auth_token: print(" ✗ Login failed") - return results + return None headers['x-auth-token'] = auth_token headers['X-Mid'] = 'siteWork' print(" ✓ Login successful") except Exception as e: print(f" ✗ Login error: {e}") - return results + return None # 월 단위 반복 current_month = datetime.strptime(start_date[:7], '%Y-%m') # YYYY-MM-01 @@ -376,14 +377,16 @@ def fetch_history_daily(plant_info, start_date, end_date): print(f" No data") else: print(f" HTTP {res.status_code}") - + fetch_failed = True + except Exception as e: print(f" Error: {e}") + fetch_failed = True current_month += relativedelta(months=1) print(f"\n[Total] Collected {len(results)} daily records\n") - return results + return None if fetch_failed else results def fetch_history_monthly(plant_info, start_month, end_month): diff --git a/crawler/crawlers/kremc.py b/crawler/crawlers/kremc.py index 035a9fd..1a67950 100644 --- a/crawler/crawlers/kremc.py +++ b/crawler/crawlers/kremc.py @@ -287,6 +287,7 @@ def fetch_history_daily(plant_info, start_date, end_date): import urllib.parse results = [] + fetch_failed = False plant_id = plant_info.get('id', 'kremc-05') auth = plant_info.get('auth', {}) system = plant_info.get('system', {}) @@ -324,7 +325,7 @@ def fetch_history_daily(plant_info, start_date, end_date): if login_res.status_code != 200: print(" ✗ Login failed") - return results + return None login_json = login_res.json() data = login_json.get('data') @@ -332,7 +333,7 @@ def fetch_history_daily(plant_info, start_date, end_date): if not token: print(" ✗ Token not found") - return results + return None print(" ✓ Login successful") @@ -411,18 +412,21 @@ def fetch_history_daily(plant_info, start_date, end_date): print(" No data") else: print(f" HTTP {res.status_code}") + fetch_failed = True except Exception as e: print(f" Error: {e}") + fetch_failed = True # 다음 기간 설정 (현재 기간 끝 다음날) loop_start = loop_end + timedelta(days=1) except Exception as e: print(f" ✗ Overall Error: {e}") + return None print(f"\n[Total] Collected {len(results)} daily records\n") - return results + return None if fetch_failed else results def fetch_history_monthly(plant_info, start_month, end_month): diff --git a/crawler/crawlers/nrems.py b/crawler/crawlers/nrems.py index 3d1d784..a13d0e7 100644 --- a/crawler/crawlers/nrems.py +++ b/crawler/crawlers/nrems.py @@ -5,16 +5,17 @@ import requests import json import re -from datetime import datetime from .base import safe_float, create_session, format_result +from time_utils import today_kst def _get_inverter_sums(session, pscode, system_config): """ 1, 2호기 인버터별 일일 발전량 추출 (JSON API 사용) """ try: - today_str = datetime.now().strftime('%Y-%m-%d') - month_str = datetime.now().strftime('%Y-%m') + current_date = today_kst() + today_str = current_date.strftime('%Y-%m-%d') + month_str = current_date.strftime('%Y-%m') headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0 Safari/537.36', @@ -199,7 +200,8 @@ def fetch_data(plant_info): 'name': f'{company_name} {plant_name}', 'kw': 0.0, 'today': 0.0, - 'status': '🔴 오류' + 'status': '🔴 오류', + 'data_valid': False }) return results @@ -373,6 +375,7 @@ def fetch_history_daily(plant_info, start_date, end_date): import calendar results = [] + fetch_failed = False # 설정 추출 plant_id = plant_info.get('id', '') @@ -484,17 +487,20 @@ def fetch_history_daily(plant_info, start_date, end_date): print(f" No data") except Exception as json_err: print(f" JSON Error: {json_err}") + fetch_failed = True else: print(f" HTTP {response.status_code}") + fetch_failed = True except Exception as e: print(f" Error: {e}") + fetch_failed = True # 다음 달 1일로 이동 current_dt = (current_dt.replace(day=1) + timedelta(days=32)).replace(day=1) print(f"\n[Total] Collected {len(results)} daily records\n") - return results + return None if fetch_failed else results def fetch_history_monthly(plant_info, start_month, end_month): diff --git a/crawler/crawlers/sun_wms.py b/crawler/crawlers/sun_wms.py index 686ed89..3331415 100644 --- a/crawler/crawlers/sun_wms.py +++ b/crawler/crawlers/sun_wms.py @@ -201,6 +201,7 @@ def fetch_history_daily(plant_info, start_date, end_date): from .base import safe_float, create_session results = [] + fetch_failed = False plant_id = plant_info.get('id', 'sunwms-06') auth = plant_info.get('auth', {}) system = plant_info.get('system', {}) @@ -231,10 +232,10 @@ def fetch_history_daily(plant_info, start_date, end_date): print(" ✓ Login successful") else: print(" ✗ Login failed") - return results + return None except Exception as e: print(f" ✗ Login error: {e}") - return results + return None # 월 단위 루프 적용 start_dt = datetime.strptime(start_date, '%Y-%m-%d') @@ -294,17 +295,20 @@ def fetch_history_daily(plant_info, start_date, end_date): print(" No data") else: print(" No tbody") + fetch_failed = True else: print(f" HTTP {res.status_code}") - + fetch_failed = True + except Exception as e: print(f" Error: {e}") + fetch_failed = True # 다음 기간 설정 loop_start = loop_end + timedelta(days=1) print(f"\n[Total] Collected {len(results)} daily records\n") - return results + return None if fetch_failed else results def fetch_history_monthly(plant_info, start_month, end_month): diff --git a/crawler/daily_summary.py b/crawler/daily_summary.py index 1da75f1..ad7c6fd 100644 --- a/crawler/daily_summary.py +++ b/crawler/daily_summary.py @@ -3,7 +3,7 @@ # ========================================== # solar_logs 데이터를 집계하여 daily_stats 테이블에 저장 -from datetime import datetime, timedelta, timezone +from datetime import datetime try: from dotenv import load_dotenv @@ -12,8 +12,9 @@ except ImportError: pass import pandas as pd -from database import get_supabase_client +from database import get_supabase_client, upsert_daily_stats from config import get_all_plants +from time_utils import kst_day_bounds_utc, today_kst, yesterday_kst def get_history_crawler(plant_type: str): """발전소 타입별 과거 데이터 크롤러 함수 반환""" @@ -56,8 +57,7 @@ def calculate_daily_stats(date_str: str = None): date_str: 집계 대상 날짜 (YYYY-MM-DD). 미지정 시 오늘. """ if date_str is None: - kst = timezone(timedelta(hours=9)) - date_str = datetime.now(kst).strftime('%Y-%m-%d') + date_str = today_kst().strftime('%Y-%m-%d') print(f"\n📊 [일일 통계 집계] {date_str}") print("-" * 60) @@ -82,17 +82,14 @@ def calculate_daily_stats(date_str: str = None): plant_info_map[p.get('id', '')] = p # 2. 해당일 로그 조회 (KST 날짜 범위를 UTC로 변환하여 쿼리) - kst = timezone(timedelta(hours=9)) - start_kst = datetime.strptime(f"{date_str} 00:00:00", "%Y-%m-%d %H:%M:%S").replace(tzinfo=kst) - end_kst = datetime.strptime(f"{date_str} 23:59:59", "%Y-%m-%d %H:%M:%S").replace(tzinfo=kst) - start_utc = start_kst.astimezone(timezone.utc).isoformat() - end_utc = end_kst.astimezone(timezone.utc).isoformat() + target_date = datetime.strptime(date_str, "%Y-%m-%d").date() + start_utc, next_start_utc = kst_day_bounds_utc(target_date) try: result = client.table("solar_logs") \ .select("plant_id, current_kw, today_kwh, created_at") \ - .gte("created_at", start_utc) \ - .lte("created_at", end_utc) \ + .gte("created_at", start_utc.isoformat()) \ + .lt("created_at", next_start_utc.isoformat()) \ .order("created_at", desc=False) \ .execute() @@ -135,9 +132,14 @@ def calculate_daily_stats(date_str: str = None): if matched: original_val = matched[0].get('generation_kwh', 0) - if original_val > 0: - print(f" ➔ 🟢 보정 성공: 기존 집계 {total_generation:.1f} kWh ➔ 원본 {original_val:.1f} kWh") + if original_val > total_generation: + print(f" ➔ 🟢 원본 상향 보정: 로그 {total_generation:.1f} kWh ➔ 원본 {original_val:.1f} kWh") total_generation = original_val + elif original_val > 0: + print( + f" ➔ 🛡️ 원본({original_val:.1f})이 로그 집계" + f"({total_generation:.1f})보다 작아 기존 값을 유지합니다." + ) else: print(f" ➔ ⚠️ 원본 값이 0이므로 보정을 건너뜁니다.") else: @@ -166,10 +168,12 @@ def calculate_daily_stats(date_str: str = None): # 4. daily_stats 테이블에 Upsert if stats_list: try: - result = client.table("daily_stats").upsert( + upsert_daily_stats( + client, stats_list, - on_conflict="plant_id,date" - ).execute() + source="daily_summary", + allow_decrease=False, + ) print("-" * 60) print(f"✅ {len(stats_list)}개 발전소 통계 저장 완료") @@ -181,93 +185,17 @@ def calculate_daily_stats(date_str: str = None): return True -def calculate_monthly_stats(target_month: str): - """ - 특정 월의 발전 통계 집계 (일간 데이터 합산) - - Args: - target_month: YYYY-MM - """ - print(f"\n📅 [월간 통계 집계] {target_month}") - print("-" * 60) - - client = get_supabase_client() - if not client: - return False - - try: - # 1. 모든 발전소 ID 조회 - plants_res = client.table("plants").select("id").execute() - plant_ids = [p['id'] for p in plants_res.data] - - updated_count = 0 - - for pid in plant_ids: - # 2. 해당 월의 Daily 합계 조회 - import calendar - year_str, month_str = target_month.split("-") - last_day = calendar.monthrange(int(year_str), int(month_str))[1] - - d_res = client.table("daily_stats").select("total_generation") \ - .eq("plant_id", pid) \ - .gte("date", f"{target_month}-01") \ - .lte("date", f"{target_month}-{last_day:02d}") \ - .execute() - - if not d_res.data: - continue - - total_gen = sum(r.get('total_generation', 0) or 0 for r in d_res.data) - - # 3. Monthly Upsert - client.table("monthly_stats").upsert({ - "plant_id": pid, - "month": target_month, - "total_generation": round(total_gen, 2), - "updated_at": datetime.now().isoformat() - }, on_conflict="plant_id, month").execute() - - print(f" {pid}: {total_gen:.1f}kWh (Month Total)") - updated_count += 1 - - print("-" * 60) - print(f"✅ {updated_count}개 발전소 월간 통계 갱신 완료") - return True - - except Exception as e: - print(f" ❌ 월간 집계 실패: {e}") - return False - - if __name__ == "__main__": import sys - from datetime import timedelta - # 인자로 날짜 지정 가능: python daily_summary.py 2026-01-22 if len(sys.argv) > 1: target_date = sys.argv[1] else: # 인자 없으면 '어제' 날짜를 기본값으로 사용 # (새벽에 실행하여 전날 데이터를 마감하는 시나리오) - yesterday = datetime.now() - timedelta(days=1) - target_date = yesterday.strftime('%Y-%m-%d') + target_date = yesterday_kst().strftime('%Y-%m-%d') print(f"ℹ️ 날짜 미지정 -> 어제({target_date}) 기준으로 집계합니다.") - # 1. 일간 통계 집계 - success = calculate_daily_stats(target_date) - - # 2. 월말 체크 및 월간 집계 트리거 - # target_date가 해당 월의 마지막 날이면 월간 집계 실행 - if success: - try: - current_dt = datetime.strptime(target_date, '%Y-%m-%d') - import calendar - last_day = calendar.monthrange(current_dt.year, current_dt.month)[1] - - if current_dt.day == last_day: - target_month = current_dt.strftime('%Y-%m') - print(f"\n🔔 월말({target_date}) 감지 -> {target_month} 월간 집계 실행") - calculate_monthly_stats(target_month) - except Exception as e: - print(f"⚠️ 월간 집계 트리거 오류: {e}") + # daily_stats 변경 트리거가 해당 월의 monthly_stats를 즉시 갱신한다. + calculate_daily_stats(target_date) diff --git a/crawler/database.py b/crawler/database.py index 42939cf..572a4e9 100644 --- a/crawler/database.py +++ b/crawler/database.py @@ -5,6 +5,8 @@ import os from datetime import datetime +from time_utils import ensure_kst, now_kst + # 환경 변수에서 Supabase 설정 로드 SUPABASE_URL = os.getenv('SUPABASE_URL', '') SUPABASE_KEY = os.getenv('SUPABASE_KEY', '') @@ -38,6 +40,38 @@ def get_supabase_client(): return _supabase_client + +def upsert_daily_stats(client, records, source, allow_decrease=False): + """Store daily stats through the database's single conflict policy. + + Automated sources preserve the highest daily generation. Only an explicit + correction path (currently Excel upload) may pass ``allow_decrease=True``. + The database function also derives generation_hours and refreshes the + affected monthly aggregate. + """ + if not records: + return [] + + payload = [] + for record in records: + normalized = { + "plant_id": record["plant_id"], + "date": record["date"], + "total_generation": float(record["total_generation"]), + "peak_kw": float(record.get("peak_kw") or 0), + } + payload.append(normalized) + + result = client.rpc( + "upsert_daily_stats", + { + "p_records": payload, + "p_source": source, + "p_allow_decrease": allow_decrease, + }, + ).execute() + return result.data or [] + def save_to_supabase(data_list): """ 수집된 발전 데이터를 Supabase solar_logs 테이블에 저장 @@ -69,9 +103,7 @@ def save_to_supabase(data_list): continue # 한국 시간(KST) 타임스탬프 생성 - from datetime import timezone, timedelta - kst = timezone(timedelta(hours=9)) - kst_now = datetime.now(kst).isoformat() + kst_now = now_kst().isoformat() status = item.get('status', '') is_error = '오류' in status # '🔴 오류' 상태 감지 @@ -96,14 +128,9 @@ def save_to_supabase(data_list): print(f"✅ [DB] Supabase 저장 완료: {len(records)}건 (solar_logs)") - # daily_stats 테이블 업데이트 (Upsert) - # [보호 로직] - # 1. 오류 상태(크롤링 실패)인 경우 daily_stats 갱신 금지 - # 2. today_kwh == 0인 경우 daily_stats 갱신 금지 (새벽 0 값으로 하루치 덮어쓰기 방지) - # 3. 야간 시간대(21:00~06:00 KST) daily_stats 갱신 금지 (일몰 이후 잔류값 보호) - # 4. DB에 이미 저장된 값보다 작은 경우 갱신 금지 (최댓값 보호) - kst = timezone(timedelta(hours=9)) - kst_now_dt = datetime.now(kst) + # daily_stats 갱신은 DB 함수의 공통 최댓값 정책을 사용한다. + # 오류/0/야간 값은 호출 전에 제외하고, 충돌과 월간 재집계는 DB가 처리한다. + kst_now_dt = now_kst() kst_date_str = kst_now_dt.strftime("%Y-%m-%d") kst_hour = kst_now_dt.hour @@ -114,18 +141,6 @@ def save_to_supabase(data_list): else: daily_records = [] - # 기존 daily_stats 값 조회 (MAX 보호용) - try: - existing_res = client.table("daily_stats") \ - .select("plant_id, total_generation") \ - .eq("date", kst_date_str) \ - .execute() - existing_map = {row['plant_id']: float(row.get('total_generation') or 0) - for row in existing_res.data} - except Exception as e: - print(f" ⚠️ [DB] 기존 daily_stats 조회 실패: {e}") - existing_map = {} - for item in data_list: plant_id = item.get('id', '') if not plant_id: @@ -143,23 +158,21 @@ def save_to_supabase(data_list): print(f" ⚠️ [{plant_id}] today_kwh=0 → daily_stats 갱신 건너뜀 (새벽/야간 추정)") continue - # [MAX 보호] 기존 값보다 작으면 갱신 건너뜀 - existing_val = existing_map.get(plant_id, 0) - if today_val <= existing_val: - print(f" ⚠️ [{plant_id}] 신규({today_val:.1f}) ≤ 기존({existing_val:.1f}) → daily_stats 갱신 건너뜀 (최댓값 보호)") - continue - daily_records.append({ "plant_id": plant_id, "date": kst_date_str, "total_generation": today_val, - "created_at": kst_now - # updated_at은 자동으로 NOW()로 설정됨 (DB 기본값) + "peak_kw": float(item.get('kw', 0)), }) if daily_records: try: - stats_result = client.table("daily_stats").upsert(daily_records, on_conflict="plant_id, date").execute() + upsert_daily_stats( + client, + daily_records, + source="realtime", + allow_decrease=False, + ) print(f"✅ [DB] daily_stats 업데이트 완료: {len(daily_records)}건") except Exception as e: print(f"⚠️ [DB] daily_stats 업데이트 실패: {e}") @@ -231,21 +244,18 @@ def save_history(data_list, data_type='hourly'): ts_iso = ts.replace(' ', 'T') # Check if future (simple string comparison works for ISO format if consistent, but datetime is safer) # KST aware comparison - from datetime import timezone, timedelta - kst = timezone(timedelta(hours=9)) - now_kst = datetime.now(kst) + current_kst = now_kst() try: # ts example: 2026-01-27 14:00:00. Assume input is local time (KST) # We convert it to aware datetime dt_ts = datetime.fromisoformat(ts_iso) - if dt_ts.tzinfo is None: - dt_ts = dt_ts.replace(tzinfo=kst) - - if dt_ts > now_kst: + dt_ts = ensure_kst(dt_ts) + + if dt_ts > current_kst: continue # Skip future data except ValueError: - pass # robust date parsing needed if format varies + continue # Ensure timezone is sent to Supabase to prevent UTC assumption final_created_at = dt_ts.isoformat() @@ -266,11 +276,17 @@ def save_history(data_list, data_type='hourly'): elif data_type == 'daily': table_name = "daily_stats" for item in data_list: + generation_kwh = float(item['generation_kwh']) + if generation_kwh < 0: + raise ValueError( + f"daily generation_kwh must be non-negative: " + f"{item.get('plant_id')} {item.get('date')}" + ) records.append({ 'plant_id': item['plant_id'], 'date': item['date'], - 'total_generation': float(item.get('generation_kwh', 0)) - # 'updated_at': datetime.now().isoformat() + 'total_generation': generation_kwh, + 'peak_kw': 0.0, }) elif data_type == 'monthly': @@ -280,7 +296,8 @@ def save_history(data_list, data_type='hourly'): 'plant_id': item['plant_id'], 'month': item['month'], # YYYY-MM 'total_generation': float(item.get('generation_kwh', 0)), - 'updated_at': datetime.now().isoformat() + 'updated_at': now_kst().isoformat(), + 'source': 'history_monthly', }) if not records: @@ -290,49 +307,13 @@ def save_history(data_list, data_type='hourly'): if data_type == 'hourly': client.table(table_name).insert(records).execute() elif data_type == 'daily': - client.table(table_name).upsert(records, on_conflict="plant_id, date").execute() - - # [Auto Update] Daily 데이터 저장 시 Monthly 통계 자동 갱신 - # 1. 업데이트된 월 목록 추출 - updated_months = set() - for rec in records: - try: - # date: YYYY-MM-DD - month_key = rec['date'][:7] - updated_months.add((rec['plant_id'], month_key)) - except: - pass - - if updated_months: - monthly_upserts = [] - for (pid, m_key) in updated_months: - # 2. 해당 월의 Daily 합계 조회 (DB Aggregation) - import calendar - try: - year, month_int = map(int, m_key.split('-')) - _, last_day = calendar.monthrange(year, month_int) - except: - last_day = 31 - - d_res = client.table("daily_stats").select("total_generation") \ - .eq("plant_id", pid) \ - .gte("date", f"{m_key}-01") \ - .lte("date", f"{m_key}-{last_day}") \ - .execute() - - total_gen = sum(r['total_generation'] or 0 for r in d_res.data) - - monthly_upserts.append({ - "plant_id": pid, - "month": m_key, - "total_generation": round(total_gen, 2), - "updated_at": datetime.now().isoformat() - }) - - # 3. Monthly Upsert - if monthly_upserts: - client.table("monthly_stats").upsert(monthly_upserts, on_conflict="plant_id, month").execute() - print(f" 🔄 [Sync] {len(monthly_upserts)}개월치 Monthly Stats 자동 갱신 완료") + # DB 함수가 최댓값 보호와 월간 통계 갱신을 한 트랜잭션으로 처리한다. + upsert_daily_stats( + client, + records, + source="history", + allow_decrease=False, + ) elif data_type == 'monthly': client.table(table_name).upsert(records, on_conflict="plant_id, month").execute() diff --git a/crawler/main.py b/crawler/main.py index 73a12fa..5474be0 100644 --- a/crawler/main.py +++ b/crawler/main.py @@ -3,7 +3,8 @@ # ========================================== import re -from datetime import datetime, timezone, timedelta + +from time_utils import now_kst # 환경 변수 로드 (최상단에서 실행) try: @@ -16,6 +17,7 @@ except ImportError: from config import get_all_plants from database import save_to_supabase, save_to_console from crawlers import get_crawler +from crawlers.base import is_data_valid from crawler_manager import CrawlerManager from alert_manager import AlertManager @@ -38,7 +40,7 @@ def integrated_monitoring(save_to_db=True, company_filter=None, force_run=False) company_filter: 특정 업체만 필터링 (예: 'sunwind') force_run: True면 스케줄러 무시하고 강제 실행 """ - now_str = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + now_str = now_kst().strftime('%Y-%m-%d %H:%M:%S') print(f"\n🚀 [통합 관제 시스템] 데이터 수집 시작... ({now_str})") print("-" * 75) @@ -97,7 +99,12 @@ def integrated_monitoring(save_to_db=True, company_filter=None, force_run=False) alert_info = plant.copy() alert_info['id'] = item_id alert_info['name'] = item.get('name', plant_name) - alert_manager.check_and_alert(alert_info, item.get('kw', 0)) + alert_manager.check_and_alert( + alert_info, + item.get('kw', 0), + item.get('today'), + data_valid=is_data_valid(item) + ) if item_id: # 크롤링 성공 기록 (항상) @@ -148,7 +155,7 @@ def integrated_monitoring(save_to_db=True, company_filter=None, force_run=False) save_to_supabase(total_results) # 이상 감지 로직 - current_hour = datetime.now().hour + current_hour = now_kst().hour if 10 <= current_hour <= 17: issues = [d['name'] for d in total_results if d.get('kw', 0) == 0] if issues: @@ -168,8 +175,7 @@ def run_daily_close(force=False): 일일 마감 집계 실행 (KST 21:00~21:10 자동 트리거 또는 force=True) solar_logs 데이터를 집계하여 daily_stats에 당일 최종값을 확정합니다. """ - kst = timezone(timedelta(hours=9)) - kst_now = datetime.now(kst) + kst_now = now_kst() kst_hour = kst_now.hour kst_minute = kst_now.minute @@ -200,4 +206,3 @@ if __name__ == "__main__": # 마감 집계: 21:00~21:10 KST 자동 실행 또는 --close 옵션 run_daily_close(force=force_close) - diff --git a/crawler/requirements.in b/crawler/requirements.in new file mode 100644 index 0000000..c842558 --- /dev/null +++ b/crawler/requirements.in @@ -0,0 +1,8 @@ +# Direct runtime dependencies for crawler, backfill, and daily summary jobs. +# Keep NumPy on the last line that supports the CI/runtime Python 3.10 baseline. +numpy==2.2.6 +pandas==2.3.3 +python-dateutil==2.9.0.post0 +python-dotenv==1.2.1 +requests==2.32.5 +supabase==2.27.2 diff --git a/crawler/requirements.txt b/crawler/requirements.txt new file mode 100644 index 0000000..a2f6b0e --- /dev/null +++ b/crawler/requirements.txt @@ -0,0 +1,59 @@ +# Fully resolved crawler dependency lock for Python 3.10/3.11. +# Direct dependencies and compatibility constraints are maintained in requirements.in. + +annotated-types==0.8.0 +anyio==4.14.2 +cachetools==6.2.6 +certifi==2026.7.22 +cffi==2.1.1 +charset-normalizer==3.4.9 +click==8.4.2 +colorama==0.4.6 +cryptography==50.0.0 +deprecation==2.1.0 +fsspec==2026.7.0 +h11==0.16.0 +h2==4.4.1 +hpack==4.2.0 +httpcore==1.0.9 +httpx==0.28.1 +hyperframe==6.1.0 +idna==3.18 +markdown-it-py==4.2.0 +mdurl==0.1.2 +mmh3==5.2.1 +multidict==6.7.1 +numpy==2.2.6 +packaging==26.3 +pandas==2.3.3 +postgrest==2.27.2 +propcache==0.5.2 +pycparser==3.0 +pydantic==2.13.4 +pydantic_core==2.46.4 +Pygments==2.20.0 +pyiceberg==0.11.1 +PyJWT==2.13.0 +pyparsing==3.3.2 +pyroaring==1.1.0 +python-dateutil==2.9.0.post0 +python-dotenv==1.2.1 +pytz==2026.3.post1 +realtime==2.27.2 +requests==2.32.5 +rich==14.3.4 +six==1.17.0 +storage3==2.27.2 +StrEnum==0.4.15 +strictyaml==1.7.3 +supabase==2.27.2 +supabase-auth==2.27.2 +supabase-functions==2.27.2 +tenacity==9.1.4 +typing-inspection==0.4.2 +typing_extensions==4.16.0 +tzdata==2026.3 +urllib3==2.7.0 +websockets==15.0.1 +yarl==1.24.5 +zstandard==0.25.0 diff --git a/crawler/tests/check_alert_setting.py b/crawler/tests/check_alert_setting.py new file mode 100644 index 0000000..7b51d21 --- /dev/null +++ b/crawler/tests/check_alert_setting.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""운영 Supabase의 발전소 알림 설정을 읽기 전용으로 확인한다.""" + +import argparse +import os +import sys + +from dotenv import load_dotenv + + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) +CRAWLER_DIR = os.path.dirname(CURRENT_DIR) +sys.path.insert(0, CRAWLER_DIR) +load_dotenv(os.path.join(CRAWLER_DIR, ".env")) + +from alert_manager import AlertManager + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("plant_id") + parser.add_argument("plant_name") + args = parser.parse_args() + + enabled = AlertManager()._is_alert_enabled(args.plant_id, args.plant_name) + print(f"ALERTS_ENABLED={str(enabled).lower()}") + + +if __name__ == "__main__": + main() diff --git a/crawler/tests/check_history_source.py b/crawler/tests/check_history_source.py new file mode 100644 index 0000000..5081c61 --- /dev/null +++ b/crawler/tests/check_history_source.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""원본 사이트의 특정 일별 값을 DB 저장 없이 확인하는 운영 점검 도구.""" + +import argparse +import importlib +import os +import sys + +from dotenv import load_dotenv + + +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) +CRAWLER_DIR = os.path.dirname(CURRENT_DIR) +sys.path.insert(0, CRAWLER_DIR) +load_dotenv(os.path.join(CRAWLER_DIR, ".env")) + +from config import get_all_plants + + +def build_plant_map(): + plants = {} + for plant in get_all_plants(): + if plant.get("options", {}).get("is_split"): + for site_id, split_index in (("nrems-01", 1), ("nrems-02", 2)): + split_plant = plant.copy() + split_plant["id"] = site_id + split_plant["options"] = plant["options"].copy() + split_plant["options"]["split_index"] = split_index + plants[site_id] = split_plant + else: + plants[plant["id"]] = plant + return plants + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "targets", + nargs="+", + metavar="PLANT_ID=YYYY-MM-DD", + help="확인할 발전소 ID와 날짜", + ) + args = parser.parse_args() + + plants = build_plant_map() + failed = False + + for target in args.targets: + site_id, separator, date = target.partition("=") + if not separator or site_id not in plants: + print(f"INVALID {target}") + failed = True + continue + + plant = plants[site_id] + module = importlib.import_module(f"crawlers.{plant['type']}") + data = module.fetch_history_daily(plant, date, date) + + if data is None: + print(f"ERROR {site_id} {date}: request or parse failure") + failed = True + continue + + matched = [row for row in data if row.get("plant_id") == site_id] + if not matched: + print(f"NO_DATA {site_id} {date}") + continue + + for row in matched: + print( + f"OK {site_id} {row.get('date')}: " + f"{float(row.get('generation_kwh', 0)):.2f} kWh" + ) + + raise SystemExit(1 if failed else 0) + + +if __name__ == "__main__": + main() diff --git a/crawler/tests/check_stats_consistency.py b/crawler/tests/check_stats_consistency.py new file mode 100644 index 0000000..146debe --- /dev/null +++ b/crawler/tests/check_stats_consistency.py @@ -0,0 +1,146 @@ +"""Read-only consistency check for daily_stats and monthly_stats. + +Usage: + python tests/check_stats_consistency.py --start-month 2026-01 --end-month 2026-08 +""" + +from __future__ import annotations + +import argparse +from collections import defaultdict +from pathlib import Path +import sys + +from dotenv import load_dotenv + + +CRAWLER_DIR = Path(__file__).resolve().parents[1] +load_dotenv(CRAWLER_DIR / ".env") +if str(CRAWLER_DIR) not in sys.path: + sys.path.insert(0, str(CRAWLER_DIR)) + +from database import get_supabase_client # noqa: E402 + + +PAGE_SIZE = 1000 + + +def fetch_all(query): + """Fetch all PostgREST rows without silently stopping at its page limit.""" + rows = [] + offset = 0 + while True: + page = query.range(offset, offset + PAGE_SIZE - 1).execute().data or [] + rows.extend(page) + if len(page) < PAGE_SIZE: + return rows + offset += PAGE_SIZE + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--start-month", default="2026-01") + parser.add_argument("--end-month", default="2026-08") + parser.add_argument("--limit", type=int, default=100) + args = parser.parse_args() + + client = get_supabase_client() + if client is None: + print("Supabase connection is not configured.") + return 2 + + start_date = f"{args.start_month}-01" + end_date = f"{args.end_month}-31" + + daily_query = ( + client.table("daily_stats") + .select("plant_id,date,total_generation") + .gte("date", start_date) + .lte("date", end_date) + .order("date") + ) + daily_rows = fetch_all(daily_query) + try: + monthly_query = ( + client.table("monthly_stats") + .select("plant_id,month,total_generation,last_date,source") + .gte("month", args.start_month) + .lte("month", args.end_month) + .order("month") + ) + monthly_rows = fetch_all(monthly_query) + schema_version = "consistent" + except Exception: + monthly_query = ( + client.table("monthly_stats") + .select("plant_id,month,total_generation,currnet_last_date") + .gte("month", args.start_month) + .lte("month", args.end_month) + .order("month") + ) + monthly_rows = fetch_all(monthly_query) + schema_version = "legacy" + + daily_totals = defaultdict(float) + daily_counts = defaultdict(int) + daily_last_dates = {} + for row in daily_rows: + key = (row["plant_id"], str(row["date"])[:7]) + daily_totals[key] += float(row.get("total_generation") or 0) + daily_counts[key] += 1 + daily_last_dates[key] = max( + daily_last_dates.get(key, ""), + str(row["date"]), + ) + + monthly_map = { + (row["plant_id"], row["month"]): float(row.get("total_generation") or 0) + for row in monthly_rows + } + all_keys = sorted(set(daily_totals) | set(monthly_map)) + + mismatches = [] + missing_monthly = [] + monthly_without_daily = [] + for key in all_keys: + if key not in monthly_map: + missing_monthly.append(key) + continue + if key not in daily_totals: + monthly_without_daily.append(key) + continue + difference = round(monthly_map[key] - daily_totals[key], 2) + if abs(difference) > 0.01: + mismatches.append((key, daily_totals[key], monthly_map[key], difference)) + + invalid_last_date = sum( + 1 + for row in monthly_rows + if schema_version == "legacy" and row.get("currnet_last_date") + ) + print( + "summary " + f"schema={schema_version} " + f"daily_rows={len(daily_rows)} monthly_rows={len(monthly_rows)} " + f"pairs={len(all_keys)} mismatches={len(mismatches)} " + f"missing_monthly={len(missing_monthly)} " + f"monthly_without_daily={len(monthly_without_daily)} " + f"legacy_last_date_non_null={invalid_last_date}" + ) + + for (plant_id, month), daily_total, monthly_total, difference in mismatches[: args.limit]: + print( + f"mismatch plant={plant_id} month={month} days={daily_counts[(plant_id, month)]} " + f"last_date={daily_last_dates[(plant_id, month)]} " + f"daily={daily_total:.2f} monthly={monthly_total:.2f} diff={difference:+.2f}" + ) + for plant_id, month in missing_monthly[: args.limit]: + print(f"missing_monthly plant={plant_id} month={month}") + for plant_id, month in monthly_without_daily[: args.limit]: + print(f"monthly_without_daily plant={plant_id} month={month}") + + return 1 if mismatches or missing_monthly else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/crawler/tests/fixtures/cmsolar_daily.html b/crawler/tests/fixtures/cmsolar_daily.html new file mode 100644 index 0000000..b12ffd3 --- /dev/null +++ b/crawler/tests/fixtures/cmsolar_daily.html @@ -0,0 +1,9 @@ + + + + + + +
51,051.25fixture
+ + diff --git a/crawler/tests/fixtures/hyundai_daily.json b/crawler/tests/fixtures/hyundai_daily.json new file mode 100644 index 0000000..79d62ee --- /dev/null +++ b/crawler/tests/fixtures/hyundai_daily.json @@ -0,0 +1,13 @@ +{ + "datas": { + "solraMonthWork": { + "runData": [ + "10.0", + "20.0", + "30.0", + "40.0", + "133.60" + ] + } + } +} diff --git a/crawler/tests/fixtures/kremc_daily.json b/crawler/tests/fixtures/kremc_daily.json new file mode 100644 index 0000000..a0447d3 --- /dev/null +++ b/crawler/tests/fixtures/kremc_daily.json @@ -0,0 +1,10 @@ +{ + "data": { + "userByTimeDataResultDtoList": [ + { + "gathDtm": "2026-08-05 00:00:00", + "dayEnergy": "53.25" + } + ] + } +} diff --git a/crawler/tests/fixtures/kremc_login.json b/crawler/tests/fixtures/kremc_login.json new file mode 100644 index 0000000..71d63a5 --- /dev/null +++ b/crawler/tests/fixtures/kremc_login.json @@ -0,0 +1,3 @@ +{ + "data": "fixture-auth-token" +} diff --git a/crawler/tests/fixtures/nrems_daily.json b/crawler/tests/fixtures/nrems_daily.json new file mode 100644 index 0000000..bbbf4c0 --- /dev/null +++ b/crawler/tests/fixtures/nrems_daily.json @@ -0,0 +1,8 @@ +{ + "pdata": [ + { + "DATE": "08-05", + "INV": "121.50" + } + ] +} diff --git a/crawler/tests/fixtures/sun_wms_daily.html b/crawler/tests/fixtures/sun_wms_daily.html new file mode 100644 index 0000000..85fd6e3 --- /dev/null +++ b/crawler/tests/fixtures/sun_wms_daily.html @@ -0,0 +1,9 @@ + + + + + + +
2026-08-0552.59
+ + diff --git a/crawler/tests/test_alert_manager.py b/crawler/tests/test_alert_manager.py new file mode 100644 index 0000000..6a6a56e --- /dev/null +++ b/crawler/tests/test_alert_manager.py @@ -0,0 +1,127 @@ +import sqlite3 +import sys +import tempfile +import unittest +from contextlib import closing +from datetime import datetime +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import Mock +from unittest.mock import patch + + +CRAWLER_DIR = Path(__file__).resolve().parents[1] +if str(CRAWLER_DIR) not in sys.path: + sys.path.insert(0, str(CRAWLER_DIR)) + +from alert_manager import AlertManager +from crawlers.base import is_data_valid + + +class AlertManagerTest(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + db_path = Path(self.temp_dir.name) / "alert_test.db" + self.manager = AlertManager( + db_path=db_path, + now_provider=lambda: datetime(2026, 8, 5, 12, 0, 0) + ) + self.real_is_alert_enabled = self.manager._is_alert_enabled + self.manager._is_alert_enabled = Mock(return_value=True) + self.manager.send_telegram_message = Mock(return_value=True) + self.plant = { + "id": "nrems-03", + "name": "3호기", + "display_name": "3호기", + "telegram_chat_id": 1234, + } + + def tearDown(self): + self.temp_dir.cleanup() + + def get_state(self): + with closing(sqlite3.connect(self.manager.db_path)) as conn: + return conn.execute(""" + SELECT alert_status, zero_count, first_zero_today_kwh + FROM alert_history + WHERE site_id = ? + """, (self.plant["id"],)).fetchone() + + def test_collection_error_breaks_zero_sequence(self): + self.manager.check_and_alert(self.plant, 0, 100) + self.manager.check_and_alert(self.plant, 0, 100) + self.assertEqual(("NORMAL", 2, 100.0), self.get_state()) + + self.manager.check_and_alert( + self.plant, + 0, + 0, + data_valid=False + ) + self.assertEqual(("NORMAL", 0, None), self.get_state()) + + self.manager.check_and_alert(self.plant, 0, 100) + self.manager.check_and_alert(self.plant, 0, 100) + self.manager.send_telegram_message.assert_not_called() + + self.manager.check_and_alert(self.plant, 0, 100) + self.manager.send_telegram_message.assert_called_once() + self.assertEqual(("ALERT", 3, 100.0), self.get_state()) + + def test_cumulative_generation_growth_clears_zero_suspicion(self): + self.manager.check_and_alert(self.plant, 0, 100) + self.manager.check_and_alert(self.plant, 0, 101) + + self.manager.send_telegram_message.assert_not_called() + self.assertEqual(("NORMAL", 0, None), self.get_state()) + + def test_disabled_alert_does_not_create_alert_state(self): + self.manager._is_alert_enabled.return_value = False + + for _ in range(3): + self.manager.check_and_alert(self.plant, 0, 100) + + self.manager.send_telegram_message.assert_not_called() + self.assertIsNone(self.get_state()) + + def test_positive_generation_recovers_alert_state(self): + for _ in range(3): + self.manager.check_and_alert(self.plant, 0, 100) + + self.assertEqual("ALERT", self.get_state()[0]) + + self.manager.check_and_alert(self.plant, 10, 101) + + self.assertEqual(("NORMAL", 0, None), self.get_state()) + + def test_alert_setting_uses_globally_unique_plant_id(self): + query = Mock() + query.select.return_value = query + query.eq.return_value = query + query.limit.return_value = query + query.execute.return_value = SimpleNamespace( + data=[{"alerts_enabled": False}] + ) + client = Mock() + client.table.return_value = query + database_module = SimpleNamespace( + get_supabase_client=Mock(return_value=client) + ) + + with patch.dict(sys.modules, {"database": database_module}): + enabled = self.real_is_alert_enabled("nrems-03", "3호기") + + self.assertFalse(enabled) + client.table.assert_called_once_with("plants") + query.eq.assert_called_once_with("id", "nrems-03") + + def test_data_validity_distinguishes_collection_error_from_plant_fault(self): + self.assertTrue(is_data_valid({"status": "🟢 정상"})) + self.assertTrue(is_data_valid({"status": "🔴 점검/고장", "kw": 0})) + self.assertFalse(is_data_valid({"status": "🔴 오류"})) + self.assertFalse(is_data_valid({"status": "ERROR"})) + self.assertFalse(is_data_valid({"status": "🟢 정상", "data_valid": False})) + + +if __name__ == "__main__": + unittest.main() diff --git a/crawler/tests/test_backward_backfill.py b/crawler/tests/test_backward_backfill.py new file mode 100644 index 0000000..cec2a36 --- /dev/null +++ b/crawler/tests/test_backward_backfill.py @@ -0,0 +1,162 @@ +import sqlite3 +import tempfile +import unittest +from contextlib import closing +from pathlib import Path +from unittest.mock import patch + +import backward_backfill + + +def make_plant(site_id="plant-a", start_date="2026-08-01"): + return { + "id": site_id, + "name": site_id, + "type": "fake", + "start_date": start_date, + "options": {}, + } + + +class BackwardBackfillTest(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.db_path = str(Path(self.temp_dir.name) / "crawler_manager.db") + self.db_path_patch = patch.object(backward_backfill, "DB_PATH", self.db_path) + self.db_path_patch.start() + + def tearDown(self): + self.db_path_patch.stop() + self.temp_dir.cleanup() + + def read_state(self, site_id): + with closing(sqlite3.connect(self.db_path)) as conn: + return conn.execute( + """ + SELECT last_backfilled_date, consecutive_zero_count, status + FROM backfill_state + WHERE site_id = ? + """, + (site_id,), + ).fetchone() + + def init_sites(self, plants, first_target="2026-08-05"): + with patch.object(backward_backfill, "get_all_plants", return_value=plants): + backward_backfill.init_backfill_states(first_target) + + def run_process(self, plants, crawler, save_result=True, days=1): + with ( + patch.object(backward_backfill, "get_all_plants", return_value=plants), + patch.object(backward_backfill, "get_history_crawler", return_value=crawler), + patch.object(backward_backfill, "save_history", return_value=save_result), + ): + backward_backfill.process_backfill( + max_days_per_run=days, + delay_sec=0, + ) + + def test_first_requested_date_is_not_skipped(self): + plant = make_plant() + requested_dates = [] + self.init_sites([plant]) + + self.assertEqual(self.read_state("plant-a")[0], "2026-08-06") + + def crawler(_plant, start_date, _end_date): + requested_dates.append(start_date) + return [{ + "plant_id": "plant-a", + "date": start_date, + "generation_kwh": 12.0, + }] + + self.run_process([plant], crawler) + + self.assertEqual(requested_dates, ["2026-08-05"]) + self.assertEqual(self.read_state("plant-a"), ("2026-08-05", 0, "RUNNING")) + + def test_fetch_failure_keeps_cursor_and_zero_count(self): + plant = make_plant() + self.init_sites([plant]) + backward_backfill.update_backfill_state("plant-a", "2026-08-06", 7, "RUNNING") + + self.run_process([plant], lambda *_args: None) + + self.assertEqual(self.read_state("plant-a"), ("2026-08-06", 7, "RUNNING")) + + def test_save_failure_keeps_cursor_and_zero_count(self): + plant = make_plant() + self.init_sites([plant]) + + def crawler(_plant, start_date, _end_date): + return [{ + "plant_id": "plant-a", + "date": start_date, + "generation_kwh": 5.0, + }] + + self.run_process([plant], crawler, save_result=False) + + self.assertEqual(self.read_state("plant-a"), ("2026-08-06", 0, "RUNNING")) + + def test_successful_no_data_advances_without_counting_zero(self): + plant = make_plant() + self.init_sites([plant]) + backward_backfill.update_backfill_state("plant-a", "2026-08-06", 4, "RUNNING") + + self.run_process([plant], lambda *_args: []) + + self.assertEqual(self.read_state("plant-a"), ("2026-08-05", 0, "RUNNING")) + + def test_thirtieth_actual_zero_completes_site(self): + plant = make_plant(start_date="2026-01-01") + self.init_sites([plant], first_target="2026-08-30") + backward_backfill.update_backfill_state("plant-a", "2026-08-31", 29, "RUNNING") + + def crawler(_plant, start_date, _end_date): + return [{ + "plant_id": "plant-a", + "date": start_date, + "generation_kwh": 0.0, + }] + + self.run_process([plant], crawler) + + self.assertEqual(self.read_state("plant-a"), ("2026-08-30", 30, "COMPLETED")) + + def test_one_site_failure_does_not_block_another_site(self): + plants = [make_plant("plant-a"), make_plant("plant-b")] + self.init_sites(plants) + + def crawler(plant, start_date, _end_date): + if plant["id"] == "plant-a": + return None + return [{ + "plant_id": "plant-b", + "date": start_date, + "generation_kwh": 9.0, + }] + + self.run_process(plants, crawler) + + self.assertEqual(self.read_state("plant-a")[0], "2026-08-06") + self.assertEqual(self.read_state("plant-b")[0], "2026-08-05") + + def test_start_date_is_processed_before_completion(self): + plant = make_plant(start_date="2026-08-05") + self.init_sites([plant]) + + def crawler(_plant, start_date, _end_date): + return [{ + "plant_id": "plant-a", + "date": start_date, + "generation_kwh": 1.0, + }] + + self.run_process([plant], crawler, days=2) + + self.assertEqual(self.read_state("plant-a"), ("2026-08-05", 0, "COMPLETED")) + + +if __name__ == "__main__": + unittest.main() diff --git a/crawler/tests/test_daily_summary_timezone.py b/crawler/tests/test_daily_summary_timezone.py new file mode 100644 index 0000000..213b38a --- /dev/null +++ b/crawler/tests/test_daily_summary_timezone.py @@ -0,0 +1,128 @@ +import sys +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + + +CRAWLER_DIR = Path(__file__).resolve().parents[1] +if str(CRAWLER_DIR) not in sys.path: + sys.path.insert(0, str(CRAWLER_DIR)) + +import daily_summary + + +class FakeRpc: + def __init__(self, db, params): + self.db = db + self.params = params + + def execute(self): + self.db.saved = self.params["p_records"] + self.db.rpc_params = self.params + return SimpleNamespace(data=self.db.saved) + + +class FakeQuery: + def __init__(self, db, table_name): + self.db = db + self.table_name = table_name + self.filters = [] + self.db.queries.append(self) + + def select(self, _columns): + return self + + def gte(self, column, value): + self.filters.append(("gte", column, value)) + return self + + def lt(self, column, value): + self.filters.append(("lt", column, value)) + return self + + def order(self, _column, desc=False): + return self + + def upsert(self, payload, on_conflict=None): + self.payload = payload + return self + + def execute(self): + if self.table_name == "plants": + return SimpleNamespace(data=[{"id": "plant-a", "capacity": 100}]) + if self.table_name == "solar_logs": + return SimpleNamespace(data=[{ + "plant_id": "plant-a", + "current_kw": 25, + "today_kwh": 120, + "created_at": "2026-08-05T06:00:00+00:00", + }]) + if self.table_name == "daily_stats": + self.db.saved = self.payload + return SimpleNamespace(data=self.payload) + raise AssertionError(f"unexpected table: {self.table_name}") + + +class FakeDb: + def __init__(self): + self.queries = [] + self.saved = None + self.rpc_params = None + + def table(self, table_name): + return FakeQuery(self, table_name) + + def rpc(self, function_name, params): + if function_name != "upsert_daily_stats": + raise AssertionError(function_name) + return FakeRpc(self, params) + + +class DailySummaryTimezoneTest(unittest.TestCase): + def test_daily_close_uses_kst_utc_half_open_bounds(self): + db = FakeDb() + + with ( + patch.object(daily_summary, "get_supabase_client", return_value=db), + patch.object(daily_summary, "get_all_plants", return_value=[]), + ): + result = daily_summary.calculate_daily_stats("2026-08-05") + + solar_query = next(q for q in db.queries if q.table_name == "solar_logs") + self.assertIn(("gte", "created_at", "2026-08-04T15:00:00+00:00"), solar_query.filters) + self.assertIn(("lt", "created_at", "2026-08-05T15:00:00+00:00"), solar_query.filters) + self.assertTrue(result) + self.assertEqual(120, db.saved[0]["total_generation"]) + self.assertEqual("daily_summary", db.rpc_params["p_source"]) + self.assertFalse(db.rpc_params["p_allow_decrease"]) + + def test_lower_original_site_value_does_not_replace_log_maximum(self): + db = FakeDb() + plant = { + "id": "plant-a", + "type": "test", + "options": {}, + } + + with ( + patch.object(daily_summary, "get_supabase_client", return_value=db), + patch.object(daily_summary, "get_all_plants", return_value=[plant]), + patch.object( + daily_summary, + "get_history_crawler", + return_value=lambda *_args: [{ + "plant_id": "plant-a", + "date": "2026-08-05", + "generation_kwh": 100, + }], + ), + ): + result = daily_summary.calculate_daily_stats("2026-08-05") + + self.assertTrue(result) + self.assertEqual(120, db.saved[0]["total_generation"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/crawler/tests/test_database_history.py b/crawler/tests/test_database_history.py new file mode 100644 index 0000000..6f7c2a9 --- /dev/null +++ b/crawler/tests/test_database_history.py @@ -0,0 +1,171 @@ +import unittest +from unittest.mock import patch + +import database + + +class FakeResponse: + def __init__(self, data=None): + self.data = data or [] + + +class FakeRpc: + def __init__(self, client, params): + self.client = client + self.params = params + + def execute(self): + if self.client.fail_rpc: + raise RuntimeError("rpc failed") + + saved = [] + for row in self.params["p_records"]: + key = (row["plant_id"], row["date"]) + existing = self.client.daily.get(key) + incoming = row["total_generation"] + if ( + existing is None + or self.params["p_allow_decrease"] + or incoming > existing + ): + self.client.daily[key] = incoming + saved.append({**row, "total_generation": self.client.daily[key]}) + self.client.rpc_calls.append(self.params) + return FakeResponse(saved) + + +class FakeQuery: + def __init__(self, client, table_name): + self.client = client + self.table_name = table_name + self.action = None + self.payload = None + self.filters = {} + + def select(self, _columns): + self.action = "select" + return self + + def upsert(self, payload, on_conflict=None): + self.action = "upsert" + self.payload = payload + self.on_conflict = on_conflict + return self + + def eq(self, column, value): + self.filters[column] = ("eq", value) + return self + + def in_(self, column, values): + self.filters[column] = ("in", set(values)) + return self + + def gte(self, column, value): + self.filters[f"{column}_gte"] = ("gte", value) + return self + + def lte(self, column, value): + self.filters[f"{column}_lte"] = ("lte", value) + return self + + def execute(self): + if self.client.fail_select and self.action == "select": + raise RuntimeError("select failed") + + if self.action == "select" and self.table_name == "daily_stats": + rows = [] + for (plant_id, date), value in self.client.daily.items(): + if "plant_id" in self.filters and plant_id != self.filters["plant_id"][1]: + continue + if "date" in self.filters and date not in self.filters["date"][1]: + continue + if "date_gte" in self.filters and date < self.filters["date_gte"][1]: + continue + if "date_lte" in self.filters and date > self.filters["date_lte"][1]: + continue + rows.append({ + "plant_id": plant_id, + "date": date, + "total_generation": value, + }) + return FakeResponse(rows) + + if self.action == "upsert" and self.table_name == "daily_stats": + for row in self.payload: + self.client.daily[(row["plant_id"], row["date"])] = row["total_generation"] + self.client.daily_upserts.append(self.payload) + return FakeResponse(self.payload) + + if self.action == "upsert" and self.table_name == "monthly_stats": + self.client.monthly_upserts.append(self.payload) + return FakeResponse(self.payload) + + raise AssertionError(f"unexpected query: {self.table_name} {self.action}") + + +class FakeClient: + def __init__(self, daily=None, fail_rpc=False): + self.daily = daily or {} + self.fail_select = False + self.fail_rpc = fail_rpc + self.daily_upserts = [] + self.monthly_upserts = [] + self.rpc_calls = [] + + def table(self, table_name): + return FakeQuery(self, table_name) + + def rpc(self, function_name, params): + if function_name != "upsert_daily_stats": + raise AssertionError(function_name) + return FakeRpc(self, params) + + +class DatabaseHistoryTest(unittest.TestCase): + def save_daily(self, client, generation): + with patch.object(database, "get_supabase_client", return_value=client): + return database.save_history([{ + "plant_id": "plant-a", + "date": "2026-08-05", + "generation_kwh": generation, + }], "daily") + + def test_existing_positive_value_is_not_replaced_by_zero(self): + client = FakeClient({("plant-a", "2026-08-05"): 100.0}) + + result = self.save_daily(client, 0.0) + + self.assertTrue(result) + self.assertEqual(client.daily[("plant-a", "2026-08-05")], 100.0) + self.assertEqual(client.daily_upserts, []) + self.assertEqual(client.rpc_calls[0]["p_source"], "history") + self.assertFalse(client.rpc_calls[0]["p_allow_decrease"]) + + def test_larger_value_updates_existing_daily_stat(self): + client = FakeClient({("plant-a", "2026-08-05"): 100.0}) + + result = self.save_daily(client, 120.0) + + self.assertTrue(result) + self.assertEqual(client.daily[("plant-a", "2026-08-05")], 120.0) + self.assertEqual(len(client.rpc_calls), 1) + + def test_rpc_failure_prevents_write(self): + client = FakeClient(fail_rpc=True) + + result = self.save_daily(client, 120.0) + + self.assertFalse(result) + self.assertEqual(client.rpc_calls, []) + + def test_negative_generation_is_rejected(self): + client = FakeClient() + + result = self.save_daily(client, -1.0) + + self.assertFalse(result) + self.assertEqual(client.rpc_calls, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/crawler/tests/test_history_fetch_contract.py b/crawler/tests/test_history_fetch_contract.py new file mode 100644 index 0000000..ab2b82a --- /dev/null +++ b/crawler/tests/test_history_fetch_contract.py @@ -0,0 +1,99 @@ +import unittest +from unittest.mock import patch + +from crawlers import cmsolar, hyundai, kremc, nrems, sun_wms + + +class FakeResponse: + def __init__(self, status_code=500, data=None, headers=None): + self.status_code = status_code + self._data = data or {} + self.headers = headers or {} + self.text = "" + self.encoding = None + + def json(self): + return self._data + + +class FakeSession: + def __init__(self, response): + self.response = response + + def post(self, *_args, **_kwargs): + return self.response + + def get(self, *_args, **_kwargs): + return self.response + + +class HistoryFetchContractTest(unittest.TestCase): + def test_nrems_http_failure_returns_none(self): + plant = { + "id": "nrems-03", + "name": "3호기", + "auth": {"pscode": "x"}, + "options": {"is_split": False}, + } + with patch.object(nrems, "create_session", return_value=FakeSession(FakeResponse())): + self.assertIsNone(nrems.fetch_history_daily(plant, "2026-08-05", "2026-08-05")) + + def test_nrems_successful_empty_response_returns_empty_list(self): + plant = { + "id": "nrems-03", + "name": "3호기", + "auth": {"pscode": "x"}, + "options": {"is_split": False}, + } + response = FakeResponse(status_code=200, data={"pdata": []}) + with patch.object(nrems, "create_session", return_value=FakeSession(response)): + self.assertEqual( + nrems.fetch_history_daily(plant, "2026-08-05", "2026-08-05"), + [], + ) + + def test_kremc_login_failure_returns_none(self): + plant = { + "id": "kremc-05", + "name": "5호기", + "auth": {}, + "system": {}, + "options": {}, + } + with patch.object(kremc, "create_session", return_value=FakeSession(FakeResponse())): + self.assertIsNone(kremc.fetch_history_daily(plant, "2026-08-05", "2026-08-05")) + + def test_sun_wms_login_failure_returns_none(self): + plant = { + "id": "sunwms-06", + "name": "6호기", + "auth": {}, + "system": {}, + } + # 이 함수는 내부에서 base.create_session을 다시 import한다. + with patch("crawlers.base.create_session", return_value=FakeSession(FakeResponse())): + self.assertIsNone(sun_wms.fetch_history_daily(plant, "2026-08-05", "2026-08-05")) + + def test_hyundai_login_failure_returns_none(self): + plant = { + "id": "hyundai-08", + "name": "8호기", + "auth": {}, + "system": {}, + } + with patch.object(hyundai, "create_session", return_value=FakeSession(FakeResponse())): + self.assertIsNone(hyundai.fetch_history_daily(plant, "2026-08-05", "2026-08-05")) + + def test_cmsolar_login_failure_returns_none(self): + plant = { + "id": "cmsolar-10", + "name": "10호기", + "auth": {}, + "system": {}, + } + with patch.object(cmsolar, "create_session", return_value=FakeSession(FakeResponse())): + self.assertIsNone(cmsolar.fetch_history_daily(plant, "2026-08-05", "2026-08-05")) + + +if __name__ == "__main__": + unittest.main() diff --git a/crawler/tests/test_history_response_fixtures.py b/crawler/tests/test_history_response_fixtures.py new file mode 100644 index 0000000..395505f --- /dev/null +++ b/crawler/tests/test_history_response_fixtures.py @@ -0,0 +1,155 @@ +import json +from pathlib import Path +import unittest +from unittest.mock import patch + +from crawlers import cmsolar, hyundai, kremc, nrems, sun_wms + + +FIXTURES = Path(__file__).resolve().parent / "fixtures" + + +def load_json(name): + with (FIXTURES / name).open(encoding="utf-8") as fixture_file: + return json.load(fixture_file) + + +def load_text(name): + return (FIXTURES / name).read_text(encoding="utf-8") + + +class FakeResponse: + def __init__(self, *, status_code=200, data=None, text="", headers=None): + self.status_code = status_code + self._data = data or {} + self.text = text + self.headers = headers or {} + self.encoding = None + + def json(self): + return self._data + + +class SequenceSession: + def __init__(self, *, post_responses=None, get_responses=None): + self.post_responses = list(post_responses or []) + self.get_responses = list(get_responses or []) + + def post(self, *_args, **_kwargs): + if not self.post_responses: + raise AssertionError("unexpected POST request") + return self.post_responses.pop(0) + + def get(self, *_args, **_kwargs): + if not self.get_responses: + raise AssertionError("unexpected GET request") + return self.get_responses.pop(0) + + +class HistoryResponseFixtureTest(unittest.TestCase): + def test_nrems_daily_json_fixture(self): + session = SequenceSession(post_responses=[ + FakeResponse(data=load_json("nrems_daily.json")), + ]) + plant = { + "id": "nrems-03", + "name": "3호기", + "auth": {"pscode": "fixture"}, + "options": {"is_split": False}, + } + + with patch.object(nrems, "create_session", return_value=session): + result = nrems.fetch_history_daily(plant, "2026-08-05", "2026-08-05") + + self.assertEqual(result, [{ + "plant_id": "nrems-03", + "date": "2026-08-05", + "generation_kwh": 121.5, + }]) + + def test_kremc_daily_json_fixture(self): + session = SequenceSession( + post_responses=[FakeResponse(data=load_json("kremc_login.json"))], + get_responses=[FakeResponse(data=load_json("kremc_daily.json"))], + ) + plant = { + "id": "kremc-05", + "name": "5호기", + "auth": {"user_id": "fixture", "password": "fixture"}, + "system": {"login_url": "https://fixture/login", "api_base": "https://fixture"}, + "options": {}, + } + + with patch.object(kremc, "create_session", return_value=session): + result = kremc.fetch_history_daily(plant, "2026-08-05", "2026-08-05") + + self.assertEqual(result[0]["date"], "2026-08-05") + self.assertEqual(result[0]["generation_kwh"], 53.25) + + def test_hyundai_daily_json_fixture(self): + session = SequenceSession( + post_responses=[FakeResponse(headers={"x-auth-token": "fixture-token"})], + get_responses=[FakeResponse(data=load_json("hyundai_daily.json"))], + ) + plant = { + "id": "hyundai-08", + "name": "8호기", + "auth": {"user_id": "fixture", "password": "fixture", "site_id": "fixture"}, + "system": {"base_url": "https://fixture", "login_path": "/login"}, + } + + with patch.object(hyundai, "create_session", return_value=session): + result = hyundai.fetch_history_daily(plant, "2026-08-05", "2026-08-05") + + self.assertEqual(result, [{ + "plant_id": "hyundai-08", + "date": "2026-08-05", + "generation_kwh": 133.6, + }]) + + def test_sun_wms_daily_html_fixture(self): + session = SequenceSession( + post_responses=[FakeResponse()], + get_responses=[FakeResponse(text=load_text("sun_wms_daily.html"))], + ) + plant = { + "id": "sunwms-06", + "name": "6호기", + "auth": {"payload_id": "fixture", "payload_pw": "fixture"}, + "system": { + "login_url": "https://fixture/login", + "base_url": "https://fixture", + "statics_url": "https://fixture/statics", + }, + } + + with patch("crawlers.base.create_session", return_value=session): + result = sun_wms.fetch_history_daily(plant, "2026-08-05", "2026-08-05") + + self.assertEqual(result[0]["date"], "2026-08-05") + self.assertEqual(result[0]["generation_kwh"], 52.59) + + def test_cmsolar_daily_html_fixture(self): + session = SequenceSession( + post_responses=[FakeResponse()], + get_responses=[ + FakeResponse(), + FakeResponse(text=load_text("cmsolar_daily.html")), + ], + ) + plant = { + "id": "cmsolar-10", + "name": "10호기", + "auth": {"login_id": "fixture", "login_pw": "fixture", "site_no": "fixture"}, + "system": {"login_url": "https://fixture/login", "api_url": "https://fixture"}, + } + + with patch.object(cmsolar, "create_session", return_value=session): + result = cmsolar.fetch_history_daily(plant, "2026-08-05", "2026-08-05") + + self.assertEqual(result[0]["date"], "2026-08-05") + self.assertEqual(result[0]["generation_kwh"], 1051.25) + + +if __name__ == "__main__": + unittest.main() diff --git a/crawler/tests/test_time_utils.py b/crawler/tests/test_time_utils.py new file mode 100644 index 0000000..b8de525 --- /dev/null +++ b/crawler/tests/test_time_utils.py @@ -0,0 +1,84 @@ +import sqlite3 +import sys +import tempfile +import unittest +from contextlib import closing +from datetime import date, datetime, timezone +from pathlib import Path + + +CRAWLER_DIR = Path(__file__).resolve().parents[1] +if str(CRAWLER_DIR) not in sys.path: + sys.path.insert(0, str(CRAWLER_DIR)) + +from crawler_manager import CrawlerManager +from time_utils import ( + KST, + ensure_kst, + kst_day_bounds_utc, + parse_legacy_kst_datetime, +) + + +class TimeUtilsTest(unittest.TestCase): + def test_new_year_kst_day_uses_previous_utc_date(self): + start, end = kst_day_bounds_utc(date(2026, 1, 1)) + + self.assertEqual("2025-12-31T15:00:00+00:00", start.isoformat()) + self.assertEqual("2026-01-01T15:00:00+00:00", end.isoformat()) + + def test_month_end_is_half_open_at_next_kst_midnight(self): + start, end = kst_day_bounds_utc(date(2026, 8, 31)) + + self.assertEqual("2026-08-30T15:00:00+00:00", start.isoformat()) + self.assertEqual("2026-08-31T15:00:00+00:00", end.isoformat()) + + def test_legacy_naive_sqlite_datetime_is_treated_as_kst(self): + parsed = parse_legacy_kst_datetime("2026-08-06T09:00:00") + + self.assertEqual(KST, parsed.tzinfo) + self.assertEqual(9, parsed.hour) + + def test_aware_utc_datetime_is_converted_to_kst(self): + parsed = ensure_kst(datetime(2026, 8, 5, 15, 0, tzinfo=timezone.utc)) + + self.assertEqual(date(2026, 8, 6), parsed.date()) + self.assertEqual(0, parsed.hour) + + +class CrawlerManagerTimezoneTest(unittest.TestCase): + def setUp(self): + self.temp_dir = tempfile.TemporaryDirectory() + self.current = datetime(2026, 8, 5, 19, 59, tzinfo=timezone.utc) + self.manager = CrawlerManager( + db_path=Path(self.temp_dir.name) / "crawler_manager.db", + now_provider=lambda: self.current, + ) + + def tearDown(self): + self.temp_dir.cleanup() + + def test_night_window_uses_kst_when_host_clock_is_utc(self): + # UTC 19:59 == KST 04:59 + self.assertFalse(self.manager.should_run("plant-a")) + + # UTC 20:00 == KST 05:00 + self.current = datetime(2026, 8, 5, 20, 0, tzinfo=timezone.utc) + self.assertTrue(self.manager.should_run("plant-a")) + + def test_legacy_naive_heartbeat_value_can_be_compared(self): + self.current = datetime(2026, 8, 6, 1, 0, tzinfo=timezone.utc) # KST 10:00 + self.assertTrue(self.manager.should_save("plant-a", {"kw": 10, "today": 20})) + + with closing(sqlite3.connect(self.manager.db_path)) as conn: + conn.execute( + "UPDATE site_data SET updated_at = ? WHERE site_id = ?", + ("2026-08-06T08:59:59", "plant-a"), + ) + conn.commit() + + self.assertTrue(self.manager.should_save("plant-a", {"kw": 10, "today": 20})) + + +if __name__ == "__main__": + unittest.main() diff --git a/crawler/time_utils.py b/crawler/time_utils.py new file mode 100644 index 0000000..c74d1d8 --- /dev/null +++ b/crawler/time_utils.py @@ -0,0 +1,44 @@ +"""KST 기준 시간 판단과 Supabase 조회 경계를 제공한다.""" + +from datetime import date, datetime, time, timedelta, timezone +from typing import Tuple + + +KST = timezone(timedelta(hours=9), name="KST") +UTC = timezone.utc + + +def ensure_kst(value: datetime) -> datetime: + """datetime을 KST aware 값으로 변환한다. + + 기존 SQLite에 저장된 naive 값은 서버 로컬 시간이 아니라 KST로 기록된 + 값이므로 KST로 간주한다. + """ + if value.tzinfo is None: + return value.replace(tzinfo=KST) + return value.astimezone(KST) + + +def now_kst() -> datetime: + """서버 OS 시간대와 무관한 현재 KST 시각을 반환한다.""" + return datetime.now(KST) + + +def today_kst() -> date: + return now_kst().date() + + +def yesterday_kst() -> date: + return today_kst() - timedelta(days=1) + + +def parse_legacy_kst_datetime(value: str) -> datetime: + """SQLite의 기존 naive ISO 값과 신규 aware ISO 값을 모두 KST로 읽는다.""" + return ensure_kst(datetime.fromisoformat(value)) + + +def kst_day_bounds_utc(target_date: date) -> Tuple[datetime, datetime]: + """KST 하루의 UTC 반개구간 [start, next_start)을 반환한다.""" + start_kst = datetime.combine(target_date, time.min, tzinfo=KST) + next_start_kst = start_kst + timedelta(days=1) + return start_kst.astimezone(UTC), next_start_kst.astimezone(UTC) diff --git a/deploy/nginx/solorpower.conf b/deploy/nginx/solorpower.conf new file mode 100644 index 0000000..477e396 --- /dev/null +++ b/deploy/nginx/solorpower.conf @@ -0,0 +1,37 @@ +# HTTP -> HTTPS +server { + listen 80; + server_name solorpower.dadot.net; + + if ($host = solorpower.dadot.net) { + return 301 https://$host$request_uri; + } + return 404; +} + +# HTTPS application server +server { + listen 443 ssl; + server_name solorpower.dadot.net; + + ssl_certificate /etc/letsencrypt/live/solorpower.dadot.net/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/solorpower.dadot.net/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # FastAPI endpoints. Keep this before the SPA fallback. + location ~ ^/(?:plants(?:/|$)|docs(?:/|$)|redoc(?:/|$)|openapi\.json$|health$) { + proxy_pass http://127.0.0.1:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # React Native Web SPA + location / { + root /var/www/html/dist; + index index.html; + try_files $uri $uri/ /index.html; + } +} diff --git a/docs/backfill_operation_guide.md b/docs/backfill_operation_guide.md index a083e60..45e22ca 100644 --- a/docs/backfill_operation_guide.md +++ b/docs/backfill_operation_guide.md @@ -6,6 +6,10 @@ ## 1. 백필 스케줄 및 인프라 현황 +* **서버 이름:** `holdem-server` +* **공인 IP:** `140.245.73.212` +* **접속 계정:** `ubuntu` +* **SSH 키 경로:** `C:\Users\haneu\.ssh\holdem_server.key` * **배포 위치:** Oracle Cloud 서버 `~/solorpower/crawler/` * **실행 엔진:** 시스템 파이썬 (`/usr/bin/python3`) * **스케줄 설정 (Crontab):** 매시간 30분 작동 @@ -14,6 +18,12 @@ ``` * **1일 수집 속도:** 24회 * 5일 = **하루 약 120일치(4개월 분량)** 역추적 수집 진행 +### 💻 서버 접속 명령어 +```powershell +ssh -i "C:\Users\haneu\.ssh\holdem_server.key" ubuntu@140.245.73.212 +``` + + --- ## 2. 향후 모니터링 및 체크 방법 (서버 터미널용) @@ -26,9 +36,17 @@ tail -n 50 -f ~/solorpower/crawler/backfill_cron.log ### 2-2. SQLite 진행 상태(State) 조회 각 발전소의 마지막 백필 날짜와 연속 무발전 일수, 진행 상태(`RUNNING` 또는 `COMPLETED`)를 데이터베이스에서 덤프합니다. -```bash -/usr/bin/python3 -c "import sqlite3; conn = sqlite3.connect('/home/ubuntu/solorpower/crawler/crawler_manager.db'); cursor = conn.cursor(); cursor.execute('SELECT * FROM backfill_state'); [print(f'발전소: {r[0]:<12} | 마지막날짜: {r[1]} | 연속무발전: {r[2]}일 | 상태: {r[3]}') for r in cursor.fetchall()]; conn.close()" -``` + +* **리눅스(서버 터미널)에서 실행 시:** + ```bash + /usr/bin/python3 -c "import sqlite3; conn = sqlite3.connect('/home/ubuntu/solorpower/crawler/crawler_manager.db'); cursor = conn.cursor(); cursor.execute('SELECT * FROM backfill_state'); [print(f'발전소: {r[0]:<12} | 마지막날짜: {r[1]} | 연속무발전: {r[2]}일 | 상태: {r[3]}') for r in cursor.fetchall()]; conn.close()" + ``` + +* **윈도우(로컬 PowerShell)에서 원격으로 실행 시 (따옴표 에러 방지):** + ```powershell + ssh -i "C:\Users\haneu\.ssh\holdem_server.key" ubuntu@140.245.73.212 "export PYTHONUTF8=1 && /usr/bin/python3 -c \"import sqlite3; conn = sqlite3.connect('/home/ubuntu/solorpower/crawler/crawler_manager.db'); cursor = conn.cursor(); cursor.execute('SELECT * FROM backfill_state'); [print(f'발전소: {r[0]:<12} | 마지막날짜: {r[1]} | 연속무발전: {r[2]}일 | 상태: {r[3]}') for r in cursor.fetchall()]; conn.close()\"" + ``` + --- @@ -68,8 +86,22 @@ cd ~/solorpower/crawler && export PYTHONUTF8=1 && /usr/bin/python3 daily_summary ## 4. 트러블슈팅 및 특이 사항 관리 1. **연속 30일 무발전 마감 (`COMPLETED`)**: - - 만약 특정 발전소의 상태가 너무 빨리 `COMPLETED`로 변경되었다면, 원본 사이트에서 제공하는 과거 이력 조회 가능 일수가 30일 미만으로 제한되어 수집이 마감된 것일 수 있습니다. - - 이때는 수동으로 복구할 수 없으며, 해당 사이트 정책상의 한계입니다. + - 성공적으로 조회된 실제 0kWh만 연속 일수에 포함됩니다. + - 정상 응답에 해당 날짜가 없거나 요청·로그인·파싱이 실패한 경우에는 30일 종료 조건에 포함되지 않습니다. 2. **백필 도중 IP 차단 또는 일시적 네트워크 에러**: - - 크롤러 모듈 실행 에러가 일시적으로 나더라도 스크립트는 해당 일자에 대한 상태를 `RUNNING`으로 유지하며 넘어가거나 에러를 기록합니다. - - 크론 스케줄에 의해 다음 시간에 다시 시도되므로 일시적인 순단 현상은 자동으로 치유됩니다. + - 해당 발전소의 진행 커서와 무발전 횟수를 변경하지 않고 그 발전소의 이번 실행을 중단합니다. + - 다른 발전소는 계속 처리하며, 다음 cron 실행에서 실패한 발전소의 같은 날짜부터 재시도합니다. +3. **Supabase 조회 또는 저장 실패**: + - 진행 커서를 갱신하지 않으므로 다음 실행에서 같은 날짜가 다시 처리됩니다. + - 기존 일간 통계보다 작거나 같은 값은 기존 값을 유지하고, 더 큰 값만 갱신합니다. + +--- + +## 5. 2026-08-06 운영 검증 결과 + +- 9개 발전소의 `backfill_state`는 모두 `COMPLETED`이며 실행 중인 작업은 없습니다. +- KREMC 원본 첫 데이터는 2018-10-09 86kWh로 DB 최초 날짜와 일치합니다. +- CMSolar 원본 첫 데이터는 2020-09-22 50kWh로 DB 최초 날짜와 일치합니다. +- 2026-08-05 원본 값은 KREMC 160kWh, CMSolar 127kWh로 운영 DB 및 일일 마감 결과와 일치합니다. +- 운영 서버에서 백필·저장 보호·사이트 오류 계약을 포함한 테스트 23개가 통과했습니다. +- 완료된 백필 cron은 매시간 무작업 실행 중입니다. 비활성화는 운영 스케줄 변경이므로 별도 결정 후 수행합니다. diff --git a/docs/development_and_testing.md b/docs/development_and_testing.md new file mode 100644 index 0000000..56a15ba --- /dev/null +++ b/docs/development_and_testing.md @@ -0,0 +1,76 @@ +# 개발 환경 및 테스트 가이드 + +이 문서는 새 개발 환경과 CI에서 SolarPower 프로젝트를 같은 방법으로 설치하고 검증하기 위한 기준이다. 실제 Supabase 자격 증명이나 운영 서버 연결 없이도 기본 회귀 테스트를 실행할 수 있다. + +## 지원 환경 + +- Crawler: Python 3.10 또는 3.11 +- API: Python 3.11 +- App: Node.js 20, npm +- DB migration test: PostgreSQL 15 (`psql` 또는 Docker 사용) + +프로젝트의 `.env` 파일은 설치 및 단위 테스트에 필요하지 않다. 운영 작업을 수행할 때만 별도로 주입한다. + +## Crawler + +`crawler/requirements.in`에는 crawler, 과거 데이터 복구, 일일 통계 작업의 직접 의존성과 Python 3.10 호환 제약이 기록되어 있다. `crawler/requirements.txt`는 새 환경에서 해석하고 검증한 전체 고정 버전 목록이며 설치와 CI는 이 파일을 사용한다. + +```powershell +cd crawler +python -m venv .venv +.\.venv\Scripts\python -m pip install -r requirements.txt +.\.venv\Scripts\python -m unittest discover -s tests -p "test_*.py" -v +``` + +Linux/macOS에서는 마지막 두 명령의 Python 경로를 `.venv/bin/python`으로 바꾼다. 외부 사이트 응답 파서 테스트는 `crawler/tests/fixtures/`의 고정 응답을 사용하므로 네트워크나 실제 계정에 의존하지 않는다. + +## API server + +`api_server/requirements.in`은 직접 의존성의 기준이고, `api_server/requirements.txt`는 배포 및 CI가 사용하는 고정 버전 목록이다. + +```powershell +cd api_server +python -m venv .venv +.\.venv\Scripts\python -m pip install -r requirements.txt +$env:SUPABASE_URL = "http://localhost:54321" +$env:SUPABASE_KEY = "test-key" +$env:DEBUG = "false" +.\.venv\Scripts\python -m unittest discover -s tests -p "test_*.py" -v +``` + +위 값은 설정 로딩을 위한 테스트용 값이며 실제 DB 연결에는 사용되지 않는다. + +## App + +`package-lock.json`을 저장소에 포함하고 CI와 새 환경에서는 `npm ci`를 사용한다. + +```powershell +cd app +npm ci +npm run test:ci +``` + +현재 `test:ci`는 Expo 웹 export를 수행하여 의존성, 번들링, 정적 산출물 생성을 함께 검사한다. 출력 디렉터리는 `app/dist/`이며 Git에서 제외된다. + +## DB migration + +통계 쓰기 일관성 migration은 독립 PostgreSQL 15 DB에서 다음 계약 테스트로 확인한다. + +```powershell +$env:PGPASSWORD = "postgres" +cd supabase/tests +psql -h localhost -U postgres -d postgres -f stats_write_consistency_test.sql +``` + +테스트 SQL이 필요한 최소 스키마를 만들고 migration을 적용한 뒤 RPC, 일별·월별 집계 trigger와 기존 데이터 보존 조건을 검증한다. + +## CI 기준 + +`.github/workflows/ci.yml`은 push와 pull request마다 다음 작업을 독립 실행한다. + +1. Crawler: Python 3.10/3.11 전체 단위·fixture 테스트 +2. API: Python 3.11 전체 단위 테스트 +3. Database: PostgreSQL 15 migration 계약 테스트 +4. App: Node 20에서 `npm ci` 후 Expo 웹 build + +CI에는 운영 비밀값을 등록하지 않는다. 실제 외부 사이트, 운영 Supabase, Telegram 전송은 배포 후 별도의 운영 확인 절차로 검증한다. diff --git a/docs/project_improvement_plan.md b/docs/project_improvement_plan.md new file mode 100644 index 0000000..027e057 --- /dev/null +++ b/docs/project_improvement_plan.md @@ -0,0 +1,743 @@ +# SolarPower 개선 작업 계획 + +작성일: 2026-08-06 + +## 1. 목적 + +현재 운영 중인 SolarPower 시스템을 중단 없이 개선하기 위한 순차 작업 계획이다. 데이터 유실 또는 잘못된 집계를 일으킬 수 있는 문제를 먼저 처리하고, 이후 앱과 API의 기능 일관성, 시간대, 유지보수성과 테스트 체계를 개선한다. + +인증 토큰 교체, 외부 서비스 계정의 환경변수 이전, Git 이력 정리 등 비밀정보 관련 작업은 현재 저장소를 단독으로 사용하고 있다는 전제하에 후순위로 보류한다. API 인증과 Supabase 접근 정책 강화도 별도 보안 단계에서 함께 검토한다. + +## 현재 진행 현황 + +최종 갱신: 2026-08-07 12:01 KST + +| 단계 | 작업 | 상태 | 현재 결과 | +|---:|---|---|---| +| 0 | 운영 서버 연결 및 배포 기준선 점검 | 완료 | 외부 사이트, NAS 프록시, 크롤러, Supabase, FastAPI, Nginx와 웹 연결 경로 확인 | +| 1 | 역방향 백필 데이터 무결성 | 완료·운영 반영 | 실패 시 커서 유지, 실제 0·데이터 없음·조회 오류 분리, 기존 정상값 보호 | +| 2 | 발전소 알림 설정 연동 | 완료·운영 반영 | `alerts_enabled` 연동, 3회 연속 0kW 판정, 수집 오류 오탐 제외 및 실제 설정 ON/OFF 검증 | +| 3 | Excel 업로드 화면과 API 계약 | 완료·운영 반영 | 일간·월간 업로드 통합, 입력 검증과 4xx 응답 정리, Web/API 배포 완료 | +| 4 | KST 시간대 처리 통일 | 완료·운영 반영 | 크롤러·API의 KST 판단 통일, UTC 반개구간 조회와 자정·월말·연말 검증 완료 | +| 5 | API 정확성과 응답 규약 | 완료·운영 반영 | 문자열 ID, 입력 오류, 실제 DB health, 응답 모델, 최신 로그 단일 조회와 동기 호출 처리 완료 | +| 6 | 통계 생성 경로 일관성 | 완료·운영 반영 | 공통 일간 저장 정책, 월간 자동 집계, 출처·갱신 시각과 오타 컬럼 마이그레이션 완료 | +| 7 | 테스트와 재현 가능한 개발 환경 | 완료·운영 반영 | Python/npm 의존성 고정, 5개 사이트 응답 fixture, 4개 CI job과 새 환경 설치 검증 완료 | +| 8 | 저장소 및 코드 구조 정리 | 대기 | 기능·데이터 안정화 후 진행 | + +현재 운영 상태: + +- 2026-08-05 NREMS 3·4·9호기 알림은 발전소 장애가 아니라 NAS 프록시 타임아웃을 오류성 0kW로 처리한 오탐으로 확인했고, 오류 데이터를 알림 판정에서 제외하도록 수정했다. +- 운영 API와 Nginx는 active 상태이며 공개 health는 실제 Supabase 조회를 포함한 JSON을 반환하고 전체 비교 통계와 시간별 통계도 HTTP 200을 반환한다. +- 2026-08-06 18:00 KST 정기 크롤링에서 9개 발전소 연결이 모두 성공했고 수집 오류나 텔레그램 오탐은 발생하지 않았다. +- 2026-08-07 10:50 KST 정기 크롤링에서 새 통계 저장 함수로 9개 발전소의 실시간·일간 저장이 모두 성공했고, 일·월 통계 72쌍이 전부 일치했다. +- 2026-08-07 12:00 KST 정기 크롤링은 SQLite 연결 종료 수정 반영 후 9개 발전소가 모두 정상 상태였고, 변경된 8개 발전소의 로그·일간 통계를 오류 없이 저장했다. +- 크롤러 시간대·알림·백필·통계·응답 fixture 회귀 테스트 36개가 Windows, Linux 컨테이너와 운영 서버에서 통과했고 API 계약·시간대·업로드 회귀 테스트 31개도 새 Python 환경에서 통과했다. +- 1~6단계 배포 전 운영 파일은 단계별 `deploy_backups/20260806_*`, `deploy_backups/20260807_*` 디렉터리에 보존했다. +- 완료된 백필 cron은 현재도 매시간 무작업 실행 중이며, 비활성화 여부는 별도 운영 결정으로 남겨 두었다. +- 인증 토큰 교체, 환경변수 정리, API 인증과 Supabase 접근 정책 강화는 사용자 결정에 따라 후순위로 보류 중이다. +- 현재 변경은 로컬과 운영 서버의 미커밋 작업 트리에 존재하며 Git 커밋은 아직 생성하지 않았다. + +다음 진행 기준: + +- 다음 구현 단계는 8단계 `저장소 및 코드 구조 정리`이다. +- 단계별 세부 원인, 수정 내용, 테스트와 배포 기록은 이 문서의 `6. 운영 서버 기준선 점검 결과`와 `7. 변경 기록`에 보존한다. + +## 2. 작업 원칙 + +- 각 단계는 원인 재현, 수정, 로컬 검증, 운영 반영 여부 확인 순서로 진행한다. +- 데이터 변경 로직은 실패 시 진행 상태를 갱신하지 않는 것을 기본 원칙으로 한다. +- 모든 날짜와 시간 판단은 KST 기준을 명시하고, DB 조회 시 UTC로 변환한다. +- 현재 작업 트리의 미커밋 변경을 보존하고 관련 파일을 수정할 때 먼저 diff를 확인한다. +- 한 단계가 검증되기 전에는 다음 단계와 한 커밋에 섞지 않는다. +- 운영 DB를 사용하는 검증은 dry-run 또는 조회 전용 확인을 먼저 수행한다. + +## 3. 작업 순서 + +### 0단계: 운영 서버 연결 및 배포 기준선 점검 + +상태: 완료 + +대상: + +- Oracle Cloud 운영 서버 +- Nginx 및 FastAPI systemd 서비스 +- 실시간 크롤러 및 역방향 백필 cron +- Tailscale 및 NAS 프록시 경로 +- FastAPI/크롤러의 Supabase 연결 +- 운영 웹 배포 디렉터리 + +검토 항목: + +- SSH 접속, 서버 시간대, 호스트 정보와 기본 자원 상태를 확인한다. +- 운영 배포 경로, Git 브랜치, 마지막 커밋과 미커밋 변경을 확인한다. +- `solar-api`, Nginx 서비스 상태와 실제 리슨 포트를 확인한다. +- 로컬 및 공개 주소의 health/API 응답을 각각 확인한다. +- API 데이터 조회를 통해 FastAPI에서 Supabase까지의 실제 연결을 간접 검증한다. +- Nginx의 정적 웹 루트, API 프록시 경로와 TLS 인증서 상태를 확인한다. +- 실시간 크롤러, 일일 집계, 백필 cron 등록 내용과 최근 로그를 확인한다. +- Tailscale 연결 상태와 NAS 프록시 도달 가능 여부를 확인한다. +- 로컬 저장소와 운영 배포본의 커밋 및 미커밋 차이를 기록한다. +- 점검 과정에서 비밀 환경변수의 실제 값은 출력하지 않는다. + +완료 기준: + +- 앱에서 외부 모니터링 사이트까지 이어지는 각 연결 구간의 성공 또는 실패 상태가 기록된다. +- 현재 운영 중인 코드 버전과 자동 실행 스케줄을 식별한다. +- 서비스 장애 또는 배포 차이를 발견하면 코드 수정 전에 영향도와 복구 방법을 정리한다. +- 운영 상태를 변경하는 명령 없이 읽기 전용 점검을 완료한다. + +### 1단계: 역방향 백필 데이터 무결성 + +상태: 완료 + +대상: + +- `crawler/backward_backfill.py` +- `crawler/database.py` +- `crawler/crawler_manager.py` +- 백필 관련 테스트 및 운영 문서 + +검토 및 수정 항목: + +- 초기 기준일을 첫 수집 전에 하루 차감하여 어제 데이터가 누락되는 커서 의미를 수정한다. +- 원본의 실제 0kWh, 원본 데이터 없음, 네트워크/파싱 오류를 서로 다른 결과로 구분한다. +- 네트워크 오류를 연속 무발전 일수에 포함하지 않는다. +- Supabase 저장 성공을 확인한 경우에만 SQLite 진행 날짜를 갱신한다. +- 기존 양수 데이터를 0 또는 더 작은 값으로 덮어쓰지 않도록 백필 저장 정책을 정의한다. +- 발전소 한 곳의 오류가 다른 발전소의 백필 진행을 막지 않도록 유지한다. +- 재시작, 부분 실패, 가동개시일 도달, 30일 연속 실제 무발전 조건을 테스트한다. + +완료 기준: + +- 지정한 시작일이 첫 번째 수집 대상이 된다. +- 요청 또는 DB 저장 실패 후 재실행하면 같은 날짜부터 재시도한다. +- 30일 연속 종료 조건은 성공적으로 조회된 실제 0값에만 적용된다. +- 기존 정상 통계가 백필의 오류성 0값으로 덮어써지지 않는다. + +### 2단계: 발전소 알림 설정 연동 + +상태: 완료 + +대상: + +- `crawler/config.py` +- `crawler/sync_plants.py` +- `crawler/alert_manager.py` +- `crawler/main.py` + +검토 및 수정 항목: + +- 크롤러의 문자열 업체 키(`sunwind`)와 DB의 숫자형 `company_id`를 분리한다. +- 발전소 ID가 DB 기본키이므로 알림 활성화 조회에 업체 조건이 실제로 필요한지 검토한다. +- 앱에서 `alerts_enabled=false`로 변경했을 때 크롤러가 알림을 보내지 않는지 검증한다. +- 현재 미커밋 상태인 3회 연속 0kW 및 누적 발전량 증가 기반 오탐 방지 로직을 상태 전이 테스트로 검증한다. +- Supabase 조회 실패 시 알림을 계속 진행할지 중단할지 명시적인 장애 정책을 정한다. + +완료 기준: + +- 앱에서 알림을 끈 발전소는 실제 텔레그램 알림 대상에서 제외된다. +- 0kW 단발값은 알림을 발생시키지 않는다. +- 정상 복구 후 다음 장애를 다시 감지할 수 있다. + +### 3단계: Excel 업로드 화면과 API 계약 정리 + +상태: 완료 + +대상: + +- `app/screens/PlantDetailScreen.js` +- `app/components/UploadModal.js` +- `api_server/app/routers/upload.py` + +검토 및 수정 항목: + +- 상세 화면에서 항상 월간 업로드 API를 호출하는 문제를 수정한다. +- 기존 `UploadModal`을 실제 화면에 연결하거나, 중복 구현을 제거하고 하나의 업로드 흐름으로 통합한다. +- 일간 형식(`date`, `generation`)과 월간 형식(`year`, `month`, `kwh`)을 UI에서 명확히 구분한다. +- Web과 Native의 `FormData` 처리 방식을 각각 검증한다. +- 파일명 누락, 잘못된 확장자, 빈 파일, 필수 열 누락, 잘못된 날짜를 4xx 오류로 반환한다. +- 업로드 파일 크기 및 행 수 제한을 정한다. +- `fillna(method='ffill')` 등 향후 제거 예정인 pandas 사용법을 정리한다. + +완료 기준: + +- 일간 및 월간 샘플 파일이 각각 올바른 API로 전송된다. +- 화면 안내와 실제 필수 열이 일치한다. +- 잘못된 입력이 서버 500 오류가 아닌 설명 가능한 4xx 응답으로 처리된다. + +### 4단계: KST 시간대 처리 통일 + +상태: 완료 + +대상: + +- `crawler/crawler_manager.py` +- `crawler/alert_manager.py` +- `crawler/main.py` +- `crawler/daily_summary.py` +- `crawler/backward_backfill.py` +- `api_server/app/routers/stats.py` + +검토 및 수정 항목: + +- 공통 KST 시간대 유틸리티를 만들고 naive `datetime.now()` 사용을 제거한다. +- 야간 크롤링 차단, 알림 시간, 일일 마감, 기본 조회 날짜, 백필의 어제 계산을 모두 KST 기준으로 통일한다. +- Supabase `timestamptz` 조회 범위는 KST 경계를 UTC ISO 문자열로 변환한다. +- KST 자정 전후와 월말/연말 경계를 테스트한다. + +완료 기준: + +- 서버 OS 시간대가 UTC여도 동일한 수집 및 통계 결과를 낸다. +- KST 날짜의 00:00~23:59 로그만 해당 일 통계에 포함된다. + +### 5단계: API 정확성과 응답 규약 + +상태: 완료 + +대상: + +- `api_server/app/routers/plants.py` +- `api_server/app/routers/stats.py` +- `api_server/app/routers/upload.py` +- `api_server/app/main.py` +- `api_server/app/schemas/` + +검토 및 수정 항목: + +- 문자열 발전소 ID를 받는 상세 API의 `plant_id: int` 선언을 수정한다. +- 잘못된 날짜와 범위 파라미터를 조용히 오늘로 대체하지 않고 400 또는 422로 반환한다. +- `HTTPException`이 일반 예외 처리에 잡혀 500으로 바뀌는 경로를 정리한다. +- 단순 URL 존재 여부가 아닌 실제 DB 연결 상태를 health check에 반영한다. +- 응답 모델과 오류 형식을 가능한 범위에서 통일한다. +- 발전소별 최신 로그 N+1 조회를 일괄 조회로 바꿀 수 있는지 검토한다. +- 동기 Supabase 호출이 `async` 요청 처리에 미치는 영향을 측정한 뒤 스레드풀 또는 동기 핸들러 적용을 결정한다. + +완료 기준: + +- 주요 API에 정상, 데이터 없음, 잘못된 입력, DB 오류 테스트가 존재한다. +- 공개된 OpenAPI 스키마와 실제 응답이 일치한다. + +### 6단계: 통계 생성 경로 일관성 + +상태: 완료·운영 반영 (2026-08-07) + +대상: + +- `crawler/database.py` +- `crawler/daily_summary.py` +- `crawler/fetch_history.py` +- `api_server/app/routers/upload.py` +- DB 스키마 및 마이그레이션 + +검토 및 수정 항목: + +- 실시간 수집, 일일 마감, 과거 백필, Excel 업로드가 동일한 덮어쓰기 정책을 사용하도록 정리한다. +- 실시간 경로에만 적용된 MAX 보호가 다른 저장 경로에도 필요한지 결정한다. +- 원본 사이트 보정값과 로그 기반 집계값 중 어떤 값을 우선할지 기록 가능한 정책으로 만든다. +- `daily_stats` 변경 후 `monthly_stats` 갱신 책임을 한 곳으로 모은다. +- `created_at`, `updated_at`, `peak_kw`, `generation_hours` 갱신 규칙을 통일한다. +- `currnet_last_date` 오타 컬럼의 사용 여부와 제거 또는 마이그레이션 필요성을 확인한다. + +완료 기준: + +- 동일 날짜를 어떤 경로로 다시 처리해도 더 신뢰도 높은 기존 데이터가 손상되지 않는다. +- 일간 데이터 수정 후 월간 합계가 일관되게 갱신된다. + +적용 결과: + +- 실시간 수집, 일일 마감, 과거 백필과 일간 Excel 업로드가 `upsert_daily_stats` DB 함수를 공통으로 사용한다. +- 자동 수집 경로는 기존보다 작은 일 발전량으로 덮어쓰지 않고, 사용자가 명시적으로 수행하는 일간 Excel 보정만 하향 수정을 허용한다. +- 일일 마감에서 원본 사이트 값이 로그 최댓값보다 큰 경우만 상향 보정하며, 작은 원본 값은 로그 집계값을 유지한다. +- `daily_stats` 변경 트리거가 영향받은 월의 `monthly_stats`를 즉시 재집계한다. Excel·원본 월간값은 권위 있는 월간 입력으로 표시하여 자동 일간 집계가 덮어쓰지 않는다. +- `daily_stats`에 `updated_at`, `source`를 추가하고 발전시간은 선택된 발전량과 발전소 용량으로 계산한다. 최고 출력은 저장 경로 간 최댓값을 유지한다. +- `monthly_stats.currnet_last_date`를 `last_date date`로 바로잡고 `source`를 추가했다. 기존 과거 월간 총액은 보존하고 현재 월 누락 행만 생성했다. +- 운영 적용 전 2026년 통계를 진단한 결과 1~7월 63쌍은 모두 일치했고 8월 9개 발전소의 월간 행만 누락되어 있었다. 적용 후 1~8월 72쌍의 불일치와 누락은 모두 0건이다. + +### 7단계: 테스트와 재현 가능한 개발 환경 + +상태: 대기 + +대상: + +- 크롤러 및 API 테스트 디렉터리 +- `crawler/requirements.txt` 또는 공통 Python 의존성 정의 +- `app/package-lock.json` +- CI 설정 + +검토 및 수정 항목: + +- 크롤러별 HTTP 응답 fixture를 사용한 파싱 테스트를 만든다. +- SQLite 임시 DB와 가짜 Supabase 클라이언트를 사용해 스케줄러, 알림, 백필 상태 전이를 테스트한다. +- FastAPI 주요 엔드포인트 테스트를 추가한다. +- 프론트엔드 lockfile을 추적하여 설치 버전을 고정한다. +- 크롤러 실행에 필요한 최소 Python 의존성을 별도로 정의한다. +- 테스트와 기본 정적 검사를 수행하는 CI를 추가한다. + +완료 기준: + +- 외부 사이트와 운영 DB에 접속하지 않고 핵심 로직을 검증할 수 있다. +- 새 환경에서 문서화된 명령으로 의존성을 재현할 수 있다. + +### 8단계: 저장소 및 코드 구조 정리 + +상태: 대기 + +대상: + +- 추적 중인 `crawler/venv_win/` +- `app/server.log` +- 이전 크롤러 및 백업 파일 +- 대형 React Native 화면 컴포넌트 + +검토 및 수정 항목: + +- Git에 추적된 가상환경 실행 파일과 로그 파일을 제거하고 ignore 규칙을 확인한다. +- `*_old.py`, `*.backup`, 일회성 스크립트는 보존 필요성을 확인한 후 archive 또는 Git 이력으로 정리한다. +- API 기본 URL을 환경별 설정으로 이동한다. +- 상세 화면의 API 호출, 업로드, 차트 포맷팅 로직을 hooks와 컴포넌트로 분리한다. +- 현재 단일 업체 ID `1` 하드코딩을 제거하고 향후 다중 업체 확장 경계를 정한다. + +완료 기준: + +- 저장소에 실행 환경 산출물과 런타임 로그가 추적되지 않는다. +- 화면 컴포넌트와 데이터 접근 로직이 독립적으로 수정 및 테스트 가능하다. + +### 9단계: 보안 개선 — 후순위 보류 + +상태: 보류 + +후속 검토 항목: + +- 텔레그램 토큰과 외부 모니터링 계정 비밀번호 교체 +- 모든 비밀정보의 환경변수 또는 비밀 저장소 이전 +- 과거 Git 이력에 포함된 비밀정보 처리 +- Excel 업로드와 알림 설정 변경 API 인증 +- Supabase anon 쓰기 정책 제거 및 최소 권한 재설계 +- 운영 CORS 허용 출처 제한 +- 요청 크기 제한, rate limiting, 감사 로그 + +## 4. 권장 커밋 단위 + +각 단계는 가능하면 다음 단위로 나눈다. + +1. 실패를 재현하는 테스트 또는 검증 스크립트 +2. 최소 기능 수정 +3. 회귀 테스트 및 운영 문서 갱신 + +긴급 수정이 필요한 경우에도 데이터 로직, UI 변경, 문서 변경은 가능한 한 별도 커밋으로 유지한다. + +## 5. 진행 기록 + +| 단계 | 상태 | 시작일 | 완료일 | 비고 | +|---|---|---|---|---| +| 0. 운영 서버 연결 및 배포 기준선 | 완료 | 2026-08-06 | 2026-08-06 | 아래 기준선 점검 결과 참조 | +| 1. 역방향 백필 무결성 | 완료 | 2026-08-06 | 2026-08-06 | 상태 전이·저장 보호·원본 시작일·운영 dry-run 검증 완료 | +| 2. 알림 설정 연동 | 완료 | 2026-08-06 | 2026-08-06 | 코드·단위 테스트·정기 실행·실제 OFF/ON 토글 검증 완료 | +| 3. Excel 업로드 계약 | 완료 | 2026-08-06 | 2026-08-06 | 공통 모달 연결, 입력 제한, API 테스트, 웹·API 배포 완료 | +| 4. KST 시간대 통일 | 완료 | 2026-08-06 | 2026-08-06 | 공통 KST 유틸리티, UTC 반개구간과 경계 테스트, 운영 cron 검증 완료 | +| 5. API 정확성과 응답 | 완료 | 2026-08-07 | 2026-08-07 | 문자열 ID, 응답 모델, DB health, 단일 최신 로그 조회와 운영 배포 완료 | +| 6. 통계 생성 경로 일관성 | 완료 | 2026-08-07 | 2026-08-07 | 공통 DB 저장 함수·월간 트리거·출처 필드·오타 마이그레이션과 운영 검증 완료 | +| 7. 테스트 및 개발 환경 | 다음 작업 | | | | +| 8. 저장소 및 코드 구조 | 대기 | | | | +| 9. 보안 개선 | 보류 | | | 단독 저장소 사용 중이므로 후순위 | + +## 6. 운영 서버 기준선 점검 결과 + +점검 시각: 2026-08-06 16:40~17:00 KST + +점검 방식: Oracle Cloud 운영 서버에 SSH로 접속하여 상태를 변경하지 않는 읽기 전용 명령만 실행했다. 환경변수와 인증정보의 실제 값은 출력하지 않았다. + +### 6.1 서버 및 배포 상태 + +- 호스트: `holdem-server` +- 서버 시간대: `Asia/Seoul` +- NTP 동기화: 정상 +- 가동 시간: 약 197일 +- 루트 디스크: 45GB 중 13GB 사용, 사용률 28% +- 운영 저장소: `/home/ubuntu/solorpower` +- 브랜치: `main` +- 운영 커밋: `b834cc4` (`docs: add historical backfill operation and verification guide`) +- 로컬 저장소와 운영 서버의 HEAD 커밋은 동일하다. +- 운영 서버에는 `crawler/alert_manager.py`, `crawler/config.py`, `crawler/main.py` 미커밋 변경이 존재한다. +- 서버의 diff는 줄바꿈 차이까지 포함되어 크게 표시되므로 배포 또는 커밋 전에 실제 의미 변경과 CRLF/LF 변경을 분리해야 한다. + +### 6.2 API, Nginx 및 TLS + +- `solar-api`: active/enabled, 재시작 횟수 0 +- `solar-api` 실행 경로: `/home/ubuntu/solorpower/api_server/venv/bin/uvicorn` +- `solar-api` 작업 경로: `/home/ubuntu/solorpower/api_server` +- API 리슨 주소: `127.0.0.1:8000` +- Nginx: active/enabled, `0.0.0.0:80` 및 `0.0.0.0:443` 리슨 +- 로컬 API `/health`: HTTP 200 및 JSON 응답 +- 공개 `/plants/1`: HTTP 200 및 JSON 응답. FastAPI에서 Supabase까지의 실제 조회 경로가 정상임을 확인했다. +- 공개 `/health`: HTTP 200이지만 API JSON이 아닌 SPA `index.html`을 반환한다. 현재 Nginx 프록시 정규식에 `health`가 포함되지 않았기 때문이다. +- TLS 인증서: Let's Encrypt, `solorpower.dadot.net`, 2026-10-18 만료 +- 최근 24시간 `solar-api` warning 이상 journal 항목은 없었다. + +후속 조치: + +- 5단계 API 작업에서 Nginx 프록시 대상에 `/health`와 필요 시 `/redoc`을 추가한다. +- health check가 실제 Supabase 쿼리를 수행하도록 개선한 뒤 외부 모니터링 경로로 사용한다. + +### 6.3 웹 배포 상태 + +- 웹 루트: `/var/www/html/dist` +- 공개 루트: HTTP 200, `text/html` +- 운영 `index.html` 수정 시각: 2026-02-12 10:53 KST +- 운영 번들에는 전체 발전소 비교와 월간 업로드 기능이 포함되어 있다. +- 운영 번들에는 현재 로컬 `App.js`의 `alerts_enabled` 기능이 포함되어 있지 않다. + +후속 조치: + +- 2단계 알림 설정 연동을 수정하고 검증한 뒤 앱을 새로 빌드하여 운영 웹 배포본을 동기화한다. + +### 6.4 자동 실행 및 최근 로그 + +운영 cron: + +```cron +*/10 * * * * cd ~/solorpower/crawler && /usr/bin/python3 main.py >> crawler.log 2>&1 +10 0 * * * cd ~/solorpower/crawler && /usr/bin/python3 daily_summary.py >> summary.log 2>&1 +30 * * * * cd ~/solorpower/crawler && export PYTHONUTF8=1 && /usr/bin/python3 backward_backfill.py --days 5 --delay 2.0 >> backfill_cron.log 2>&1 +``` + +- 실시간 크롤러 로그는 점검 시각까지 10분마다 갱신되고 있었다. +- 최근 실행마다 9개 발전소의 `solar_logs` 저장 성공을 확인했다. +- 일일 집계 로그에서 최근 대상일의 9개 발전소 저장 성공을 확인했다. +- Supabase URL과 키 환경변수의 설정 여부를 확인했으며 둘 다 설정되어 있다. +- `USE_PROXY=true`가 서버 `.env`에 설정되어 있다. +- 로그에 `company_id="sunwind"`를 bigint와 비교하는 오류가 발전소마다 반복되고 있다. 앱의 알림 비활성화 설정이 크롤러에 반영되지 않는 문제가 운영 환경에서도 재현되었다. + +후속 조치: + +- 2단계 알림 설정 연동을 데이터 백필 다음 우선순위로 유지한다. +- 로그에서 Supabase URL prefix를 출력하는 디버그 문구를 제거한다. + +### 6.5 Tailscale, NAS 프록시 및 외부 사이트 + +- Oracle 서버 Tailscale 주소: `100.116.0.32` +- NAS 프록시 노드 `100.83.7.81`이 Tailscale peer로 확인된다. +- NAS 프록시 TCP 3128 포트 연결에 성공했다. +- 서버의 `USE_PROXY=true` 설정과 코드의 프록시 주소가 일치한다. +- NAS 프록시를 경유한 비인증 기본 URL 점검 결과 NREMS, KREMC, Sun-WMS, Hyundai, CMSolar 모두 HTTP 200을 반환했다. +- 최근 실시간 크롤러가 9개 결과를 저장했으므로 인증을 포함한 실제 크롤러 경로도 현재 동작 중인 것으로 판단한다. + +### 6.6 백필 상태와 운영 데이터 범위 + +- SQLite `backfill_state`의 9개 발전소가 모두 `COMPLETED` 상태다. +- 완료 이후에도 백필 cron이 매시간 실행되어 `실행 중인 작업이 없습니다`만 기록하고 있다. +- 일부 사이트는 연속 30일 0값 조건으로 완료되었으나 Supabase에는 더 오래된 데이터가 이미 존재하여 SQLite 상태만으로 전체 데이터 완전성을 판단할 수 없다. + +운영 `daily_stats` 범위: + +| 발전소 | 건수 | 최초 날짜 | 최종 날짜 | +|---|---:|---|---| +| nrems-01 | 4,512 | 2014-03-31 | 2026-08-06 | +| nrems-02 | 4,512 | 2014-03-31 | 2026-08-06 | +| nrems-03 | 3,881 | 2015-12-22 | 2026-08-06 | +| nrems-04 | 3,495 | 2017-01-11 | 2026-08-06 | +| kremc-05 | 2,791 | 2018-10-09 | 2026-08-06 | +| sunwms-06 | 2,412 | 2019-12-30 | 2026-08-06 | +| hyundai-08 | 2,374 | 2020-02-06 | 2026-08-06 | +| nrems-09 | 2,109 | 2020-10-28 | 2026-08-06 | +| cmsolar-10 | 2,115 | 2020-09-22 | 2026-08-06 | + +확인 결과: + +- `kremc-05`는 원본에서 2018-06-28과 2018-10-08 데이터가 없고, 2018-10-09의 첫 데이터 86kWh가 확인됐다. DB 최초 날짜와 일치하므로 백필 누락이 아니다. +- `cmsolar-10`은 원본에서 2020-08-31과 2020-09-21 데이터가 없고, 2020-09-22의 첫 데이터 50kWh가 확인됐다. DB 최초 날짜와 일치하므로 백필 누락이 아니다. +- 같은 원본 조회 도구로 2026-08-05의 KREMC 160kWh와 CMSolar 127kWh도 재확인했다. +- 백필 작업이 모두 완료되었으므로 데이터 검증 후 cron을 비활성화할지 결정한다. 변경은 별도 승인 후 수행한다. + +### 6.7 2026-08-05 NREMS 3·4·9호기 오탐 분석 + +사용자 확인에 따르면 실제 NREMS 사이트의 발전 상태에는 문제가 없었지만 3·4·9호기 장애 알림이 전송되었다. 운영 로그와 당일 집계 데이터를 대조한 결과 발전소 장애가 아니라 NAS 프록시 연결 장애를 발전소 0kW로 잘못 분류한 오탐으로 확인되었다. + +발생 타임라인: + +| KST 시각 | 관측 내용 | +|---|---| +| 14:30 | 3호기 71.10kW, 4호기 50.70kW, 9호기 70.72kW로 정상 수집 | +| 14:40 | NAS 프록시 `100.83.7.81:3128` 연결 타임아웃, 각 호기 0kW 의심 1회 | +| 14:50 | 동일 프록시 타임아웃, 0kW 의심 2회 | +| 15:00 | 동일 프록시 타임아웃, 0kW 3회 연속으로 텔레그램 알림 전송 | +| 15:10 | 프록시 타임아웃 지속 | +| 15:20 | 연결 복구. 3호기 61.30kW, 4호기 43.37kW, 9호기 62.76kW 정상 수집 | + +같은 시간 NREMS 1·2호기뿐 아니라 KREMC, Sun-WMS, CMSolar도 동일 NAS 프록시 타임아웃을 기록했다. 따라서 특정 발전소 또는 NREMS 원본 사이트 장애가 아닌 공통 프록시 경로 장애다. + +3·4·9호기에만 텔레그램 알림이 전송된 이유: + +- NREMS 비분할 발전소의 예외 처리 경로는 수집 실패 시 `kw=0`, `today=0`, `status='🔴 오류'`인 결과를 생성한다. +- `main.py`는 결과의 오류 상태를 확인하지 않고 `AlertManager.check_and_alert()`에 0값을 전달한다. +- `AlertManager`는 오류/미수집 상태와 실제 정상 응답의 0kW를 구분하지 않아 세 번의 프록시 오류를 발전소 정지로 판단한다. +- NREMS 1·2호기 분할 경로와 다른 사이트 크롤러는 같은 예외에서 결과를 반환하지 않아 알림 검사 자체가 호출되지 않았다. 이로 인해 크롤러별 실패 처리도 일관되지 않다. + +데이터 영향: + +- 14:40의 3·4·9호기 오류 레코드는 장애 이력 추적 목적으로 `solar_logs`에 한 번 저장되었다. +- `database.py`의 오류 상태 보호 로직으로 해당 0값은 `daily_stats`를 덮어쓰지 않았다. +- 동일 오류값이 반복된 14:50~15:10에는 중복 저장 방지 로직이 적용되었다. +- 15:20 정상값 수집 후 상태가 복구되었다. +- 8월 6일 00:10 일일 마감에서 로그 최댓값과 원본 사이트 일 통계를 대조했으며 9개 발전소 모두 저장에 성공했다. + +2026-08-05 최종 일 발전량: + +| 발전소 | 최종 발전량(kWh) | 원본 보정 결과 | +|---|---:|---| +| nrems-01 | 188.0 | 일치 | +| nrems-02 | 186.0 | 일치 | +| nrems-03 | 432.0 | 일치 | +| nrems-04 | 339.0 | 일치 | +| kremc-05 | 160.0 | 일치 | +| sunwms-06 | 287.7 | 일치 | +| hyundai-08 | 389.5 | 일치 | +| nrems-09 | 533.2 | 일치 | +| cmsolar-10 | 127.0 | 일치 | + +수정 방향: + +1. 크롤러 결과에 `data_valid` 또는 명확한 수집 상태를 추가하여 `오류/미수집`, `정상 0kW`, `정상 발전`을 구분한다. +2. 수집 오류 상태는 발전소 0kW 카운터를 증가시키지 않는다. +3. 공통 프록시 또는 여러 사이트의 동시 실패는 발전소 장애가 아닌 수집 인프라 장애로 한 번만 알린다. +4. 공통 HTTP 세션에 연결 오류 재시도와 backoff를 추가한다. +5. 모든 크롤러가 동일한 실패 결과 규약을 사용하도록 통일한다. +6. 오류 레코드의 `solar_logs` 보존 여부와 별도 수집 장애 로그 테이블 도입 여부를 검토한다. + +### 6.8 기준선 결론 + +현재 연결 구조인 `외부 사이트 → Tailscale/NAS 프록시 → 크롤러 → Supabase → FastAPI → Nginx → 웹 앱`은 동작 중이다. 즉시 서비스 장애는 발견되지 않았다. + +코드 수정 전에 해결해야 할 운영상 주요 문제는 다음과 같다. + +1. 백필 완료 상태와 실제 데이터 완전성의 불일치 가능성 +2. 운영 중 반복되는 알림 설정 `company_id` 타입 오류 +3. 공개 `/health`의 Nginx 오라우팅 +4. 로컬 소스보다 오래된 운영 웹 번들 +5. 완료된 백필 cron의 불필요한 반복 실행 + +## 7. 변경 기록 + +### 7.1 알림 오탐 선조치 + +작업일: 2026-08-06 + +변경 내용: + +- 크롤러 결과의 `data_valid` 값과 오류 상태를 이용해 수집 오류를 실제 발전소 0kW와 분리했다. +- 수집 오류가 발생하면 진행 중인 0kW 의심 카운트를 초기화하고 알림 판정에서 제외한다. +- NREMS 비분할 발전소의 예외 결과에 `data_valid=False`를 명시했다. +- `alerts_enabled` 조회에서 문자열 크롤러 업체 키를 제거하고 전역 고유키인 `plants.id`만 사용한다. +- 알림 설정 조회가 일시적으로 실패하면 마지막 성공 값을 사용하는 메모리 캐시를 추가했다. +- 테스트를 위해 현재 시각 주입이 가능하도록 `AlertManager` 생성자를 확장했다. + +검증 결과: + +- 알림 상태 전이 단위 테스트 6개 통과 +- 변경 Python 파일 5개 AST 구문 검증 통과 +- 운영 Supabase에서 9개 발전소 모두 `plants.id` 단독 조건으로 `alerts_enabled` 조회 성공 +- 점검 시점의 9개 발전소 알림 설정은 모두 활성 상태 +- 운영 서버의 기존 미커밋 변경과 대조하여 이번 변경이 기존 3회 연속 0kW 로직을 보존함을 확인했다. +- 배포 전 파일은 `/home/ubuntu/solorpower/deploy_backups/20260806_alert_data_valid/`에 백업했다. +- `alert_manager.py`, `main.py`, `crawlers/base.py`, `crawlers/nrems.py`와 알림 테스트를 운영 서버에 배포했다. +- 운영 서버에서 Python 구문 검사와 알림 상태 전이 단위 테스트 6개가 모두 통과했다. +- 로컬과 운영 서버의 배포 대상 5개 파일 SHA-256 해시가 모두 일치했다. +- 2026-08-06 17:10 KST 정기 실행에서 외부 사이트 연결과 Supabase 저장이 정상 완료됐다. 6건을 저장하고 변경 없는 3건은 정상적으로 건너뛰었다. +- 배포 후 실행 구간에서 기존 `company_id` bigint 변환 오류와 텔레그램 알림 발송은 발생하지 않았다. +- 운영 PATCH API로 8호기의 `alerts_enabled`를 잠시 `false`로 변경했을 때 크롤러가 실제로 `false`를 읽는 것을 확인했다. +- 같은 검증 흐름에서 8호기 설정을 즉시 `true`로 복구했고, 크롤러에서 최종 `true` 상태를 재확인했다. +- 운영 알림 설정 확인용 읽기 전용 도구 `tests/check_alert_setting.py`를 추가했다. +- 서비스 재시작과 cron 변경은 필요하지 않아 수행하지 않았다. + +남은 관찰 항목: + +- 배포 직전인 17:00 KST에 NAS 프록시 연결 실패가 재발했고 17:10에는 복구됐다. 다음 실제 수집 오류에서 `수집 실패 데이터는 0kW 판정에서 제외` 로그가 남는지 확인한다. + +### 7.2 역방향 백필 무결성 + +작업일: 2026-08-06 + +변경 내용: + +- 최초 상태 커서를 첫 수집 대상의 다음 날로 저장하여 지정한 시작일을 건너뛰지 않도록 수정했다. +- 일별 원본 조회 계약을 `list=정상 응답`, 빈 `list=정상 응답이지만 해당 날짜 없음`, `None=요청·로그인·파싱 실패`로 구분했다. +- 네트워크·로그인·파싱 실패 시 무발전 일수를 늘리지 않고 커서를 유지하여 다음 실행이 같은 날짜부터 재시도하게 했다. +- Supabase 저장 성공이 확인된 경우에만 SQLite 진행 날짜를 갱신하도록 수정했다. +- 원본에 날짜가 없는 경우는 실제 0kWh와 분리하고 연속 무발전 횟수를 초기화한다. +- 성공적으로 조회된 실제 0kWh만 30일 연속 종료 조건에 포함한다. +- `daily_stats`의 기존 값보다 작거나 같은 백필 값은 upsert하지 않으며, 기존값 조회 실패 시에도 저장하지 않는다. +- 음수 발전량은 잘못된 과거 데이터로 거부한다. +- 일간 저장 후 월 집계 실패가 발생해도 같은 날짜 재시도에서 월간 합계를 다시 계산하도록 했다. +- 운영 원본 값을 DB에 쓰지 않고 확인하는 `tests/check_history_source.py`를 추가했다. + +검증 결과: + +- 알림 회귀 테스트 6개, 백필 상태 전이 7개, DB 저장 보호 4개, 사이트 조회 계약 6개 등 총 23개 테스트가 로컬과 운영 서버에서 모두 통과했다. +- 운영 서버 대상 Python 파일 구문 검사가 통과했다. +- 배포 전 파일은 `/home/ubuntu/solorpower/deploy_backups/20260806_backfill_integrity/`에 백업했다. +- 원본과 운영 DB의 KREMC·CMSolar 최초 날짜가 일치하며 2026-08-05 값도 일치함을 확인했다. +- 운영 dry-run에서 9개 발전소 모두 완료 상태이며 실행 중인 백필이 없음을 확인했다. +- 백필 cron은 변경하지 않았으며 현재 매시간 무작업 실행만 반복한다. + +### 7.3 Excel 업로드 화면과 API 계약 + +작업일: 2026-08-06 + +변경 내용: + +- 상세 화면의 월간 전용 즉시 업로드 구현을 제거하고 기존 `UploadModal`을 연결했다. +- 사용자가 일간 또는 월간 형식을 선택한 후 파일을 업로드하도록 흐름을 하나로 통합했다. +- Web에서는 DocumentPicker의 실제 `File`을, Native에서는 URI 객체를 `FormData`에 추가한다. +- `multipart/form-data`의 boundary를 런타임이 만들도록 수동 `Content-Type` 헤더를 제거했다. +- API에 `.xlsx`·`.xls` 확장자, 빈 파일, 5MB 파일 크기, 5,000행 제한을 추가했다. +- 일간 업로드의 필수 열, 날짜, 빈 발전량, 음수 발전량을 검증한다. +- 월간 업로드의 필수 열, 월 범위, 빈·비숫자·음수 발전량을 검증한다. +- 존재하지 않는 발전소가 내부 오류 500이 아닌 404를 반환하도록 조회 방식을 정리했다. +- pandas의 `fillna(method='ffill')`를 `ffill()`로 변경했다. + +검증 및 배포 결과: + +- Expo Web production export가 성공했다. +- 정상 일간·월간 샘플과 잘못된 확장자, 빈 파일, 필수 열 누락, 잘못된 날짜, 음수, 미등록 발전소, 파일 크기, 행 수를 다루는 API 테스트 10개가 운영과 동일한 Python 환경에서 통과했다. +- API 수정 전 파일과 기존 웹 배포본을 `/home/ubuntu/solorpower/deploy_backups/20260806_upload_contract/`에 백업했다. +- 이전 웹 디렉터리는 `/var/www/html/dist.pre_20260806_upload_contract`에도 보존했다. +- `solar-api` 재시작 후 내부 `/health`가 정상이고 Nginx와 API 서비스가 모두 active 상태임을 확인했다. +- 공개 웹과 `/plants/1`은 HTTP 200, 잘못된 확장자의 공개 업로드 요청은 설명 가능한 HTTP 400을 반환했다. +- 운영 웹 번들은 `AppEntry-80396685aa3d044c4ab36bbf91a02696.js`로 갱신됐고 일간·월간 업로드 경로가 포함됐다. + +### 7.4 KST 시간대 처리 통일 + +작업일: 2026-08-06 + +변경 내용: + +- 크롤러와 API의 독립 배포 구조에 맞춰 각각 공통 KST 시간 유틸리티를 추가했다. +- 서버 OS 시간대에 의존하던 크롤러 시작 로그, 야간 실행 차단, 저장 heartbeat, 알림 허용 시간, 일일 마감, 백필 기본 날짜를 KST 기준으로 통일했다. +- 기존 SQLite에 저장된 naive ISO 시각은 과거 기록 방식과의 호환을 위해 KST로 간주하고, 신규 기록은 `+09:00` 오프셋을 포함한다. +- NREMS 1·2호기 인버터 조회의 오늘 날짜와 월도 KST 기준으로 계산한다. +- 실시간 및 과거 이력 저장의 생성·갱신 시각을 공통 KST 함수로 통일했다. +- API의 오늘 비교 통계, 발전소 일별 통계, 시간별 통계와 크롤러 일일 마감 조회 범위를 KST 하루의 UTC 반개구간으로 변경했다. +- 조회 조건은 `KST 00:00 이상, 다음 날 KST 00:00 미만`으로 적용하여 다음 날 자정 데이터가 이전 날짜에 포함되지 않도록 했다. +- 시간별 통계의 잘못된 날짜 형식에서 발생한 `HTTPException`이 일반 예외 처리에 잡혀 500으로 바뀌지 않도록 유지했다. + +검증 및 배포 결과: + +- UTC 시각 주입 시 KST 04:59에는 크롤링이 차단되고 KST 05:00에는 허용되는 것을 테스트했다. +- KST 자정, 8월 말일, 12월 31일·1월 1일 경계가 UTC 전날 15:00부터 다음 15:00 미만으로 변환되는 것을 테스트했다. +- 크롤러 시간대 및 기존 알림·백필 회귀 테스트 30개가 Linux 컨테이너와 운영 서버에서 통과했다. +- API 시간대 테스트 4개와 Excel 업로드 회귀 테스트 10개 등 운영 서버 API 테스트 14개가 통과했다. +- 운영 배포 전 파일은 `/home/ubuntu/solorpower/deploy_backups/20260806_kst_time/`에 백업했다. +- `solar-api` 재시작 후 서비스와 8000 포트 리슨을 확인했고 내부 `/health`가 HTTP 200을 반환했다. +- 공개 `/health`, 오늘 전체 비교 통계, 2026-08-05 시간별 통계가 모두 HTTP 200을 반환했다. +- API 재시작 직후 약 4초의 기동 구간에는 Nginx가 일시적으로 502를 반환했으며 애플리케이션 시작 완료 후 정상 복구됐다. +- 2026-08-06 18:00 KST 정기 cron 실행에서 9개 발전소 연결이 모두 성공했고, 값이 변경된 8개 로그를 저장했으며 변경 없는 현대 8호기는 정상적으로 건너뛰었다. +- 같은 정기 실행에서 시작 시각과 마감 스킵 판단이 모두 KST 18:00으로 기록됐고 수집 오류나 텔레그램 알림은 발생하지 않았다. + +### 7.5 API 정확성과 응답 규약 + +작업일: 2026-08-07 + +변경 내용: + +- 상세 API의 `plant_id`를 정수형에서 실제 DB 키와 같은 문자열형으로 수정하여 `nrems-03` 같은 요청이 422가 되던 문제를 해결했다. +- 상세 발전소 조회에서 `.single()`이 미등록 데이터를 내부 오류로 바꾸지 않도록 `.limit(1)` 조회와 명시적 404 처리를 적용했다. +- 비교·발전소·시간별 통계에 응답 모델을 추가하고 발전소 상세, 알림 변경, health, Excel 업로드 응답도 Pydantic 모델로 OpenAPI에 명시했다. +- 비교 통계와 시간별 통계 날짜는 `YYYY-MM-DD`만 허용하고 잘못된 형식이나 존재하지 않는 날짜를 400으로 반환한다. +- 통계의 연도는 2000~2100, 월은 1~12로 제한하여 범위를 벗어나면 FastAPI 검증 응답 422를 반환한다. +- 미등록 발전소의 일별·시간별 통계가 0으로 채운 성공 응답을 반환하지 않고 404를 반환하도록 수정했다. +- `HTTPException`을 일반 예외 처리보다 먼저 다시 전달하여 의도한 4xx 상태가 500으로 바뀌지 않게 했다. +- 선택한 연도의 통계 조회 종료 월이 현재 연도로 고정되던 문제를 수정하고 윤년의 연간 발전시간 계산에 366일을 적용했다. +- 발전소 목록의 최신 로그 조회를 발전소 1회와 로그 9회의 N+1 요청에서 관계 중첩 단일 요청으로 변경했다. +- 동기 Supabase 호출을 사용하는 조회 API는 FastAPI 동기 핸들러로 전환하여 threadpool에서 실행되게 했다. +- 비동기 파일 업로드에서는 Excel 파싱과 Supabase 조회·저장을 `run_in_threadpool`로 분리하여 이벤트 루프 차단을 줄였다. +- `/health`가 환경변수 존재 여부가 아니라 실제 `plants` 읽기 요청으로 Supabase 연결을 확인하며 실패 시 503을 반환하도록 변경했다. +- Nginx에 `/health`와 `/redoc`을 포함한 명시적 API 프록시 규칙을 추가하고 재현 가능한 설정을 `deploy/nginx/solorpower.conf`에 보존했다. + +검증 및 배포 결과: + +- 정상 업로드, 시간대 경계, 문자열 ID, 빈 목록, 날짜·범위 오류, 미등록 발전소, DB 조회 실패, DB health 실패, OpenAPI 모델과 단일 관계 조회를 다루는 API 테스트 31개가 스테이징과 운영 서버에서 모두 통과했다. +- 운영 Supabase 읽기 전용 스테이징 점검에서 health, 발전소 목록·상세, 비교 통계와 시간별 통계가 모두 JSON HTTP 200을 반환했다. +- 공개 발전소 목록 응답 시간은 변경 전 약 2.46초에서 배포 후 약 0.23초로 확인됐다. +- 운영 API 배포 전 파일은 `/home/ubuntu/solorpower/deploy_backups/20260807_api_contract/`에 백업했다. +- 기존 Nginx 설정은 `/etc/nginx/sites-available/solorpower.pre_20260807_api_contract`에 백업했다. +- Nginx 설정 검사가 성공한 경우에만 reload했으며 `solar-api`와 Nginx 모두 active 상태를 유지했다. +- 공개 `/health`는 `application/json`과 `supabase_connected=true`를 반환하고, 문자열 발전소 상세와 OpenAPI는 200, 잘못된 날짜는 400, 잘못된 월은 422를 반환했다. + +### 7.6 통계 생성 경로 일관성 + +작업일: 2026-08-07 + +변경 전 확인: + +- 운영 Supabase에는 최초 원격 스키마 마이그레이션 1건만 적용되어 있었다. +- `daily_stats`에는 값 변경 시각과 입력 출처 컬럼이 없었고, `monthly_stats`에는 사용되지 않는 `currnet_last_date text` 오타 컬럼이 존재했다. 운영 937개 월간 행에서 이 오타 컬럼의 실제 값은 모두 NULL이었다. +- 실시간 저장만 기존 최댓값을 보호했고 조회 실패 시 빈 기존값으로 간주하여 보호가 우회될 수 있었다. +- 과거 백필과 일일 마감은 각각 별도 일간 upsert를 사용했고, 백필과 월말 마감이 별도로 월간 합계를 갱신했다. 일간 Excel 업로드는 월간 합계를 갱신하지 않았다. +- 변경 전 2026-01~2026-08 일·월 통계를 읽기 전용으로 대조한 결과 1~7월 63쌍의 합계 차이는 0건이었으나 8월 9개 발전소의 월간 행이 모두 누락되어 있었다. + +저장 정책: + +- `realtime`, `daily_summary`, `history`는 자동 경로이며 같은 날짜의 기존 발전량보다 큰 값만 반영한다. +- `excel_daily`는 사용자의 명시적 보정 경로이므로 발전량의 상향·하향 수정을 모두 허용한다. +- 자동 경로가 더 작은 값을 제출하더라도 기존 총발전량과 출처를 유지한다. 최고 출력은 경로와 관계없이 기존값과 신규값 중 큰 값을 유지한다. +- 발전시간은 최종 선택된 총발전량을 `plants.capacity`로 나눠 DB에서 계산한다. `created_at`은 최초 생성 시각을 유지하고, 총발전량 또는 최고 출력의 실제 변경 시에만 `updated_at`을 갱신한다. +- 일일 마감의 원본 사이트 값은 로그 기반 당일 최댓값보다 큰 경우에만 상향 보정한다. 원본 0 또는 더 작은 값은 로그 집계를 낮추지 않는다. +- `derived_daily` 월간 값은 일간 변경 트리거가 합계와 마지막 포함일을 갱신한다. `excel_monthly`, `history_monthly`는 권위 있는 월간 입력으로 보호한다. +- 마이그레이션 당시의 기존 월간 값은 `legacy`로 표시하고 총액을 일괄 재계산하지 않았다. 현재 KST 월만 생성·갱신하여 과거 수동 입력 손상 가능성을 차단했다. + +코드 및 스키마 변경: + +- `crawler/database.py`와 `api_server/app/core/stats_storage.py`에 공통 RPC 호출 래퍼를 추가했다. +- `crawler/database.py`의 실시간·백필 일간 저장과 `crawler/daily_summary.py`의 마감 저장을 공통 DB 함수로 전환했다. +- 백필과 월말 마감에 중복되어 있던 애플리케이션 월간 재집계 코드를 제거했다. +- 두 일간 Excel API는 `excel_daily`, 월간 Excel API는 `excel_monthly` 출처를 기록한다. 월간 업로드는 해당 월의 말일을 `last_date`로 저장한다. +- `supabase/migrations/20260807000001_stats_write_consistency.sql`에 컬럼·제약조건, `upsert_daily_stats`, `refresh_monthly_stat`과 `daily_stats_sync_monthly` 트리거를 추가했다. +- `currnet_last_date`는 `last_date date`로 이름과 자료형을 바로잡았다. +- `crawler/tests/check_stats_consistency.py`를 추가하여 PostgREST 1,000행 제한을 페이지 처리하면서 일·월 합계, 누락과 스키마 버전을 읽기 전용으로 점검할 수 있게 했다. + +검증 및 배포 결과: + +- 격리된 Supabase PostgreSQL 15 컨테이너에서 실제 마이그레이션을 적용하고 자동 하향 방지, 자동 상향, Excel 하향 정정, 최고 출력 보호, 발전시간 재계산, 월간 트리거와 권위 월간 보호를 SQL로 검증했다. +- 크롤러 전체 회귀 테스트 31개가 Linux 컨테이너에서 통과했다. Windows 직접 실행에서 보인 15개 오류는 SQLite 연결이 열린 상태의 임시 파일 정리 잠금 문제였으며 Linux 실행에서는 모두 통과했다. +- API 전체 회귀 테스트 31개가 운영과 동일한 서버 Python 환경의 스테이징 및 배포본에서 모두 통과했다. +- Supabase 마이그레이션 적용 후 2026-01~2026-08 통계는 `daily_rows=1970`, `monthly_rows=72`, `mismatches=0`, `missing_monthly=0`으로 확인됐다. +- 운영 배포 전 API·크롤러 파일은 `/home/ubuntu/solorpower/deploy_backups/20260807_stats_consistency/`에 백업했다. +- `solar-api` 재시작 후 active 상태이며 공개 `/health`는 실제 Supabase 연결을 포함한 JSON 200을 반환한다. 공개 `/plants/stats/comparison`의 2026년 8월 월간 및 2026-08-05 일간 요청도 JSON 200을 반환한다. +- 2026-08-07 10:50 KST 실제 cron이 새 저장 함수를 사용하여 9개 발전소의 `solar_logs`와 `daily_stats`를 모두 저장했고 수집·통계 오류 없이 종료했다. + +### 7.7 테스트와 재현 가능한 개발 환경 + +작업일: 2026-08-07 + +의존성과 실행 기준: + +- crawler의 직접 의존성과 Python 3.10 호환 NumPy 제약을 `crawler/requirements.in`에 기록하고, 해석된 전이 의존성 56개 전체를 `crawler/requirements.txt`에 고정했다. +- API의 직접 의존성 기준을 `api_server/requirements.in`에 분리하고 기존 전체 고정 목록인 `requirements.txt`를 배포·CI 기준으로 유지했다. +- Linux 전용 `uvloop`에 `sys_platform != "win32"` 조건을 추가하여 같은 고정 목록이 Windows 개발 환경에서도 설치되도록 수정했다. +- 앱의 `package-lock.json`을 Git 관리 대상으로 전환하고 중국 npm mirror 주소와 버전이 비어 있던 Supabase CLI 선택 패키지 항목을 제거한 공식 npm registry 기반 lockfile로 재생성했다. +- 깨끗한 설치에서 드러난 누락 직접 의존성 `expo-asset ~11.0.5`를 추가했다. +- `npm run build:web`과 CI용 `npm run test:ci` 명령을 추가했다. + +테스트 보강: + +- NREMS, KREMC, 현대, Sun-WMS, CMSolar의 성공 응답을 개인정보 없는 최소 JSON/HTML fixture로 저장했다. +- 외부 네트워크와 실제 계정 없이 로그인·월간 요청·날짜와 발전량 파싱을 검증하는 성공 경로 테스트 5개를 추가했다. +- Windows 임시 SQLite 파일 잠금의 원인이었던 연결 미종료를 `contextlib.closing`으로 수정했다. 운영 코드의 알림 상태, scheduler, 백필 DB와 관련 테스트 헬퍼가 연결을 명시적으로 닫는다. + +CI 및 문서: + +- `.github/workflows/ci.yml`에 crawler Python 3.10/3.11, API Python 3.11, PostgreSQL 15 migration 계약, Node 20 Expo 웹 build의 4개 독립 job을 추가했다. +- CI는 테스트용 더미 Supabase 설정만 사용하며 실제 외부 사이트, 운영 DB, Telegram 비밀값을 요구하지 않는다. +- 설치와 실행 명령, 지원 버전, 테스트 범위를 `docs/development_and_testing.md`에 정리했다. +- 워크플로는 현재 미커밋 작업 트리에 있으므로 Git 커밋·push 후 최초로 자동 실행된다. + +검증 및 배포 결과: + +- 새 Python 3.11 가상환경에 crawler와 API 고정 의존성을 함께 설치했다. Windows에서 Linux 전용 `uvloop`가 정상 제외됐고 `pip install`이 성공했다. +- 같은 새 환경에서 crawler 36개와 API 31개 테스트가 모두 통과했다. +- crawler 36개 테스트는 기존 Linux 컨테이너와 운영 서버 `/usr/bin/python3`에서도 모두 통과했다. +- 공식 registry lockfile만 있는 깨끗한 npm 환경에서 931개 패키지를 `npm ci`로 설치하고 Expo 웹 export를 완료했다. +- 격리된 PostgreSQL 15 컨테이너에서 `stats_write_consistency_test.sql`을 다시 실행하여 migration 계약이 통과했다. 테스트용 컨테이너는 종료 후 자동 제거했다. +- `npm ci` 감사 결과는 17건(중간 10, 높음 6, 치명적 1)을 보고했다. 대부분 현재 Expo 52 의존성 트리의 전이 패키지이며 호환성 검토가 필요한 버전 업그레이드는 8단계 이후 별도 작업으로 남긴다. +- 운영 반영 전 SQLite 관련 파일 3개는 `/home/ubuntu/solorpower/deploy_backups/20260807_test_environment/`에 백업했다. +- 2026-08-07 12:00 KST 실제 cron에서 3호기는 직전 값과 동일하여 정상적으로 저장을 건너뛰었고 나머지 8개 발전소는 저장에 성공했다. SQLite 잠금, 수집 실패, 통계 저장 실패와 Telegram 오탐은 발생하지 않았다. diff --git a/docs/system_infrastructure_guide.md b/docs/system_infrastructure_guide.md index 1078371..66e7638 100644 --- a/docs/system_infrastructure_guide.md +++ b/docs/system_infrastructure_guide.md @@ -27,17 +27,21 @@ ssh -i "C:\Users\haneu\.ssh\holdem_server.key" ubuntu@100.116.0.32 서버 내 주요 서비스 설치 경로입니다. -* **`~/solorpower_crawler/`**: 실시간 데이터 수집기(Crawler)가 실행되는 메인 경로 - * `main.py`: 10분마다 실행되는 수집 엔트리 - * `crawler.log`: 크롤링 실행 로그 (표준 출력/에러 통합) - * `database.py`: Supabase 저장 로직 (최근 0kW 보호 패치 적용됨) -* **`~/solorpower_server/`**: FastAPI 기반의 백엔드 API 서버 경로 -* **`~/plant_sync/`**: 발전소 정보 동기화 및 기타 유틸리티 +* **`~/solorpower/`**: 모노레포 통합 경로 + * `~/solorpower/crawler/`: 실시간 데이터 수집기(Crawler) 및 역추적 백필 프로그램 경로 + * `main.py`: 10분마다 실행되는 실시간 수집 엔트리 + * `backward_backfill.py`: 과거 백필 스케줄러 + * `crawler_manager.db`: SQLite 백필 및 스케줄 상태 DB + * `crawler.log`: 실시간 크롤링 실행 로그 + * `backfill_cron.log`: 백필 실행 로그 + * `~/solorpower/api_server/`: FastAPI 기반의 백엔드 API 서버 경로 ### ⏰ 자동화 스케줄 (Crontab) 서버에서 `crontab -l` 명령어로 확인된 자동화 설정입니다. -* `*/10 * * * *`: 10분마다 데이터 수집 실행 (`main.py`) -* `10 0 * * *`: 매일 0시 10분에 일일 통계 요약 실행 (`daily_summary.py`) +* `*/10 * * * *`: 10분마다 실시간 데이터 수집 실행 (`main.py`) +* `10 0 * * *`: 매일 0시 10분에 일일 통계 요약 및 하이브리드 보정 실행 (`daily_summary.py`) +* `30 * * * *`: 매시간 30분마다 과거 5일치 백필 실행 (`backward_backfill.py --days 5 --delay 2.0`) + --- @@ -72,4 +76,4 @@ ssh -i "C:\Users\haneu\.ssh\holdem_server.key" ubuntu@100.116.0.32 4. **DB 확인:** Supabase `solar_logs` 테이블에서 해당 시간대 `status` 확인 --- -*마지막 업데이트: 2026-05-14 (0kW 오보 보호 패치 및 인프라 정리)* +*마지막 업데이트: 2026-06-18 (백필 크론 확인, 인프라 및 DB 경로 동기화)* diff --git a/supabase/migrations/20260807000001_stats_write_consistency.sql b/supabase/migrations/20260807000001_stats_write_consistency.sql new file mode 100644 index 0000000..3ef0135 --- /dev/null +++ b/supabase/migrations/20260807000001_stats_write_consistency.sql @@ -0,0 +1,303 @@ +-- Centralize daily-stat write policy and keep monthly aggregates in sync. + +ALTER TABLE public.daily_stats + ADD COLUMN IF NOT EXISTS updated_at timestamp with time zone NOT NULL DEFAULT now(), + ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'legacy'; + +UPDATE public.daily_stats +SET updated_at = COALESCE(created_at, now()) +WHERE updated_at IS NULL; + +ALTER TABLE public.daily_stats + DROP CONSTRAINT IF EXISTS daily_stats_source_check; + +ALTER TABLE public.daily_stats + ADD CONSTRAINT daily_stats_source_check CHECK ( + source IN ('legacy', 'realtime', 'daily_summary', 'history', 'excel_daily') + ); + +ALTER TABLE public.monthly_stats + RENAME COLUMN currnet_last_date TO last_date; + +ALTER TABLE public.monthly_stats + ALTER COLUMN last_date TYPE date + USING CASE + WHEN last_date IS NULL OR btrim(last_date) = '' THEN NULL + WHEN last_date ~ '^\d{4}-\d{2}-\d{2}$' THEN last_date::date + ELSE NULL + END; + +ALTER TABLE public.monthly_stats + ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'legacy'; + +ALTER TABLE public.monthly_stats + DROP CONSTRAINT IF EXISTS monthly_stats_source_check; + +ALTER TABLE public.monthly_stats + ADD CONSTRAINT monthly_stats_source_check CHECK ( + source IN ('legacy', 'derived_daily', 'excel_monthly', 'history_monthly') + ); + +COMMENT ON COLUMN public.daily_stats.updated_at IS '통계 값이 실제로 변경된 시각'; +COMMENT ON COLUMN public.daily_stats.source IS '현재 일 발전량 값을 결정한 저장 경로'; +COMMENT ON COLUMN public.monthly_stats.last_date IS '월간 합계에 포함된 마지막 일자'; +COMMENT ON COLUMN public.monthly_stats.source IS '월간 값의 생성 경로'; + +CREATE OR REPLACE FUNCTION public.refresh_monthly_stat( + p_plant_id text, + p_month text +) +RETURNS void +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +DECLARE + v_month_start date; + v_next_month date; + v_total double precision; + v_last_date date; + v_count integer; +BEGIN + IF p_month !~ '^\d{4}-(0[1-9]|1[0-2])$' THEN + RAISE EXCEPTION 'invalid month: %', p_month; + END IF; + + v_month_start := (p_month || '-01')::date; + v_next_month := (v_month_start + interval '1 month')::date; + + SELECT + COALESCE(sum(ds.total_generation), 0)::double precision, + max(ds.date), + count(*)::integer + INTO v_total, v_last_date, v_count + FROM public.daily_stats AS ds + WHERE ds.plant_id = p_plant_id + AND ds.date >= v_month_start + AND ds.date < v_next_month; + + IF v_count = 0 THEN + DELETE FROM public.monthly_stats AS ms + WHERE ms.plant_id = p_plant_id + AND ms.month = p_month + AND ms.source NOT IN ('excel_monthly', 'history_monthly'); + RETURN; + END IF; + + INSERT INTO public.monthly_stats AS ms ( + plant_id, + month, + total_generation, + last_date, + updated_at, + source + ) VALUES ( + p_plant_id, + p_month, + round(v_total::numeric, 2)::double precision, + v_last_date, + now(), + 'derived_daily' + ) + ON CONFLICT (plant_id, month) DO UPDATE + SET total_generation = EXCLUDED.total_generation, + last_date = EXCLUDED.last_date, + updated_at = CASE + WHEN ms.total_generation IS DISTINCT FROM EXCLUDED.total_generation + OR ms.last_date IS DISTINCT FROM EXCLUDED.last_date + OR ms.source IS DISTINCT FROM EXCLUDED.source + THEN now() + ELSE ms.updated_at + END, + source = EXCLUDED.source + WHERE ms.source NOT IN ('excel_monthly', 'history_monthly'); +END; +$$; + +CREATE OR REPLACE FUNCTION public.sync_monthly_stat_from_daily() +RETURNS trigger +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +BEGIN + IF TG_OP = 'DELETE' THEN + PERFORM public.refresh_monthly_stat(OLD.plant_id, to_char(OLD.date, 'YYYY-MM')); + RETURN OLD; + END IF; + + IF TG_OP = 'UPDATE' + AND OLD.plant_id IS NOT DISTINCT FROM NEW.plant_id + AND OLD.date IS NOT DISTINCT FROM NEW.date + AND OLD.total_generation IS NOT DISTINCT FROM NEW.total_generation THEN + RETURN NEW; + END IF; + + IF TG_OP = 'UPDATE' + AND (OLD.plant_id IS DISTINCT FROM NEW.plant_id OR OLD.date IS DISTINCT FROM NEW.date) THEN + PERFORM public.refresh_monthly_stat(OLD.plant_id, to_char(OLD.date, 'YYYY-MM')); + END IF; + + PERFORM public.refresh_monthly_stat(NEW.plant_id, to_char(NEW.date, 'YYYY-MM')); + RETURN NEW; +END; +$$; + +DROP TRIGGER IF EXISTS daily_stats_sync_monthly ON public.daily_stats; + +CREATE TRIGGER daily_stats_sync_monthly +AFTER INSERT OR UPDATE OR DELETE ON public.daily_stats +FOR EACH ROW EXECUTE FUNCTION public.sync_monthly_stat_from_daily(); + +CREATE OR REPLACE FUNCTION public.upsert_daily_stats( + p_records jsonb, + p_source text, + p_allow_decrease boolean DEFAULT false +) +RETURNS SETOF public.daily_stats +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = '' +AS $$ +DECLARE + v_record jsonb; + v_plant_id text; + v_date date; + v_total double precision; + v_peak double precision; + v_capacity double precision; + v_result public.daily_stats%ROWTYPE; +BEGIN + IF jsonb_typeof(p_records) <> 'array' THEN + RAISE EXCEPTION 'p_records must be a JSON array'; + END IF; + + IF p_source NOT IN ('realtime', 'daily_summary', 'history', 'excel_daily') THEN + RAISE EXCEPTION 'invalid daily stats source: %', p_source; + END IF; + + FOR v_record IN SELECT value FROM jsonb_array_elements(p_records) + LOOP + v_plant_id := NULLIF(btrim(v_record ->> 'plant_id'), ''); + v_date := (v_record ->> 'date')::date; + v_total := (v_record ->> 'total_generation')::double precision; + v_peak := COALESCE((v_record ->> 'peak_kw')::double precision, 0); + + IF v_plant_id IS NULL THEN + RAISE EXCEPTION 'plant_id is required'; + END IF; + IF v_total < 0 OR v_peak < 0 THEN + RAISE EXCEPTION 'generation values must be non-negative: % %', v_plant_id, v_date; + END IF; + + SELECT COALESCE(p.capacity, 0) + INTO v_capacity + FROM public.plants AS p + WHERE p.id = v_plant_id; + + IF NOT FOUND THEN + RAISE EXCEPTION 'unknown plant_id: %', v_plant_id; + END IF; + + INSERT INTO public.daily_stats AS ds ( + plant_id, + date, + total_generation, + peak_kw, + generation_hours, + created_at, + updated_at, + source + ) VALUES ( + v_plant_id, + v_date, + round(v_total::numeric, 2)::double precision, + round(v_peak::numeric, 2)::double precision, + CASE + WHEN v_capacity > 0 + THEN round((v_total / v_capacity)::numeric, 2)::double precision + ELSE 0 + END, + now(), + now(), + p_source + ) + ON CONFLICT (plant_id, date) DO UPDATE + SET total_generation = CASE + WHEN p_allow_decrease OR EXCLUDED.total_generation > ds.total_generation + THEN EXCLUDED.total_generation + ELSE ds.total_generation + END, + peak_kw = GREATEST(COALESCE(ds.peak_kw, 0), EXCLUDED.peak_kw), + generation_hours = CASE + WHEN v_capacity > 0 THEN round(( + (CASE + WHEN p_allow_decrease OR EXCLUDED.total_generation > ds.total_generation + THEN EXCLUDED.total_generation + ELSE ds.total_generation + END) / v_capacity + )::numeric, 2)::double precision + ELSE 0 + END, + updated_at = CASE + WHEN ds.total_generation IS DISTINCT FROM ( + CASE + WHEN p_allow_decrease OR EXCLUDED.total_generation > ds.total_generation + THEN EXCLUDED.total_generation + ELSE ds.total_generation + END + ) OR ds.peak_kw IS DISTINCT FROM GREATEST(COALESCE(ds.peak_kw, 0), EXCLUDED.peak_kw) + THEN now() + ELSE ds.updated_at + END, + source = CASE + WHEN ds.total_generation IS DISTINCT FROM EXCLUDED.total_generation + AND (p_allow_decrease OR EXCLUDED.total_generation > ds.total_generation) + THEN p_source + ELSE ds.source + END + RETURNING ds.* INTO v_result; + + RETURN NEXT v_result; + END LOOP; + + RETURN; +END; +$$; + +REVOKE ALL ON FUNCTION public.upsert_daily_stats(jsonb, text, boolean) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION public.upsert_daily_stats(jsonb, text, boolean) + TO anon, authenticated, service_role; + +REVOKE ALL ON FUNCTION public.refresh_monthly_stat(text, text) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION public.refresh_monthly_stat(text, text) + TO authenticated, service_role; + +-- Preserve legacy monthly totals, but populate their last included daily date. +UPDATE public.monthly_stats AS ms +SET last_date = daily.last_date +FROM ( + SELECT plant_id, to_char(date, 'YYYY-MM') AS month, max(date) AS last_date + FROM public.daily_stats + GROUP BY plant_id, to_char(date, 'YYYY-MM') +) AS daily +WHERE ms.plant_id = daily.plant_id + AND ms.month = daily.month + AND ms.last_date IS DISTINCT FROM daily.last_date; + +-- Create or refresh only the current KST month. Historical totals remain untouched. +DO $$ +DECLARE + v_pair record; + v_current_month text := to_char(timezone('Asia/Seoul', now()), 'YYYY-MM'); +BEGIN + FOR v_pair IN + SELECT DISTINCT plant_id + FROM public.daily_stats + WHERE date >= (v_current_month || '-01')::date + AND date < ((v_current_month || '-01')::date + interval '1 month')::date + LOOP + PERFORM public.refresh_monthly_stat(v_pair.plant_id, v_current_month); + END LOOP; +END; +$$; diff --git a/supabase/tests/stats_write_consistency_test.sql b/supabase/tests/stats_write_consistency_test.sql new file mode 100644 index 0000000..402818c --- /dev/null +++ b/supabase/tests/stats_write_consistency_test.sql @@ -0,0 +1,153 @@ +\set ON_ERROR_STOP on + +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN + CREATE ROLE anon NOLOGIN; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN + CREATE ROLE authenticated NOLOGIN; + END IF; + IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN + CREATE ROLE service_role NOLOGIN; + END IF; +END; +$$; + +CREATE TABLE public.plants ( + id text PRIMARY KEY, + capacity double precision +); + +CREATE TABLE public.daily_stats ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + plant_id text NOT NULL REFERENCES public.plants(id), + date date NOT NULL, + total_generation double precision DEFAULT 0, + peak_kw double precision DEFAULT 0, + generation_hours double precision DEFAULT 0, + created_at timestamp with time zone DEFAULT now(), + UNIQUE (plant_id, date) +); + +CREATE TABLE public.monthly_stats ( + id bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + plant_id text NOT NULL REFERENCES public.plants(id), + month text NOT NULL, + total_generation double precision DEFAULT 0, + currnet_last_date text, + updated_at timestamp with time zone DEFAULT now() NOT NULL, + UNIQUE (plant_id, month) +); + +INSERT INTO public.plants (id, capacity) VALUES ('plant-a', 100); +INSERT INTO public.monthly_stats (plant_id, month, total_generation) +VALUES ('plant-a', '2026-05', 500); + +\ir ../migrations/20260807000001_stats_write_consistency.sql + +SELECT public.upsert_daily_stats( + '[{"plant_id":"plant-a","date":"2026-06-01","total_generation":100,"peak_kw":20}]'::jsonb, + 'realtime', + false +); + +SELECT public.upsert_daily_stats( + '[{"plant_id":"plant-a","date":"2026-06-01","total_generation":80,"peak_kw":15}]'::jsonb, + 'history', + false +); + +DO $$ +DECLARE + v_daily public.daily_stats%ROWTYPE; + v_monthly public.monthly_stats%ROWTYPE; +BEGIN + SELECT * INTO v_daily FROM public.daily_stats + WHERE plant_id = 'plant-a' AND date = '2026-06-01'; + IF v_daily.total_generation <> 100 + OR v_daily.peak_kw <> 20 + OR v_daily.generation_hours <> 1 + OR v_daily.source <> 'realtime' THEN + RAISE EXCEPTION 'automated decrease protection failed: %', row_to_json(v_daily); + END IF; + + SELECT * INTO v_monthly FROM public.monthly_stats + WHERE plant_id = 'plant-a' AND month = '2026-06'; + IF v_monthly.total_generation <> 100 + OR v_monthly.last_date <> '2026-06-01'::date + OR v_monthly.source <> 'derived_daily' THEN + RAISE EXCEPTION 'monthly trigger failed: %', row_to_json(v_monthly); + END IF; +END; +$$; + +SELECT public.upsert_daily_stats( + '[{"plant_id":"plant-a","date":"2026-06-01","total_generation":120,"peak_kw":25}]'::jsonb, + 'history', + false +); + +SELECT public.upsert_daily_stats( + '[{"plant_id":"plant-a","date":"2026-06-01","total_generation":90,"peak_kw":0}]'::jsonb, + 'excel_daily', + true +); + +DO $$ +DECLARE + v_daily public.daily_stats%ROWTYPE; + v_monthly public.monthly_stats%ROWTYPE; +BEGIN + SELECT * INTO v_daily FROM public.daily_stats + WHERE plant_id = 'plant-a' AND date = '2026-06-01'; + IF v_daily.total_generation <> 90 + OR v_daily.peak_kw <> 25 + OR v_daily.generation_hours <> 0.9 + OR v_daily.source <> 'excel_daily' THEN + RAISE EXCEPTION 'manual correction policy failed: %', row_to_json(v_daily); + END IF; + + SELECT * INTO v_monthly FROM public.monthly_stats + WHERE plant_id = 'plant-a' AND month = '2026-06'; + IF v_monthly.total_generation <> 90 THEN + RAISE EXCEPTION 'monthly correction sync failed: %', row_to_json(v_monthly); + END IF; +END; +$$; + +INSERT INTO public.monthly_stats ( + plant_id, month, total_generation, last_date, source +) VALUES ( + 'plant-a', '2026-07', 777, '2026-07-31', 'excel_monthly' +); + +SELECT public.upsert_daily_stats( + '[{"plant_id":"plant-a","date":"2026-07-01","total_generation":10,"peak_kw":1}]'::jsonb, + 'history', + false +); + +DO $$ +DECLARE + v_total double precision; + v_source text; + v_legacy_total double precision; +BEGIN + SELECT total_generation, source INTO v_total, v_source + FROM public.monthly_stats + WHERE plant_id = 'plant-a' AND month = '2026-07'; + IF v_total <> 777 OR v_source <> 'excel_monthly' THEN + RAISE EXCEPTION 'authoritative monthly value was overwritten'; + END IF; + + SELECT total_generation INTO v_legacy_total + FROM public.monthly_stats + WHERE plant_id = 'plant-a' AND month = '2026-05'; + IF v_legacy_total <> 500 THEN + RAISE EXCEPTION 'legacy monthly value was changed during migration'; + END IF; +END; +$$; + +SELECT 'stats_write_consistency_test passed' AS result;