From 977783c1265be9e736234212ea4b72e3a1db5adc Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 22 Sep 2026 20:15:00 +0800 Subject: [PATCH] =?UTF-8?q?build:=20=E6=89=93=E9=80=9A=20FastAPI=20?= =?UTF-8?q?=E4=B8=8E=20Vue=20=E6=9C=80=E5=B0=8F=E5=B7=A5=E7=A8=8B=E3=80=81?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E4=B8=8E=E6=95=B0=E6=8D=AE=E5=BA=93=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端入口、配置、Redis 与 Alembic 迁移就位;前端 Vite 工程可安装可构建。 --- backend/.coveragerc | 20 + backend/.env.example | 170 + backend/.gitignore | 10 + backend/__init__.py | 16 + backend/alembic.ini | 117 + backend/alembic/README | 1 + backend/alembic/env.py | 74 + backend/alembic/script.py.mako | 26 + ...4e15242_add_priority_to_knowledge_queue.py | 46 + ...ee_merge_guardrails_and_chat_initiation.py | 42 + ...f5fa1db0cb_add_teamid_to_agent_sessions.py | 86 + ...9e_removed_message_node_specific_fields.py | 48 + .../1493b8279a2f_added_shopify_models.py | 63 + .../186c65ae9406_added_rating_table.py | 72 + .../2037204bbebf_add_guardrails_node_type.py | 59 + .../2381319791e8_merge_multiple_heads.py | 42 + ...fd826882d08_add_workflow_history_column.py | 44 + .../versions/30caca239191_add_jira_config.py | 64 + .../520df92643e3_added_replypier_ai_model.py | 46 + ...a829bb8_make_user_org_and_role_nullable.py | 58 + .../537f2b7d4a7c_added_end_chat_support.py | 67 + ...6cd9e4_role_remove_name_alone_as_unique.py | 48 + .../621095beb234_add_mcp_tools_tables.py | 84 + ...widget_position_to_agent_customizations.py | 70 + ...b4fc00d8_added_wlecome_header_and_title.py | 51 + ...add_oauth_state_fields_to_shopify_shops.py | 46 + .../7fd0447b3d45_workflow_tables_added.py | 131 + .../versions/85364d188f15_merge_heads.py | 42 + ...8aa4_change_datetime_to_timestamp_with_.py | 64 + ...b_make_knowledge_queue_user_id_nullable.py | 48 + ...726029_add_extra_data_to_agent_sessions.py | 87 + ...3b2_add_agent_shopify_config_with_shop_.py | 61 + ...ew_node_type_user_input.py 14-13-41-620.py | 60 + ...b2c3d4e5f6_add_chat_initiation_messages.py | 47 + ...b0_merge_ai_ticketing_agent_queue_heads.py | 26 + ...a8_merge_crm_and_chat_session_idx_heads.py | 42 + ...abc123456789_add_file_attachments_table.py | 63 + .../add_agent_ai_replies_toggle_001.py | 45 + .../add_agent_guardrail_prompt_001.py | 51 + .../add_agent_ticketing_toggle_001.py | 45 + .../versions/add_ai_disclaimer_toggle_001.py | 46 + .../versions/add_allow_attachments_001.py | 44 + .../versions/add_allow_new_chat_001.py | 57 + .../add_allowed_attachment_types_001.py | 45 + .../versions/add_channel_tables_001.py | 126 + .../versions/add_chat_session_idx_001.py | 50 + .../versions/add_crm_customer_syncs_001.py | 61 + .../add_crm_tables_001_add_crm_tables.py | 97 + ...d_customer_phone_001_add_customer_phone.py | 56 + .../versions/add_db_connector_row_scope.py | 40 + .../alembic/versions/add_faq_seo_fields.py | 27 + .../versions/add_guardrails_layer_001.py | 77 + ...chat_widget_001_add_chat_widget_enabled.py | 33 + .../add_hc_cta_enabled_001_add_cta_enabled.py | 30 + ...dd_hc_faq_feedback_001_add_faq_feedback.py | 42 + .../add_hc_faq_slug_001_add_faq_slug.py | 61 + ...hc_faq_url_path_001_add_preserved_paths.py | 51 + .../alembic/versions/add_hc_favicon_url.py | 24 + .../add_hc_job_meter_001_add_job_metering.py | 48 + .../add_invest_glassbox_001_add_glassbox.py | 124 + .../versions/add_investigation_metered.py | 41 + .../add_investigation_runs_001_add_runs.py | 93 + .../versions/add_mcp_usage_guidance_001.py | 61 + ...if_prefs_001_user_notification_settings.py | 57 + .../add_openai_compat_ai_model_001.py | 40 + .../versions/add_require_token_auth_001.py | 44 + .../versions/add_run_connector_status_001.py | 44 + .../versions/add_sentiment_analysis_001.py | 67 + .../versions/add_slack_integration_001.py | 165 + backend/alembic/versions/add_ticket_csat.py | 43 + ...ticketing_core_001_add_native_ticketing.py | 229 + .../versions/add_tkt_db_ssh_001_add_ssh.py | 54 + .../versions/add_tkt_phase4_001_add_phase4.py | 165 + .../alembic/versions/add_widget_apps_001.py | 67 + .../versions/agent_quick_replies_001.py | 30 + backend/alembic/versions/auth_sessions_001.py | 39 + .../b2c3d4e5f6a7_add_customer_meta_data.py | 50 + .../versions/b365b8447598_initial_version.py | 310 + ..._add_agent_queue_and_people_permissions.py | 151 + ...aa7a5d6109_removed_node_specific_fields.py | 72 + ..._premium_chat_styles_and_show_citations.py | 71 + .../versions/c3d4e5f6a7b8_add_lead_capture.py | 123 + .../versions/chat_channel_status_001.py | 64 + ...crm_sync_varchar_001_crm_target_varchar.py | 62 + ...f4a5b6c7_add_contact_collection_columns.py | 67 + .../d4e5f6a7b8c9_add_customer_merge.py | 37 + .../d9a4b9902f80_add_attempts_to_otp_table.py | 64 + ...dd_progress_tracking_to_knowledge_queue.py | 52 + .../dd0e5ceefe4b_added_new_node_type.py | 72 + .../alembic/versions/drop_slack_tables_001.py | 60 + .../e252efacd083_agent_createddate_added.py | 51 + .../e3f4a5b6c7d8_add_aurora_chat_style.py | 54 + ...6a7b8c9d0_add_customer_is_authenticated.py | 32 + .../e65e1b8e7e3c_add_jira_agent_config.py | 61 + ...add_rate_limiting_fields_to_agent_model.py | 52 + ...8e9_add_sunrise_style_and_quick_actions.py | 65 + .../f7a1c2d3e4b5_add_faq_help_center.py | 182 + ...4f644fc0c_added_user_id_in_rating_table.py | 50 + .../versions/fcm_tokens_per_device_001.py | 88 + ...48f854bcbf_add_ticket_fields_to_session.py | 58 + .../versions/hc_article_image_paths.py | 44 + .../alembic/versions/idx_kq_org_status_001.py | 35 + .../alembic/versions/merge_hc_url_path_001.py | 50 + .../alembic/versions/merge_tkt_csat_001.py | 44 + .../alembic/versions/merge_tkt_phone_001.py | 44 + .../versions/normalise_upload_prefix_001.py | 79 + .../versions/purge_mangled_crawl_pages_001.py | 103 + .../versions/relative_hc_upload_urls.py | 48 + .../versions/repair_agent_role_seed_001.py | 184 + .../versions/session_conversation_key_001.py | 55 + .../alembic/versions/widget_key_lookup_001.py | 34 + backend/app/__init__.py | 27 + backend/app/api/__init__.py | 17 + backend/app/config/.gitignore | 3 + backend/app/core/application.py | 43 + backend/app/core/auth.py | 517 + backend/app/core/auth_utils.py | 224 + backend/app/core/config.py | 471 + backend/app/core/cors.py | 387 + backend/app/core/default_templates.py | 116 + backend/app/core/edition.py | 54 + backend/app/core/encryption.py | 223 + backend/app/core/error_handlers.py | 73 + backend/app/core/exceptions.py | 19 + backend/app/core/external_urls.py | 29 + backend/app/core/file_validation.py | 87 + backend/app/core/help_center_host.py | 149 + backend/app/core/local_file_urls.py | 58 + backend/app/core/logger.py | 73 + backend/app/core/model_catalog.py | 201 + backend/app/core/oauth_state.py | 63 + backend/app/core/outbound_http.py | 139 + backend/app/core/outbound_policy.py | 119 + backend/app/core/params.py | 32 + backend/app/core/redis.py | 61 + backend/app/core/s3.py | 310 + backend/app/core/security.py | 489 + backend/app/database.py | 65 + backend/app/main.py | 388 + backend/pytest.ini | 10 + backend/requirements.txt | 130 + frontend/.editorconfig | 6 + frontend/.env.example | 33 + frontend/.gitignore | 38 + frontend/.npmrc | 9 + frontend/.prettierrc.json | 7 + frontend/README.md | 64 + frontend/env.d.ts | 30 + frontend/eslint.config.js | 73 + frontend/package-lock.json | 12970 ++++++++++++++++ frontend/package.json | 104 + frontend/scripts/build-webclient-dev.ts | 76 + frontend/scripts/build-webclient-prod.ts | 69 + frontend/scripts/build-widget-dev.ts | 67 + frontend/scripts/build-widget-docker.ts | 67 + frontend/scripts/build-widget-prod.ts | 67 + frontend/scripts/build-widget.ts | 63 + frontend/scripts/generate-dev-env.ts | 56 + frontend/scripts/generate-prod-env.ts | 57 + frontend/tsconfig.app.json | 13 + frontend/tsconfig.json | 14 + frontend/tsconfig.node.json | 19 + frontend/tsconfig.vitest.json | 11 + frontend/vite.config.ts | 153 + frontend/vite.widget.config.ts | 44 + frontend/vite.widget.docker.config.ts | 44 + frontend/vitest.config.ts | 23 + 167 files changed, 26082 insertions(+) create mode 100644 backend/.coveragerc create mode 100644 backend/.env.example create mode 100644 backend/.gitignore create mode 100644 backend/__init__.py create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/README create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/alembic/versions/013174e15242_add_priority_to_knowledge_queue.py create mode 100644 backend/alembic/versions/0cf34ff05aee_merge_guardrails_and_chat_initiation.py create mode 100644 backend/alembic/versions/0cf5fa1db0cb_add_teamid_to_agent_sessions.py create mode 100644 backend/alembic/versions/0ee9dbc73d9e_removed_message_node_specific_fields.py create mode 100644 backend/alembic/versions/1493b8279a2f_added_shopify_models.py create mode 100644 backend/alembic/versions/186c65ae9406_added_rating_table.py create mode 100644 backend/alembic/versions/2037204bbebf_add_guardrails_node_type.py create mode 100644 backend/alembic/versions/2381319791e8_merge_multiple_heads.py create mode 100644 backend/alembic/versions/2fd826882d08_add_workflow_history_column.py create mode 100644 backend/alembic/versions/30caca239191_add_jira_config.py create mode 100644 backend/alembic/versions/520df92643e3_added_replypier_ai_model.py create mode 100644 backend/alembic/versions/52e6ca829bb8_make_user_org_and_role_nullable.py create mode 100644 backend/alembic/versions/537f2b7d4a7c_added_end_chat_support.py create mode 100644 backend/alembic/versions/5b5f936cd9e4_role_remove_name_alone_as_unique.py create mode 100644 backend/alembic/versions/621095beb234_add_mcp_tools_tables.py create mode 100644 backend/alembic/versions/6885c9f36712_add_chat_style_and_widget_position_to_agent_customizations.py create mode 100644 backend/alembic/versions/7991b4fc00d8_added_wlecome_header_and_title.py create mode 100644 backend/alembic/versions/7af9828611e7_add_oauth_state_fields_to_shopify_shops.py create mode 100644 backend/alembic/versions/7fd0447b3d45_workflow_tables_added.py create mode 100644 backend/alembic/versions/85364d188f15_merge_heads.py create mode 100644 backend/alembic/versions/89a4f4898aa4_change_datetime_to_timestamp_with_.py create mode 100644 backend/alembic/versions/910bfe03518b_make_knowledge_queue_user_id_nullable.py create mode 100644 backend/alembic/versions/91a148726029_add_extra_data_to_agent_sessions.py create mode 100644 backend/alembic/versions/9a01e6b653b2_add_agent_shopify_config_with_shop_.py create mode 100644 backend/alembic/versions/9ce623009955_added_new_node_type_user_input.py 14-13-41-620.py create mode 100644 backend/alembic/versions/a1b2c3d4e5f6_add_chat_initiation_messages.py create mode 100644 backend/alembic/versions/a1dbf557bbb0_merge_ai_ticketing_agent_queue_heads.py create mode 100644 backend/alembic/versions/a8efc6b31aa8_merge_crm_and_chat_session_idx_heads.py create mode 100644 backend/alembic/versions/abc123456789_add_file_attachments_table.py create mode 100644 backend/alembic/versions/add_agent_ai_replies_toggle_001.py create mode 100644 backend/alembic/versions/add_agent_guardrail_prompt_001.py create mode 100644 backend/alembic/versions/add_agent_ticketing_toggle_001.py create mode 100644 backend/alembic/versions/add_ai_disclaimer_toggle_001.py create mode 100644 backend/alembic/versions/add_allow_attachments_001.py create mode 100644 backend/alembic/versions/add_allow_new_chat_001.py create mode 100644 backend/alembic/versions/add_allowed_attachment_types_001.py create mode 100644 backend/alembic/versions/add_channel_tables_001.py create mode 100644 backend/alembic/versions/add_chat_session_idx_001.py create mode 100644 backend/alembic/versions/add_crm_customer_syncs_001.py create mode 100644 backend/alembic/versions/add_crm_tables_001_add_crm_tables.py create mode 100644 backend/alembic/versions/add_customer_phone_001_add_customer_phone.py create mode 100644 backend/alembic/versions/add_db_connector_row_scope.py create mode 100644 backend/alembic/versions/add_faq_seo_fields.py create mode 100644 backend/alembic/versions/add_guardrails_layer_001.py create mode 100644 backend/alembic/versions/add_hc_chat_widget_001_add_chat_widget_enabled.py create mode 100644 backend/alembic/versions/add_hc_cta_enabled_001_add_cta_enabled.py create mode 100644 backend/alembic/versions/add_hc_faq_feedback_001_add_faq_feedback.py create mode 100644 backend/alembic/versions/add_hc_faq_slug_001_add_faq_slug.py create mode 100644 backend/alembic/versions/add_hc_faq_url_path_001_add_preserved_paths.py create mode 100644 backend/alembic/versions/add_hc_favicon_url.py create mode 100644 backend/alembic/versions/add_hc_job_meter_001_add_job_metering.py create mode 100644 backend/alembic/versions/add_invest_glassbox_001_add_glassbox.py create mode 100644 backend/alembic/versions/add_investigation_metered.py create mode 100644 backend/alembic/versions/add_investigation_runs_001_add_runs.py create mode 100644 backend/alembic/versions/add_mcp_usage_guidance_001.py create mode 100644 backend/alembic/versions/add_notif_prefs_001_user_notification_settings.py create mode 100644 backend/alembic/versions/add_openai_compat_ai_model_001.py create mode 100644 backend/alembic/versions/add_require_token_auth_001.py create mode 100644 backend/alembic/versions/add_run_connector_status_001.py create mode 100644 backend/alembic/versions/add_sentiment_analysis_001.py create mode 100644 backend/alembic/versions/add_slack_integration_001.py create mode 100644 backend/alembic/versions/add_ticket_csat.py create mode 100644 backend/alembic/versions/add_ticketing_core_001_add_native_ticketing.py create mode 100644 backend/alembic/versions/add_tkt_db_ssh_001_add_ssh.py create mode 100644 backend/alembic/versions/add_tkt_phase4_001_add_phase4.py create mode 100644 backend/alembic/versions/add_widget_apps_001.py create mode 100644 backend/alembic/versions/agent_quick_replies_001.py create mode 100644 backend/alembic/versions/auth_sessions_001.py create mode 100644 backend/alembic/versions/b2c3d4e5f6a7_add_customer_meta_data.py create mode 100644 backend/alembic/versions/b365b8447598_initial_version.py create mode 100644 backend/alembic/versions/b7c4e91a2d38_add_agent_queue_and_people_permissions.py create mode 100644 backend/alembic/versions/bdaa7a5d6109_removed_node_specific_fields.py create mode 100644 backend/alembic/versions/c1d2e3f4a5b6_add_premium_chat_styles_and_show_citations.py create mode 100644 backend/alembic/versions/c3d4e5f6a7b8_add_lead_capture.py create mode 100644 backend/alembic/versions/chat_channel_status_001.py create mode 100644 backend/alembic/versions/crm_sync_varchar_001_crm_target_varchar.py create mode 100644 backend/alembic/versions/d2e3f4a5b6c7_add_contact_collection_columns.py create mode 100644 backend/alembic/versions/d4e5f6a7b8c9_add_customer_merge.py create mode 100644 backend/alembic/versions/d9a4b9902f80_add_attempts_to_otp_table.py create mode 100644 backend/alembic/versions/dc827ab30bd4_add_progress_tracking_to_knowledge_queue.py create mode 100644 backend/alembic/versions/dd0e5ceefe4b_added_new_node_type.py create mode 100644 backend/alembic/versions/drop_slack_tables_001.py create mode 100644 backend/alembic/versions/e252efacd083_agent_createddate_added.py create mode 100644 backend/alembic/versions/e3f4a5b6c7d8_add_aurora_chat_style.py create mode 100644 backend/alembic/versions/e5f6a7b8c9d0_add_customer_is_authenticated.py create mode 100644 backend/alembic/versions/e65e1b8e7e3c_add_jira_agent_config.py create mode 100644 backend/alembic/versions/f3dad24474e0_add_rate_limiting_fields_to_agent_model.py create mode 100644 backend/alembic/versions/f4a5b6c7d8e9_add_sunrise_style_and_quick_actions.py create mode 100644 backend/alembic/versions/f7a1c2d3e4b5_add_faq_help_center.py create mode 100644 backend/alembic/versions/fa04f644fc0c_added_user_id_in_rating_table.py create mode 100644 backend/alembic/versions/fcm_tokens_per_device_001.py create mode 100644 backend/alembic/versions/ff48f854bcbf_add_ticket_fields_to_session.py create mode 100644 backend/alembic/versions/hc_article_image_paths.py create mode 100644 backend/alembic/versions/idx_kq_org_status_001.py create mode 100644 backend/alembic/versions/merge_hc_url_path_001.py create mode 100644 backend/alembic/versions/merge_tkt_csat_001.py create mode 100644 backend/alembic/versions/merge_tkt_phone_001.py create mode 100644 backend/alembic/versions/normalise_upload_prefix_001.py create mode 100644 backend/alembic/versions/purge_mangled_crawl_pages_001.py create mode 100644 backend/alembic/versions/relative_hc_upload_urls.py create mode 100644 backend/alembic/versions/repair_agent_role_seed_001.py create mode 100644 backend/alembic/versions/session_conversation_key_001.py create mode 100644 backend/alembic/versions/widget_key_lookup_001.py create mode 100644 backend/app/__init__.py create mode 100644 backend/app/api/__init__.py create mode 100644 backend/app/config/.gitignore create mode 100644 backend/app/core/application.py create mode 100644 backend/app/core/auth.py create mode 100644 backend/app/core/auth_utils.py create mode 100644 backend/app/core/config.py create mode 100644 backend/app/core/cors.py create mode 100644 backend/app/core/default_templates.py create mode 100644 backend/app/core/edition.py create mode 100644 backend/app/core/encryption.py create mode 100644 backend/app/core/error_handlers.py create mode 100644 backend/app/core/exceptions.py create mode 100644 backend/app/core/external_urls.py create mode 100644 backend/app/core/file_validation.py create mode 100644 backend/app/core/help_center_host.py create mode 100644 backend/app/core/local_file_urls.py create mode 100644 backend/app/core/logger.py create mode 100644 backend/app/core/model_catalog.py create mode 100644 backend/app/core/oauth_state.py create mode 100644 backend/app/core/outbound_http.py create mode 100644 backend/app/core/outbound_policy.py create mode 100644 backend/app/core/params.py create mode 100644 backend/app/core/redis.py create mode 100644 backend/app/core/s3.py create mode 100644 backend/app/core/security.py create mode 100644 backend/app/database.py create mode 100644 backend/app/main.py create mode 100644 backend/pytest.ini create mode 100644 backend/requirements.txt create mode 100644 frontend/.editorconfig create mode 100644 frontend/.env.example create mode 100644 frontend/.gitignore create mode 100644 frontend/.npmrc create mode 100644 frontend/.prettierrc.json create mode 100644 frontend/README.md create mode 100644 frontend/env.d.ts create mode 100644 frontend/eslint.config.js create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/scripts/build-webclient-dev.ts create mode 100644 frontend/scripts/build-webclient-prod.ts create mode 100644 frontend/scripts/build-widget-dev.ts create mode 100644 frontend/scripts/build-widget-docker.ts create mode 100644 frontend/scripts/build-widget-prod.ts create mode 100644 frontend/scripts/build-widget.ts create mode 100644 frontend/scripts/generate-dev-env.ts create mode 100644 frontend/scripts/generate-prod-env.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/tsconfig.vitest.json create mode 100644 frontend/vite.config.ts create mode 100644 frontend/vite.widget.config.ts create mode 100644 frontend/vite.widget.docker.config.ts create mode 100644 frontend/vitest.config.ts diff --git a/backend/.coveragerc b/backend/.coveragerc new file mode 100644 index 0000000..96fbb30 --- /dev/null +++ b/backend/.coveragerc @@ -0,0 +1,20 @@ +[run] +source = app +omit = + */tests/* + */__init__.py + */migrations/* + */alembic/* + +[report] +exclude_lines = + pragma: no cover + def __repr__ + raise NotImplementedError + if __name__ == .__main__.: + pass + raise ImportError + except ImportError: + +[html] +directory = coverage_html \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..1132e5d --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,170 @@ +# Database Configuration +DATABASE_URL=postgresql+psycopg://postgres:postgres@db:5432/replypier + +# CORS Configuration add backend url to allow cors from frontend +CORS_ORIGINS=["http://localhost:3000","http://localhost:8000"] + +# Firebase Configuration +FIREBASE_CREDENTIALS=app/config/firebase-config.json + +# Widget Configuration # prod image remove :3000 port, since frontend is running on port 80 +VITE_WIDGET_URL=http://localhost:8000 +FRONTEND_URL=http://localhost:3000 +# Public URL of THIS backend. Self-hosters MUST set this to their domain +# (e.g. https://support.example.com) — the chat widget derives its API/WS URLs +# from it, so if it stays localhost the embedded widget can't reach the backend. +BACKEND_URL=http://localhost:8000 + +# Public help center URL style: +# path -> {BACKEND_URL}/help/{slug}, served same-origin (no DNS/TLS/proxy). Default, best for self-host. +# subdomain -> https://{slug}. (needs wildcard DNS + TLS; used by cloud). +# A verified custom domain always overrides this. +HELP_CENTER_PUBLIC_MODE=path + +# JWT Configuration +# These sign your auth and conversation tokens - anyone who knows them can forge +# tokens for any user, so the placeholders below must be replaced. Outside +# ENVIRONMENT=development the app refuses to start while they are still in place. +# Generate each (64 hex characters) with: +# openssl rand -hex 32 +JWT_SECRET_KEY=your_jwt_secret_key_here +JWT_ALGORITHM=HS256 +CONVERSATION_SECRET_KEY=your_conversation_secret_key_here + +# Encryption and Redis +# REQUIRED outside development: encrypts API keys, provider/OAuth credentials, chat +# messages, session summaries and agent memory at rest. Losing or changing it makes +# existing data unreadable, so back it up separately from the database. +# +# The value is a Fernet key that has been base64-encoded a second time: a +# 60-character string ending in '='. Generate one with either of: +# python -c "import base64;from cryptography.fernet import Fernet;print(base64.b64encode(Fernet.generate_key()).decode())" +# openssl rand -base64 32 | tr '+/' '-_' | tr -d '\n' | base64 +# +# Leaving the placeholder below in place means: outside development the app +# refuses to start; in development it generates a throwaway key on every boot, +# so anything encrypted before a restart becomes unreadable after it. +ENCRYPTION_KEY=your_fernet_encryption_key_here + +# Escape hatch for deployments created before ENCRYPTION_KEY had to be set, whose +# data is encrypted under the old public default. It downgrades the startup error +# to a warning so those instances still boot. Anyone with a copy of the database +# can read their conversations and stored credentials, so treat it as temporary. +# Moving off the old key means re-encrypting what it wrote, not just swapping the +# value - see the key-id rotation note at the top of app/core/encryption.py. +# ALLOW_INSECURE_ENCRYPTION_KEY=false +REDIS_ENABLED=true +REDIS_URL=redis://localhost:6379/0 + +# Environment +ENVIRONMENT=development + +# S3 Configuration +S3_FILE_STORAGE=false +S3_BUCKET=replypier-uploads +S3_REGION=us-east-1 +# Leave both blank on EC2/ECS/EKS to use the attached IAM role — boto3 then +# resolves credentials from its default chain. Set them only for static keys; +# a placeholder value here is treated as a real (invalid) key. +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +# Lifetime of generated presigned URLs, in seconds. They are re-signed on every +# response, so this only needs to outlive a page render. AWS rejects SigV4 +# presigns above 604800 (7 days); larger values are clamped to that. +S3_PRESIGN_EXPIRY_SECONDS=3600 + +# Jira — create an OAuth 2.0 (3LO) app at developer.atlassian.com. Unlike the +# other integrations the redirect URI is configured here rather than derived +# from BACKEND_URL, so it must match the callback registered on the Atlassian +# app exactly (route: {API_V1_STR}/jira/callback). +JIRA_CLIENT_ID=your_jira_client_id_here +JIRA_CLIENT_SECRET=your_jira_client_secret_here +JIRA_REDIRECT_URI=http://localhost:8000/api/v1/jira/callback + +SHOPIFY_API_KEY=your_shopify_key +SHOPIFY_API_SECRET=your_shopify_secret +SHOPIFY_API_VERSION=2025-04 + +# Slack channel — create an app at api.slack.com/apps. The OAuth redirect URI is +# derived from BACKEND_URL, so add {BACKEND_URL}/api/v1/channels/slack/callback +# to the app's Redirect URLs. Without SLACK_SIGNING_SECRET every incoming Slack +# webhook is rejected with a 403, since signature checks fail closed. +SLACK_CLIENT_ID=your_slack_client_id +SLACK_CLIENT_SECRET=your_slack_client_secret +SLACK_SIGNING_SECRET=your_slack_signing_secret + +# CRM lead push (HubSpot / Pipedrive) — create OAuth apps at +# developers.hubspot.com and developers.pipedrive.com. Redirect URIs derive from +# BACKEND_URL: add {BACKEND_URL}/api/v1/crm/hubspot/callback and +# {BACKEND_URL}/api/v1/crm/pipedrive/callback to the respective apps. +HUBSPOT_CLIENT_ID=your_hubspot_client_id +HUBSPOT_CLIENT_SECRET=your_hubspot_client_secret +PIPEDRIVE_CLIENT_ID=your_pipedrive_client_id +PIPEDRIVE_CLIENT_SECRET=your_pipedrive_client_secret + +# Meta channels (WhatsApp Cloud API, Messenger, Instagram) — create an app at +# developers.facebook.com. META_WEBHOOK_VERIFY_TOKEN is any random string you pick +# and also enter in the App Dashboard webhook config. +META_APP_ID=your_meta_app_id +META_APP_SECRET=your_meta_app_secret +META_WEBHOOK_VERIFY_TOKEN=any_random_string_you_choose +META_GRAPH_VERSION=v21.0 +# Only for cloud-hosted one-click onboarding; leave blank for self-hosting. +# META_CONFIG_ID: WhatsApp Embedded Signup. META_MESSENGER_CONFIG_ID: Facebook +# Login for Business, for connecting a Page (Messenger + Instagram DM). +META_CONFIG_ID= +META_MESSENGER_CONFIG_ID= +# Instagram API with Instagram Login (no Facebook Page needed). These are the +# Instagram app id/secret from the app's Instagram product, NOT the Meta ones. +INSTAGRAM_APP_ID= +INSTAGRAM_APP_SECRET= +# Comma-separated emails allowed to use one-click Meta signup. Empty = everyone. +# Set this while the Meta app is in App Review so the flow can be exercised in +# production without offering customers a login that would fail for them. +SIGNUP_ALLOWED_EMAILS= + +VERIFY_SSL_CERTIFICATES=true + +# Which addresses the AI base URL, MCP server URLs and ticket database (or its +# SSH host) may point at. These are dialled by this server, so: +# any -> no restriction. Default; lets a self-hosted install reach its +# own network (Ollama on localhost, an internal MCP server, a +# private database). +# public_only -> refuse private, loopback, link-local and reserved addresses. +# Use it when people you don't control can configure these +# (a shared or multi-tenant deployment). Checked on the +# address actually dialled, redirects included; these +# connections then bypass HTTP(S)_PROXY. +OUTBOUND_URL_POLICY=any + +# STDIO MCP connectors run a command on this server, typed by an org admin. +# Keep true on your own install; set false wherever people you don't control +# can add connectors (a shared or multi-tenant deployment). +MCP_STDIO_ENABLED=true + +# Enhanced Website Knowledge Base Configuration +KB_MAX_DEPTH=5 +KB_MAX_LINKS=25 +KB_MIN_CONTENT_LENGTH=100 +KB_TIMEOUT=30 +KB_MAX_RETRIES=3 +KB_MAX_WORKERS=5 +KB_BATCH_SIZE=5 +KB_SOURCE_TIME_LIMIT_SECONDS=1800 +KB_PDF_MAX_PAGES=300 + +# Seconds a single agent run (model + tool calls) may take before it is +# cancelled and the visitor gets an error reply. Raise it if agents with many +# MCP/Shopify tools hit the limit on legitimate answers. +AGENT_RUN_TIMEOUT=90 + +# agno's own run reporting to api.agno.com. Off unless set to true here; when on, +# it runs inside every agent run and counts against AGENT_RUN_TIMEOUT. +# AGNO_TELEMETRY=true +# AGNO_MONITOR=true + +# Tool calls an agent may make in one turn before it has to answer from what it +# already retrieved. Hitting this is not an error — the agent answers without +# further tools. Raise it if agents with many knowledge sources or MCP tools are +# answering from too little context. +AGENT_TOOL_CALL_LIMIT=5 diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..ba57c8d --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.pyc +.env +firebase-credentials.json +uploads/ +# Coverage reports +.coverage +coverage_html/ +htmlcov/ +app/enterprise/ \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 0000000..11ab711 --- /dev/null +++ b/backend/__init__.py @@ -0,0 +1,16 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..0e50bdb --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,117 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +# Use forward slashes (/) also on windows to provide an os agnostic path +script_location = alembic + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python>=3.9 or backports.zoneinfo library. +# Any required deps can installed by adding `alembic[tz]` to the pip requirements +# string value is passed to ZoneInfo() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:alembic/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +# version_path_separator = newline +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/README b/backend/alembic/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/backend/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..652aea4 --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,74 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# alembic/env.py + +import os +from logging.config import fileConfig +from sqlalchemy import engine_from_config +from sqlalchemy import pool +from alembic import context +from app.database import Base +from app.models import Organization, User, Role, AIConfig, Agent, Knowledge, KnowledgeToAgent, AIModelType +from app.core.config import settings + +# this is the Alembic Config object, which provides access to the values within the .ini file +config = context.config + +# Override sqlalchemy.url with our database URL from settings + +config.set_main_option('sqlalchemy.url', settings.DATABASE_URL) + +# Interpret the config file for Python logging. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# add your model's MetaData object here +# for 'autogenerate' support +target_metadata = Base.metadata + + +def run_migrations_offline(): + context.configure( + url=config.get_main_option("sqlalchemy.url"), + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure(connection=connection, + target_metadata=target_metadata) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/013174e15242_add_priority_to_knowledge_queue.py b/backend/alembic/versions/013174e15242_add_priority_to_knowledge_queue.py new file mode 100644 index 0000000..5fbf9c0 --- /dev/null +++ b/backend/alembic/versions/013174e15242_add_priority_to_knowledge_queue.py @@ -0,0 +1,46 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add_priority_to_knowledge_queue + +Revision ID: 013174e15242 +Revises: dc827ab30bd4 +Create Date: 2025-10-06 14:35:40.640506 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '013174e15242' +down_revision: Union[str, None] = 'dc827ab30bd4' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add priority column with default value 0 and create index + op.add_column('knowledge_queue', sa.Column('priority', sa.Integer(), nullable=False, server_default='0')) + op.create_index(op.f('ix_knowledge_queue_priority'), 'knowledge_queue', ['priority'], unique=False) + + +def downgrade() -> None: + # Remove index and column + op.drop_index(op.f('ix_knowledge_queue_priority'), table_name='knowledge_queue') + op.drop_column('knowledge_queue', 'priority') diff --git a/backend/alembic/versions/0cf34ff05aee_merge_guardrails_and_chat_initiation.py b/backend/alembic/versions/0cf34ff05aee_merge_guardrails_and_chat_initiation.py new file mode 100644 index 0000000..6135766 --- /dev/null +++ b/backend/alembic/versions/0cf34ff05aee_merge_guardrails_and_chat_initiation.py @@ -0,0 +1,42 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""merge guardrails and chat initiation + +Revision ID: 0cf34ff05aee +Revises: 2037204bbebf, a1b2c3d4e5f6 +Create Date: 2025-10-31 19:08:54.718682 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0cf34ff05aee' +down_revision: Union[str, None] = ('2037204bbebf', 'a1b2c3d4e5f6') +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/backend/alembic/versions/0cf5fa1db0cb_add_teamid_to_agent_sessions.py b/backend/alembic/versions/0cf5fa1db0cb_add_teamid_to_agent_sessions.py new file mode 100644 index 0000000..6d4fc00 --- /dev/null +++ b/backend/alembic/versions/0cf5fa1db0cb_add_teamid_to_agent_sessions.py @@ -0,0 +1,86 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add_teamid_to_agent_sessions + +Revision ID: 0cf5fa1db0cb +Revises: 91a148726029 +Create Date: 2025-03-21 13:57:55.161703 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0cf5fa1db0cb' +down_revision: Union[str, None] = '91a148726029' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +def upgrade() -> None: + # Check if 'ai' schema and agent_sessions table exist before attempting to modify + conn = op.get_bind() + + # Check if schema exists + schema_result = conn.execute(sa.text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'ai'")) + schema_exists = schema_result.fetchone() is not None + + if not schema_exists: + print("Schema 'ai' does not exist, skipping migration") + return + + # Check if table exists + table_result = conn.execute(sa.text( + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'ai' AND table_name = 'agent_sessions'" + )) + table_exists = table_result.fetchone() is not None + + if table_exists: + # Add team_id column to ai.agent_sessions table + op.add_column('agent_sessions', + sa.Column('team_id', sa.UUID(), nullable=True), + schema='ai' + ) + else: + print("Table 'ai.agent_sessions' does not exist, skipping migration") + + +def downgrade() -> None: + # Check if 'ai' schema and agent_sessions table exist before attempting to modify + conn = op.get_bind() + + # Check if schema exists + schema_result = conn.execute(sa.text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'ai'")) + schema_exists = schema_result.fetchone() is not None + + if not schema_exists: + print("Schema 'ai' does not exist, skipping migration rollback") + return + + # Check if table exists + table_result = conn.execute(sa.text( + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'ai' AND table_name = 'agent_sessions'" + )) + table_exists = table_result.fetchone() is not None + + if table_exists: + # Drop team_id column from ai.agent_sessions table + op.drop_column('agent_sessions', 'team_id', schema='ai') + else: + print("Table 'ai.agent_sessions' does not exist, skipping migration rollback") diff --git a/backend/alembic/versions/0ee9dbc73d9e_removed_message_node_specific_fields.py b/backend/alembic/versions/0ee9dbc73d9e_removed_message_node_specific_fields.py new file mode 100644 index 0000000..2d414dd --- /dev/null +++ b/backend/alembic/versions/0ee9dbc73d9e_removed_message_node_specific_fields.py @@ -0,0 +1,48 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""removed message node specific fields + +Revision ID: 0ee9dbc73d9e +Revises: bdaa7a5d6109 +Create Date: 2025-07-23 12:19:03.629271 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0ee9dbc73d9e' +down_revision: Union[str, None] = 'bdaa7a5d6109' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.drop_column('workflow_nodes', 'message_text') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('workflow_nodes', sa.Column('message_text', sa.TEXT(), autoincrement=False, nullable=True)) + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/1493b8279a2f_added_shopify_models.py b/backend/alembic/versions/1493b8279a2f_added_shopify_models.py new file mode 100644 index 0000000..2f2947d --- /dev/null +++ b/backend/alembic/versions/1493b8279a2f_added_shopify_models.py @@ -0,0 +1,63 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""added shopify models + +Revision ID: 1493b8279a2f +Revises: 520df92643e3 +Create Date: 2025-04-10 13:50:54.439561 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '1493b8279a2f' +down_revision: Union[str, None] = '520df92643e3' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.create_table('shopify_shops', + sa.Column('id', sa.String(), nullable=False), + sa.Column('shop_domain', sa.String(), nullable=False), + sa.Column('access_token', sa.String(), nullable=True), + sa.Column('scope', sa.String(), nullable=True), + sa.Column('is_installed', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_shopify_shops_shop_domain'), 'shopify_shops', ['shop_domain'], unique=True) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.drop_index(op.f('ix_shopify_shops_shop_domain'), table_name='shopify_shops') + op.drop_table('shopify_shops') + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/186c65ae9406_added_rating_table.py b/backend/alembic/versions/186c65ae9406_added_rating_table.py new file mode 100644 index 0000000..f95cc22 --- /dev/null +++ b/backend/alembic/versions/186c65ae9406_added_rating_table.py @@ -0,0 +1,72 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""added rating table + +Revision ID: 186c65ae9406 +Revises: 537f2b7d4a7c +Create Date: 2025-03-04 12:18:45.004571 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '186c65ae9406' +down_revision: Union[str, None] = '537f2b7d4a7c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.create_table('ratings', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('session_id', sa.UUID(), nullable=False), + sa.Column('customer_id', sa.UUID(), nullable=True), + sa.Column('agent_id', sa.UUID(), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('rating', sa.Integer(), nullable=False), + sa.Column('feedback', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_ratings_id'), 'ratings', ['id'], unique=False) + op.alter_column('agents', 'ask_for_rating', + existing_type=sa.BOOLEAN(), + nullable=True, + existing_server_default=sa.text('true')) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('agents', 'ask_for_rating', + existing_type=sa.BOOLEAN(), + nullable=False, + existing_server_default=sa.text('true')) + op.drop_index(op.f('ix_ratings_id'), table_name='ratings') + op.drop_table('ratings') + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/2037204bbebf_add_guardrails_node_type.py b/backend/alembic/versions/2037204bbebf_add_guardrails_node_type.py new file mode 100644 index 0000000..d628523 --- /dev/null +++ b/backend/alembic/versions/2037204bbebf_add_guardrails_node_type.py @@ -0,0 +1,59 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add guardrails node type + +Revision ID: 2037204bbebf +Revises: 7af9828611e7 +Create Date: 2025-10-29 11:47:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +# revision identifiers, used by Alembic. +revision: str = '2037204bbebf' +down_revision: Union[str, None] = '7af9828611e7' + + +def upgrade() -> None: + # Add GUARDRAILS to the NodeType enum + op.execute("ALTER TYPE nodetype ADD VALUE IF NOT EXISTS 'GUARDRAILS'") + + +def downgrade() -> None: + # First, delete or convert any existing GUARDRAILS nodes + # Option 1: Delete GUARDRAILS nodes (uncomment if you want to delete them) + # op.execute("DELETE FROM workflow_nodes WHERE node_type = 'GUARDRAILS'") + + # Option 2: Convert GUARDRAILS nodes to ACTION nodes (safer - preserves data) + op.execute("UPDATE workflow_nodes SET node_type = 'ACTION' WHERE node_type = 'GUARDRAILS'") + + # Create a new enum without the guardrails value + op.execute(""" + CREATE TYPE nodetype_new AS ENUM ( + 'MESSAGE','LLM','CONDITION','FORM','ACTION','HUMAN_TRANSFER','WAIT','END','LANDING_PAGE','USER_INPUT' + ) + """) + + # Update the column type to use the new enum + op.execute("ALTER TABLE workflow_nodes ALTER COLUMN node_type TYPE nodetype_new USING node_type::text::nodetype_new") + + # Drop the old enum + op.execute("DROP TYPE nodetype") + + # Rename the new enum to the original name + op.execute("ALTER TYPE nodetype_new RENAME TO nodetype") diff --git a/backend/alembic/versions/2381319791e8_merge_multiple_heads.py b/backend/alembic/versions/2381319791e8_merge_multiple_heads.py new file mode 100644 index 0000000..3017663 --- /dev/null +++ b/backend/alembic/versions/2381319791e8_merge_multiple_heads.py @@ -0,0 +1,42 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""merge multiple heads + +Revision ID: 2381319791e8 +Revises: add_allowed_attachment_types_001, add_require_token_auth_001 +Create Date: 2026-01-08 10:31:54.013991 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '2381319791e8' +down_revision: Union[str, None] = ('add_allowed_attachment_types_001', 'add_require_token_auth_001') +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/backend/alembic/versions/2fd826882d08_add_workflow_history_column.py b/backend/alembic/versions/2fd826882d08_add_workflow_history_column.py new file mode 100644 index 0000000..181e10d --- /dev/null +++ b/backend/alembic/versions/2fd826882d08_add_workflow_history_column.py @@ -0,0 +1,44 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add_workflow_history_column + +Revision ID: 2fd826882d08 +Revises: 0ee9dbc73d9e +Create Date: 2025-07-24 21:10:23.798939 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '2fd826882d08' +down_revision: Union[str, None] = '0ee9dbc73d9e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add workflow_history column to store form submissions and workflow interactions + op.add_column('session_to_agents', sa.Column('workflow_history', sa.JSON(), nullable=True)) + + +def downgrade() -> None: + # Remove workflow_history column + op.drop_column('session_to_agents', 'workflow_history') diff --git a/backend/alembic/versions/30caca239191_add_jira_config.py b/backend/alembic/versions/30caca239191_add_jira_config.py new file mode 100644 index 0000000..90dadf6 --- /dev/null +++ b/backend/alembic/versions/30caca239191_add_jira_config.py @@ -0,0 +1,64 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add jira config + +Revision ID: 30caca239191 +Revises: fa04f644fc0c +Create Date: 2025-03-06 20:27:48.691533 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '30caca239191' +down_revision: Union[str, None] = 'fa04f644fc0c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.create_table('jira_tokens', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('access_token', sa.String(), nullable=False), + sa.Column('refresh_token', sa.String(), nullable=False), + sa.Column('token_type', sa.String(), nullable=False), + sa.Column('expires_at', sa.DateTime(), nullable=False), + sa.Column('cloud_id', sa.String(), nullable=False), + sa.Column('site_url', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_jira_tokens_id'), 'jira_tokens', ['id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_jira_tokens_id'), table_name='jira_tokens') + op.drop_table('jira_tokens') + # ### end Alembic commands ### + diff --git a/backend/alembic/versions/520df92643e3_added_replypier_ai_model.py b/backend/alembic/versions/520df92643e3_added_replypier_ai_model.py new file mode 100644 index 0000000..51560c5 --- /dev/null +++ b/backend/alembic/versions/520df92643e3_added_replypier_ai_model.py @@ -0,0 +1,46 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Added replypier AI model + +Revision ID: 520df92643e3 +Revises: 85364d188f15 +Create Date: 2025-03-28 23:27:11.906265 + +""" +from typing import Sequence, Union +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '520df92643e3' +down_revision: Union[str, None] = '85364d188f15' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Create a new temporary type with the new value + op.execute("ALTER TYPE aimodeltype ADD VALUE IF NOT EXISTS 'REPLYPIER'") + # ### end Alembic commands ### + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Note: PostgreSQL does not support removing enum values + # We can only create a new enum without the value and replace the old one + pass + # ### end Alembic commands ### diff --git a/backend/alembic/versions/52e6ca829bb8_make_user_org_and_role_nullable.py b/backend/alembic/versions/52e6ca829bb8_make_user_org_and_role_nullable.py new file mode 100644 index 0000000..ff9c5b6 --- /dev/null +++ b/backend/alembic/versions/52e6ca829bb8_make_user_org_and_role_nullable.py @@ -0,0 +1,58 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""make user org and role nullable + +Revision ID: 52e6ca829bb8 +Revises: b365b8447598 +Create Date: 2024-02-04 18:54:47.823 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '52e6ca829bb8' +down_revision: Union[str, None] = 'b365b8447598' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Make organization_id nullable + op.alter_column('users', 'organization_id', + existing_type=sa.UUID(), + nullable=True) + + # Make role_id nullable + op.alter_column('users', 'role_id', + existing_type=sa.INTEGER(), + nullable=True) + + +def downgrade() -> None: + # Revert organization_id to non-nullable + op.alter_column('users', 'organization_id', + existing_type=sa.UUID(), + nullable=False) + + # Revert role_id to non-nullable + op.alter_column('users', 'role_id', + existing_type=sa.INTEGER(), + nullable=False) diff --git a/backend/alembic/versions/537f2b7d4a7c_added_end_chat_support.py b/backend/alembic/versions/537f2b7d4a7c_added_end_chat_support.py new file mode 100644 index 0000000..671a9a9 --- /dev/null +++ b/backend/alembic/versions/537f2b7d4a7c_added_end_chat_support.py @@ -0,0 +1,67 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""added end chat support + +Revision ID: 537f2b7d4a7c +Revises: 5b5f936cd9e4 +Create Date: 2025-03-04 12:07:37.679734 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '537f2b7d4a7c' +down_revision: Union[str, None] = '5b5f936cd9e4' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create the enum type first + end_chat_reason_type = postgresql.ENUM( + 'ISSUE_RESOLVED', + 'CUSTOMER_REQUEST', + 'CONFIRMATION_RECEIVED', + 'FAREWELL', + 'THANK_YOU', + 'NATURAL_CONCLUSION', + 'TASK_COMPLETED', + name='endchatreasontype' + ) + end_chat_reason_type.create(op.get_bind()) + + # Add columns + op.add_column('agents', sa.Column('ask_for_rating', sa.Boolean(), server_default='true', nullable=False)) + op.add_column('session_to_agents', sa.Column('end_chat_reason', postgresql.ENUM('ISSUE_RESOLVED', 'CUSTOMER_REQUEST', 'CONFIRMATION_RECEIVED', 'FAREWELL', 'THANK_YOU', 'NATURAL_CONCLUSION', 'TASK_COMPLETED', name='endchatreasontype'), nullable=True)) + op.add_column('session_to_agents', sa.Column('end_chat_description', sa.String(), nullable=True)) + op.drop_column('session_to_agents', 'closed_at') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('session_to_agents', sa.Column('closed_at', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True)) + op.drop_column('session_to_agents', 'end_chat_description') + op.drop_column('session_to_agents', 'end_chat_reason') + op.drop_column('agents', 'ask_for_rating') + + # Drop the enum type + op.execute('DROP TYPE endchatreasontype') diff --git a/backend/alembic/versions/5b5f936cd9e4_role_remove_name_alone_as_unique.py b/backend/alembic/versions/5b5f936cd9e4_role_remove_name_alone_as_unique.py new file mode 100644 index 0000000..dfcf9fc --- /dev/null +++ b/backend/alembic/versions/5b5f936cd9e4_role_remove_name_alone_as_unique.py @@ -0,0 +1,48 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Role remove name alone as unique + +Revision ID: 5b5f936cd9e4 +Revises: 52e6ca829bb8 +Create Date: 2025-02-08 07:36:33.283138 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '5b5f936cd9e4' +down_revision: Union[str, None] = '52e6ca829bb8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('roles_name_key', 'roles', type_='unique') + op.create_unique_constraint('uq_role_name_org', 'roles', ['name', 'organization_id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint('uq_role_name_org', 'roles', type_='unique') + op.create_unique_constraint('roles_name_key', 'roles', ['name']) + # ### end Alembic commands ### diff --git a/backend/alembic/versions/621095beb234_add_mcp_tools_tables.py b/backend/alembic/versions/621095beb234_add_mcp_tools_tables.py new file mode 100644 index 0000000..2c4c82c --- /dev/null +++ b/backend/alembic/versions/621095beb234_add_mcp_tools_tables.py @@ -0,0 +1,84 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add_mcp_tools_tables + +Revision ID: 621095beb234 +Revises: 7991b4fc00d8 +Create Date: 2025-07-27 16:45:37.520270 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '621095beb234' +down_revision: Union[str, None] = '7991b4fc00d8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + + op.create_table('mcp_tools', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('transport_type', sa.Enum('STDIO', 'SSE', 'HTTP', name='mcptransporttype'), nullable=False), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('command', sa.String(), nullable=True), + sa.Column('args', postgresql.JSON(astext_type=sa.Text()), nullable=True), + sa.Column('env_vars', postgresql.JSON(astext_type=sa.Text()), nullable=True), + sa.Column('url', sa.String(), nullable=True), + sa.Column('headers', postgresql.JSON(astext_type=sa.Text()), nullable=True), + sa.Column('timeout', sa.Integer(), nullable=True), + sa.Column('sse_read_timeout', sa.Integer(), nullable=True), + sa.Column('terminate_on_close', sa.Boolean(), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_mcp_tools_id'), 'mcp_tools', ['id'], unique=False) + + op.create_table('mcp_tool_to_agent', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('mcp_tool_id', sa.Integer(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ), + sa.ForeignKeyConstraint(['mcp_tool_id'], ['mcp_tools.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_mcp_tool_to_agent_id'), 'mcp_tool_to_agent', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_mcp_tool_to_agent_id'), table_name='mcp_tool_to_agent') + op.drop_table('mcp_tool_to_agent') + + op.drop_index(op.f('ix_mcp_tools_id'), table_name='mcp_tools') + op.drop_table('mcp_tools') + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/6885c9f36712_add_chat_style_and_widget_position_to_agent_customizations.py b/backend/alembic/versions/6885c9f36712_add_chat_style_and_widget_position_to_agent_customizations.py new file mode 100644 index 0000000..aa1bcae --- /dev/null +++ b/backend/alembic/versions/6885c9f36712_add_chat_style_and_widget_position_to_agent_customizations.py @@ -0,0 +1,70 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add chat_style and widget_position to agent_customizations + +Revision ID: 6885c9f36712 +Revises: 9ce623009955 +Create Date: 2025-01-27 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '6885c9f36712' +down_revision: Union[str, None] = '9ce623009955' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create ChatStyle enum + chatstyle_enum = sa.Enum('CHATBOT', 'ASK_ANYTHING', name='chatstyle') + chatstyle_enum.create(op.get_bind()) + + # Create WidgetPosition enum + widgetposition_enum = sa.Enum('FLOATING', 'FIXED', name='widgetposition') + widgetposition_enum.create(op.get_bind()) + + # Add columns to agent_customizations table as nullable first + op.add_column('agent_customizations', + sa.Column('chat_style', chatstyle_enum, nullable=True) + ) + op.add_column('agent_customizations', + sa.Column('widget_position', widgetposition_enum, nullable=True) + ) + + # Update existing rows with default values + op.execute("UPDATE agent_customizations SET chat_style = 'CHATBOT' WHERE chat_style IS NULL") + op.execute("UPDATE agent_customizations SET widget_position = 'FLOATING' WHERE widget_position IS NULL") + + # Now make the columns NOT NULL + op.alter_column('agent_customizations', 'chat_style', nullable=False) + op.alter_column('agent_customizations', 'widget_position', nullable=False) + + +def downgrade() -> None: + # Drop columns + op.drop_column('agent_customizations', 'widget_position') + op.drop_column('agent_customizations', 'chat_style') + + # Drop enums + sa.Enum(name='widgetposition').drop(op.get_bind()) + sa.Enum(name='chatstyle').drop(op.get_bind()) \ No newline at end of file diff --git a/backend/alembic/versions/7991b4fc00d8_added_wlecome_header_and_title.py b/backend/alembic/versions/7991b4fc00d8_added_wlecome_header_and_title.py new file mode 100644 index 0000000..850cb56 --- /dev/null +++ b/backend/alembic/versions/7991b4fc00d8_added_wlecome_header_and_title.py @@ -0,0 +1,51 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""added wlecome header and title + +Revision ID: 7991b4fc00d8 +Revises: 6885c9f36712 +Create Date: 2025-07-27 16:11:49.392307 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7991b4fc00d8' +down_revision: Union[str, None] = '6885c9f36712' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.add_column('agent_customizations', sa.Column('welcome_title', sa.String(), nullable=True)) + op.add_column('agent_customizations', sa.Column('welcome_subtitle', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('agent_customizations', 'welcome_subtitle') + op.drop_column('agent_customizations', 'welcome_title') + + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/7af9828611e7_add_oauth_state_fields_to_shopify_shops.py b/backend/alembic/versions/7af9828611e7_add_oauth_state_fields_to_shopify_shops.py new file mode 100644 index 0000000..546fea7 --- /dev/null +++ b/backend/alembic/versions/7af9828611e7_add_oauth_state_fields_to_shopify_shops.py @@ -0,0 +1,46 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add oauth_state fields to shopify_shops + +Revision ID: 7af9828611e7 +Revises: d9a4b9902f80 +Create Date: 2025-10-28 20:28:32.936719 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7af9828611e7' +down_revision: Union[str, None] = 'd9a4b9902f80' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add oauth_state and oauth_state_expiry columns to shopify_shops table + op.add_column('shopify_shops', sa.Column('oauth_state', sa.String(), nullable=True)) + op.add_column('shopify_shops', sa.Column('oauth_state_expiry', sa.DateTime(), nullable=True)) + + +def downgrade() -> None: + # Remove oauth_state and oauth_state_expiry columns from shopify_shops table + op.drop_column('shopify_shops', 'oauth_state_expiry') + op.drop_column('shopify_shops', 'oauth_state') diff --git a/backend/alembic/versions/7fd0447b3d45_workflow_tables_added.py b/backend/alembic/versions/7fd0447b3d45_workflow_tables_added.py new file mode 100644 index 0000000..6ff4dad --- /dev/null +++ b/backend/alembic/versions/7fd0447b3d45_workflow_tables_added.py @@ -0,0 +1,131 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Workflow tables added + +Revision ID: 7fd0447b3d45 +Revises: e252efacd083 +Create Date: 2025-07-09 11:12:34.104222 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '7fd0447b3d45' +down_revision: Union[str, None] = 'e252efacd083' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.create_table('workflows', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('status', sa.Enum('DRAFT', 'PUBLISHED', 'ARCHIVED', name='workflowstatus'), nullable=True), + sa.Column('version', sa.Integer(), nullable=True), + sa.Column('is_template', sa.Boolean(), nullable=True), + sa.Column('default_language', sa.String(length=10), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.UUID(), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=True), + sa.Column('agent_id', sa.UUID(), nullable=True), + sa.Column('canvas_data', sa.JSON(), nullable=True), + sa.Column('settings', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id') + ) + + + + op.create_table('workflow_nodes', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('workflow_id', sa.UUID(), nullable=False), + sa.Column('node_type', sa.Enum('MESSAGE', 'LLM', 'CONDITION', 'FORM', 'ACTION', 'HUMAN_TRANSFER', 'WAIT', 'END', name='nodetype'), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('position_x', sa.Float(), nullable=True), + sa.Column('position_y', sa.Float(), nullable=True), + sa.Column('config', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('message_text', sa.Text(), nullable=True), + sa.Column('system_prompt', sa.Text(), nullable=True), + sa.Column('temperature', sa.Float(), nullable=True), + sa.Column('model_id', sa.Integer(), nullable=True), + sa.Column('form_fields', sa.JSON(), nullable=True), + sa.Column('condition_expression', sa.Text(), nullable=True), + sa.Column('action_type', sa.String(), nullable=True), + sa.Column('action_config', sa.JSON(), nullable=True), + sa.Column('transfer_rules', sa.JSON(), nullable=True), + sa.Column('wait_duration', sa.Integer(), nullable=True), + sa.Column('wait_until_condition', sa.Text(), nullable=True), + sa.ForeignKeyConstraint(['model_id'], ['ai_configs.id'], ), + sa.ForeignKeyConstraint(['workflow_id'], ['workflows.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('workflow_connections', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('workflow_id', sa.UUID(), nullable=False), + sa.Column('source_node_id', sa.UUID(), nullable=False), + sa.Column('target_node_id', sa.UUID(), nullable=False), + sa.Column('label', sa.String(length=100), nullable=True), + sa.Column('condition', sa.Text(), nullable=True), + sa.Column('priority', sa.Integer(), nullable=True), + sa.Column('connection_metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['source_node_id'], ['workflow_nodes.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['target_node_id'], ['workflow_nodes.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['workflow_id'], ['workflows.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.add_column('agents', sa.Column('use_workflow', sa.Boolean(), nullable=True)) + op.add_column('agents', sa.Column('active_workflow_id', sa.UUID(), nullable=True)) + op.create_foreign_key(None, 'agents', 'workflows', ['active_workflow_id'], ['id']) + op.add_column('session_to_agents', sa.Column('workflow_id', sa.UUID(), nullable=True)) + op.add_column('session_to_agents', sa.Column('current_node_id', sa.UUID(), nullable=True)) + op.add_column('session_to_agents', sa.Column('workflow_state', sa.JSON(), nullable=True)) + op.create_foreign_key(None, 'session_to_agents', 'workflow_nodes', ['current_node_id'], ['id'], ondelete='SET NULL') + op.create_foreign_key(None, 'session_to_agents', 'workflows', ['workflow_id'], ['id'], ondelete='SET NULL') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'session_to_agents', type_='foreignkey') + op.drop_constraint(None, 'session_to_agents', type_='foreignkey') + op.drop_column('session_to_agents', 'workflow_state') + op.drop_column('session_to_agents', 'current_node_id') + op.drop_column('session_to_agents', 'workflow_id') + op.drop_constraint(None, 'agents', type_='foreignkey') + op.drop_column('agents', 'active_workflow_id') + op.drop_column('agents', 'use_workflow') + op.drop_table('workflow_connections') + op.drop_table('workflow_nodes') + + op.drop_table('workflows') + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/85364d188f15_merge_heads.py b/backend/alembic/versions/85364d188f15_merge_heads.py new file mode 100644 index 0000000..cd86f6d --- /dev/null +++ b/backend/alembic/versions/85364d188f15_merge_heads.py @@ -0,0 +1,42 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""merge heads + +Revision ID: 85364d188f15 +Revises: 0cf5fa1db0cb, 89a4f4898aa4 +Create Date: 2025-03-22 20:43:28.206404 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '85364d188f15' +down_revision: Union[str, None] = ('0cf5fa1db0cb', '89a4f4898aa4') +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/backend/alembic/versions/89a4f4898aa4_change_datetime_to_timestamp_with_.py b/backend/alembic/versions/89a4f4898aa4_change_datetime_to_timestamp_with_.py new file mode 100644 index 0000000..38b1970 --- /dev/null +++ b/backend/alembic/versions/89a4f4898aa4_change_datetime_to_timestamp_with_.py @@ -0,0 +1,64 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""change datetime to timestamp with timezone + +Revision ID: 89a4f4898aa4 +Revises: 91a148726029 +Create Date: 2024-03-21 19:18:36.159906 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '89a4f4898aa4' +down_revision: Union[str, None] = '91a148726029' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Change columns from DateTime to TIMESTAMP WITH TIME ZONE in chat_history table + op.alter_column('chat_history', 'created_at', + existing_type=sa.DateTime(), + type_=sa.TIMESTAMP(timezone=True), + existing_nullable=True, + existing_server_default=sa.text('now()')) + + op.alter_column('chat_history', 'updated_at', + existing_type=sa.DateTime(), + type_=sa.TIMESTAMP(timezone=True), + existing_nullable=True, + existing_server_default=sa.text('now()')) + + +def downgrade() -> None: + # Change columns back to DateTime in chat_history table + op.alter_column('chat_history', 'created_at', + existing_type=sa.TIMESTAMP(timezone=True), + type_=sa.DateTime(), + existing_nullable=True, + existing_server_default=sa.text('now()')) + + op.alter_column('chat_history', 'updated_at', + existing_type=sa.TIMESTAMP(timezone=True), + type_=sa.DateTime(), + existing_nullable=True, + existing_server_default=sa.text('now()')) diff --git a/backend/alembic/versions/910bfe03518b_make_knowledge_queue_user_id_nullable.py b/backend/alembic/versions/910bfe03518b_make_knowledge_queue_user_id_nullable.py new file mode 100644 index 0000000..2be3f69 --- /dev/null +++ b/backend/alembic/versions/910bfe03518b_make_knowledge_queue_user_id_nullable.py @@ -0,0 +1,48 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""make_knowledge_queue_user_id_nullable + +Revision ID: 910bfe03518b +Revises: 0cf34ff05aee +Create Date: 2025-11-04 17:18:35.904635 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '910bfe03518b' +down_revision: Union[str, None] = '0cf34ff05aee' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Make user_id nullable in knowledge_queue table + op.alter_column('knowledge_queue', 'user_id', + existing_type=sa.UUID(), + nullable=True) + + +def downgrade() -> None: + # Revert user_id to not nullable + op.alter_column('knowledge_queue', 'user_id', + existing_type=sa.UUID(), + nullable=False) diff --git a/backend/alembic/versions/91a148726029_add_extra_data_to_agent_sessions.py b/backend/alembic/versions/91a148726029_add_extra_data_to_agent_sessions.py new file mode 100644 index 0000000..04942a2 --- /dev/null +++ b/backend/alembic/versions/91a148726029_add_extra_data_to_agent_sessions.py @@ -0,0 +1,87 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add extra_data to agent_sessions + +Revision ID: 91a148726029 +Revises: f3dad24474e0 +Create Date: 2024-03-21 10:57:23.123456 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '91a148726029' +down_revision: Union[str, None] = 'f3dad24474e0' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Check if 'ai' schema and agent_sessions table exist before attempting to modify + conn = op.get_bind() + + # Check if schema exists + schema_result = conn.execute(sa.text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'ai'")) + schema_exists = schema_result.fetchone() is not None + + if not schema_exists: + print("Schema 'ai' does not exist, skipping migration") + return + + # Check if table exists + table_result = conn.execute(sa.text( + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'ai' AND table_name = 'agent_sessions'" + )) + table_exists = table_result.fetchone() is not None + + if table_exists: + # Add extra_data column to ai.agent_sessions table + op.add_column('agent_sessions', + sa.Column('extra_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + schema='ai' + ) + else: + print("Table 'ai.agent_sessions' does not exist, skipping migration") + + +def downgrade() -> None: + # Check if 'ai' schema and agent_sessions table exist before attempting to modify + conn = op.get_bind() + + # Check if schema exists + schema_result = conn.execute(sa.text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'ai'")) + schema_exists = schema_result.fetchone() is not None + + if not schema_exists: + print("Schema 'ai' does not exist, skipping migration rollback") + return + + # Check if table exists + table_result = conn.execute(sa.text( + "SELECT table_name FROM information_schema.tables WHERE table_schema = 'ai' AND table_name = 'agent_sessions'" + )) + table_exists = table_result.fetchone() is not None + + if table_exists: + # Drop extra_data column from ai.agent_sessions table + op.drop_column('agent_sessions', 'extra_data', schema='ai') + else: + print("Table 'ai.agent_sessions' does not exist, skipping migration rollback") diff --git a/backend/alembic/versions/9a01e6b653b2_add_agent_shopify_config_with_shop_.py b/backend/alembic/versions/9a01e6b653b2_add_agent_shopify_config_with_shop_.py new file mode 100644 index 0000000..7fc3f05 --- /dev/null +++ b/backend/alembic/versions/9a01e6b653b2_add_agent_shopify_config_with_shop_.py @@ -0,0 +1,61 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add agent shopify config with shop relation + +Revision ID: 9a01e6b653b2 +Revises: 1493b8279a2f +Create Date: 2025-04-11 13:43:05.945125 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '9a01e6b653b2' +down_revision: Union[str, None] = '1493b8279a2f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.create_table('agent_shopify_configs', + sa.Column('id', sa.String(), nullable=False), + sa.Column('agent_id', sa.String(), nullable=False), + sa.Column('shop_id', sa.String(), nullable=True), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['shop_id'], ['shopify_shops.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_agent_shopify_configs_agent_id'), 'agent_shopify_configs', ['agent_id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.drop_index(op.f('ix_agent_shopify_configs_agent_id'), table_name='agent_shopify_configs') + op.drop_table('agent_shopify_configs') + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/9ce623009955_added_new_node_type_user_input.py 14-13-41-620.py b/backend/alembic/versions/9ce623009955_added_new_node_type_user_input.py 14-13-41-620.py new file mode 100644 index 0000000..6a57a2b --- /dev/null +++ b/backend/alembic/versions/9ce623009955_added_new_node_type_user_input.py 14-13-41-620.py @@ -0,0 +1,60 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""added new node type user input + +Revision ID: 9ce623009955 +Revises: 2fd826882d08 +Create Date: 2025-07-25 13:59:26.571283 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '9ce623009955' +down_revision: Union[str, None] = '2fd826882d08' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.execute("ALTER TYPE nodetype ADD VALUE IF NOT EXISTS 'USER_INPUT'") + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + # Create a new enum without the landing_page value + op.execute(""" + CREATE TYPE nodetype_new AS ENUM ( + 'MESSAGE','LLM','CONDITION','FORM','ACTION','HUMAN_TRANSFER','WAIT','END','LANDING_PAGE' + ) + """) + + # Update the column type to use the new enum + op.execute("ALTER TABLE workflow_nodes ALTER COLUMN node_type TYPE nodetype_new USING node_type::text::nodetype_new") + + # Drop the old enum + op.execute("DROP TYPE nodetype") + + # Rename the new enum to the original name + op.execute("ALTER TYPE nodetype_new RENAME TO nodetype") + # ### end Alembic commands ### diff --git a/backend/alembic/versions/a1b2c3d4e5f6_add_chat_initiation_messages.py b/backend/alembic/versions/a1b2c3d4e5f6_add_chat_initiation_messages.py new file mode 100644 index 0000000..deb593c --- /dev/null +++ b/backend/alembic/versions/a1b2c3d4e5f6_add_chat_initiation_messages.py @@ -0,0 +1,47 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add chat initiation messages + +Revision ID: a1b2c3d4e5f6 +Revises: 7af9828611e7 +Create Date: 2025-10-29 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'a1b2c3d4e5f6' +down_revision: Union[str, None] = '7af9828611e7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('agent_customizations', sa.Column('chat_initiation_messages', postgresql.JSON(astext_type=sa.Text()), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('agent_customizations', 'chat_initiation_messages') + # ### end Alembic commands ### + diff --git a/backend/alembic/versions/a1dbf557bbb0_merge_ai_ticketing_agent_queue_heads.py b/backend/alembic/versions/a1dbf557bbb0_merge_ai_ticketing_agent_queue_heads.py new file mode 100644 index 0000000..b718ab6 --- /dev/null +++ b/backend/alembic/versions/a1dbf557bbb0_merge_ai_ticketing_agent_queue_heads.py @@ -0,0 +1,26 @@ +"""merge ai-ticketing + agent-queue heads + +Revision ID: a1dbf557bbb0 +Revises: add_agent_tkt_toggle_001, b7c4e91a2d38 +Create Date: 2026-07-20 11:02:56.904328 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a1dbf557bbb0' +down_revision: Union[str, None] = ('add_agent_tkt_toggle_001', 'b7c4e91a2d38') +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/backend/alembic/versions/a8efc6b31aa8_merge_crm_and_chat_session_idx_heads.py b/backend/alembic/versions/a8efc6b31aa8_merge_crm_and_chat_session_idx_heads.py new file mode 100644 index 0000000..4b4db32 --- /dev/null +++ b/backend/alembic/versions/a8efc6b31aa8_merge_crm_and_chat_session_idx_heads.py @@ -0,0 +1,42 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""merge crm and chat-session-idx heads + +Revision ID: a8efc6b31aa8 +Revises: add_chat_session_idx_001, add_crm_tables_001 +Create Date: 2026-07-28 18:55:32.388594 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'a8efc6b31aa8' +down_revision: Union[str, None] = ('add_chat_session_idx_001', 'add_crm_tables_001') +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/backend/alembic/versions/abc123456789_add_file_attachments_table.py b/backend/alembic/versions/abc123456789_add_file_attachments_table.py new file mode 100644 index 0000000..6d96a01 --- /dev/null +++ b/backend/alembic/versions/abc123456789_add_file_attachments_table.py @@ -0,0 +1,63 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add_file_attachments_table + +Revision ID: abc123456789 +Revises: 0cf34ff05aee +Create Date: 2025-10-30 00:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'abc123456789' +down_revision: Union[str, None] = '0cf34ff05aee' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create file_attachments table + op.create_table('file_attachments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('file_url', sa.String(), nullable=False), + sa.Column('filename', sa.String(), nullable=False), + sa.Column('content_type', sa.String(), nullable=False), + sa.Column('file_size', sa.BigInteger(), nullable=False), + sa.Column('chat_history_id', sa.Integer(), nullable=True), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('uploaded_by_user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('uploaded_by_customer_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['chat_history_id'], ['chat_history.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['uploaded_by_user_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['uploaded_by_customer_id'], ['customers.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_file_attachments_id'), 'file_attachments', ['id'], unique=False) + + +def downgrade() -> None: + # Drop the table and index + op.drop_index(op.f('ix_file_attachments_id'), table_name='file_attachments') + op.drop_table('file_attachments') diff --git a/backend/alembic/versions/add_agent_ai_replies_toggle_001.py b/backend/alembic/versions/add_agent_ai_replies_toggle_001.py new file mode 100644 index 0000000..9fdb61a --- /dev/null +++ b/backend/alembic/versions/add_agent_ai_replies_toggle_001.py @@ -0,0 +1,45 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add agents.ai_replies_enabled (human-only agents) + +Revision ID: add_agent_ai_replies_001 +Revises: purge_mangled_crawl_pages_001 +Create Date: 2026-08-02 + +Off means the AI never answers on this agent: every chat is queued for the team +from the customer's first message. Defaults on, so existing agents keep +answering exactly as before. +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'add_agent_ai_replies_001' +down_revision = 'purge_mangled_crawl_pages_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + 'agents', + sa.Column('ai_replies_enabled', sa.Boolean(), server_default=sa.text('true'), nullable=False), + ) + + +def downgrade() -> None: + op.drop_column('agents', 'ai_replies_enabled') diff --git a/backend/alembic/versions/add_agent_guardrail_prompt_001.py b/backend/alembic/versions/add_agent_guardrail_prompt_001.py new file mode 100644 index 0000000..d743830 --- /dev/null +++ b/backend/alembic/versions/add_agent_guardrail_prompt_001.py @@ -0,0 +1,51 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add agents.guardrail_prompt and agents.guardrail_enabled + +Revision ID: add_agent_guardrail_prompt_001 +Revises: add_guardrails_layer_001 +Create Date: 2026-07-31 + +Makes the agent's scope rule tenant-editable. A code-owned topic list is a +guess about what a business is NOT — the shipped wording refused maths problems +for a maths tutor and algorithm questions for a coding bootcamp, i.e. their core +use case. guardrail_prompt NULL means "use the platform default", so the default +wording can still be improved centrally without rewriting existing rows. +guardrail_enabled defaults true so every existing agent keeps its protection. +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'add_agent_guardrail_prompt_001' +down_revision = 'add_guardrails_layer_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column('agents', sa.Column('guardrail_prompt', sa.Text(), nullable=True)) + op.add_column( + 'agents', + sa.Column('guardrail_enabled', sa.Boolean(), + server_default=sa.text('true'), nullable=False), + ) + + +def downgrade() -> None: + op.drop_column('agents', 'guardrail_enabled') + op.drop_column('agents', 'guardrail_prompt') diff --git a/backend/alembic/versions/add_agent_ticketing_toggle_001.py b/backend/alembic/versions/add_agent_ticketing_toggle_001.py new file mode 100644 index 0000000..f91695b --- /dev/null +++ b/backend/alembic/versions/add_agent_ticketing_toggle_001.py @@ -0,0 +1,45 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add agents.ticketing_enabled (per-agent native ticketing toggle) + +Revision ID: add_agent_tkt_toggle_001 +Revises: add_tkt_db_ssh_001 +Create Date: 2026-07-17 + +Per-agent switch for native AI ticketing. Defaults on so paid orgs get +ticketing on every agent out of the box; the org's plan gate still applies on +top of it. +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'add_agent_tkt_toggle_001' +down_revision = 'add_tkt_db_ssh_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + 'agents', + sa.Column('ticketing_enabled', sa.Boolean(), server_default=sa.text('true'), nullable=False), + ) + + +def downgrade() -> None: + op.drop_column('agents', 'ticketing_enabled') diff --git a/backend/alembic/versions/add_ai_disclaimer_toggle_001.py b/backend/alembic/versions/add_ai_disclaimer_toggle_001.py new file mode 100644 index 0000000..7c8b11a --- /dev/null +++ b/backend/alembic/versions/add_ai_disclaimer_toggle_001.py @@ -0,0 +1,46 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add agent_customizations.show_ai_disclaimer + +Revision ID: add_ai_disclaimer_001 +Revises: add_agent_guardrail_prompt_001 +Create Date: 2026-07-31 + +Controls the "AI can make mistakes" line in the widget footer. Defaults true so +existing widgets disclose by default; operators who show the disclosure +elsewhere on their site can switch it off. +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'add_ai_disclaimer_001' +down_revision = 'add_agent_guardrail_prompt_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + 'agent_customizations', + sa.Column('show_ai_disclaimer', sa.Boolean(), + server_default=sa.text('true'), nullable=False), + ) + + +def downgrade() -> None: + op.drop_column('agent_customizations', 'show_ai_disclaimer') diff --git a/backend/alembic/versions/add_allow_attachments_001.py b/backend/alembic/versions/add_allow_attachments_001.py new file mode 100644 index 0000000..398d50f --- /dev/null +++ b/backend/alembic/versions/add_allow_attachments_001.py @@ -0,0 +1,44 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add allow_attachments to agents table + +Revision ID: add_allow_attachments_001 +Revises: abc123456789, 910bfe03518b +Create Date: 2025-11-08 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'add_allow_attachments_001' +down_revision: Union[str, Sequence[str]] = ('abc123456789', '910bfe03518b') +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('agents', + sa.Column('allow_attachments', sa.Boolean(), nullable=False, server_default='false') + ) + + +def downgrade() -> None: + op.drop_column('agents', 'allow_attachments') diff --git a/backend/alembic/versions/add_allow_new_chat_001.py b/backend/alembic/versions/add_allow_new_chat_001.py new file mode 100644 index 0000000..da75b0d --- /dev/null +++ b/backend/alembic/versions/add_allow_new_chat_001.py @@ -0,0 +1,57 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add agent_customizations.allow_new_chat + +Revision ID: add_allow_new_chat_001 +Revises: repair_agent_role_seed_001 +Create Date: 2026-08-09 + +Shows a "New chat" control in the widget so a visitor can abandon the current +conversation and start a clean one. Defaults false: starting a new chat closes +the session, which would cut off a human agent mid-handover, so operators opt in. +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'add_allow_new_chat_001' +down_revision = 'repair_agent_role_seed_001' +branch_labels = None +depends_on = None + + +def _has_column() -> bool: + """Development databases are shared between feature branches, so the column can + already exist by the time this runs. Adding it twice would abort the upgrade.""" + inspector = sa.inspect(op.get_bind()) + return any(c['name'] == 'allow_new_chat' + for c in inspector.get_columns('agent_customizations')) + + +def upgrade() -> None: + if _has_column(): + return + op.add_column( + 'agent_customizations', + sa.Column('allow_new_chat', sa.Boolean(), + server_default=sa.text('false'), nullable=False), + ) + + +def downgrade() -> None: + if _has_column(): + op.drop_column('agent_customizations', 'allow_new_chat') diff --git a/backend/alembic/versions/add_allowed_attachment_types_001.py b/backend/alembic/versions/add_allowed_attachment_types_001.py new file mode 100644 index 0000000..f365ad6 --- /dev/null +++ b/backend/alembic/versions/add_allowed_attachment_types_001.py @@ -0,0 +1,45 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add allowed_attachment_types to agents table + +Revision ID: add_allowed_attachment_types_001 +Revises: add_slack_integration_001 +Create Date: 2024-12-17 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'add_allowed_attachment_types_001' +down_revision: Union[str, Sequence[str]] = 'add_slack_integration_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add allowed_attachment_types column as JSON + op.add_column('agents', + sa.Column('allowed_attachment_types', sa.JSON(), nullable=True) + ) + + +def downgrade() -> None: + op.drop_column('agents', 'allowed_attachment_types') diff --git a/backend/alembic/versions/add_channel_tables_001.py b/backend/alembic/versions/add_channel_tables_001.py new file mode 100644 index 0000000..326f733 --- /dev/null +++ b/backend/alembic/versions/add_channel_tables_001.py @@ -0,0 +1,126 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add omni-channel tables and session channel column + +Revision ID: add_channel_tables_001 +Revises: e5f6a7b8c9d0 +Create Date: 2026-07-10 10:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'add_channel_tables_001' +down_revision: Union[str, None] = 'e5f6a7b8c9d0' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'channel_accounts', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('channel_type', sa.String(), nullable=False), + sa.Column('external_account_id', sa.String(), nullable=False), + sa.Column('display_name', sa.String(), nullable=True), + sa.Column('encrypted_credentials', sa.Text(), nullable=False), + sa.Column('webhook_secret', sa.String(), nullable=False), + sa.Column('settings', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.true()), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('channel_type', 'external_account_id', name='uq_channel_account_external'), + ) + op.create_index('ix_channel_accounts_organization_id', 'channel_accounts', ['organization_id']) + op.create_index('ix_channel_accounts_channel_type', 'channel_accounts', ['channel_type']) + + op.create_table( + 'channel_conversations', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('channel_account_id', sa.UUID(), nullable=False), + sa.Column('channel_type', sa.String(), nullable=False), + sa.Column('external_conversation_id', sa.String(), nullable=False), + sa.Column('external_user_id', sa.String(), nullable=False), + sa.Column('session_id', sa.UUID(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=True), + sa.Column('customer_id', sa.UUID(), nullable=True), + sa.Column('last_inbound_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('extra', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['channel_account_id'], ['channel_accounts.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint( + 'channel_account_id', 'external_conversation_id', 'session_id', + name='uq_channel_conversation_session' + ), + ) + op.create_index('ix_channel_conversations_channel_account_id', 'channel_conversations', ['channel_account_id']) + op.create_index('ix_channel_conversations_external_conversation_id', 'channel_conversations', ['external_conversation_id']) + op.create_index('ix_channel_conversations_session_id', 'channel_conversations', ['session_id']) + + op.create_table( + 'agent_channel_configs', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=False), + sa.Column('channel_account_id', sa.UUID(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default=sa.true()), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['channel_account_id'], ['channel_accounts.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('channel_account_id', name='uq_agent_channel_config_account'), + ) + op.create_index('ix_agent_channel_configs_agent_id', 'agent_channel_configs', ['agent_id']) + op.create_index('ix_agent_channel_configs_channel_account_id', 'agent_channel_configs', ['channel_account_id']) + + op.add_column( + 'session_to_agents', + sa.Column('channel', sa.String(), nullable=False, server_default='web') + ) + op.create_index('ix_session_to_agents_channel', 'session_to_agents', ['channel']) + + +def downgrade() -> None: + op.drop_index('ix_session_to_agents_channel', table_name='session_to_agents') + op.drop_column('session_to_agents', 'channel') + + op.drop_index('ix_agent_channel_configs_channel_account_id', table_name='agent_channel_configs') + op.drop_index('ix_agent_channel_configs_agent_id', table_name='agent_channel_configs') + op.drop_table('agent_channel_configs') + + op.drop_index('ix_channel_conversations_session_id', table_name='channel_conversations') + op.drop_index('ix_channel_conversations_external_conversation_id', table_name='channel_conversations') + op.drop_index('ix_channel_conversations_channel_account_id', table_name='channel_conversations') + op.drop_table('channel_conversations') + + op.drop_index('ix_channel_accounts_channel_type', table_name='channel_accounts') + op.drop_index('ix_channel_accounts_organization_id', table_name='channel_accounts') + op.drop_table('channel_accounts') diff --git a/backend/alembic/versions/add_chat_session_idx_001.py b/backend/alembic/versions/add_chat_session_idx_001.py new file mode 100644 index 0000000..ff7270d --- /dev/null +++ b/backend/alembic/versions/add_chat_session_idx_001.py @@ -0,0 +1,50 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""index chat_history on (session_id, created_at) + +Revision ID: add_chat_session_idx_001 +Revises: hc_img_paths_001 +Create Date: 2026-07-22 + +chat_history had no index on session_id — only the primary key — so every lookup of +a conversation's messages was a sequential scan. The columns match the ordering +every reader now uses, (created_at, id), so the inbox previews and +get_session_history are both answered from the index alone. +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'add_chat_session_idx_001' +down_revision = 'hc_img_paths_001' +branch_labels = None +depends_on = None + +INDEX_NAME = 'ix_chat_history_session_created' + + +def upgrade() -> None: + op.create_index( + INDEX_NAME, + 'chat_history', + ['session_id', sa.text('created_at DESC'), sa.text('id DESC')], + unique=False, + ) + + +def downgrade() -> None: + op.drop_index(INDEX_NAME, table_name='chat_history') diff --git a/backend/alembic/versions/add_crm_customer_syncs_001.py b/backend/alembic/versions/add_crm_customer_syncs_001.py new file mode 100644 index 0000000..b4cc775 --- /dev/null +++ b/backend/alembic/versions/add_crm_customer_syncs_001.py @@ -0,0 +1,61 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add crm_customer_syncs (per-person CRM link) + +Revision ID: add_crm_customer_syncs_001 +Revises: a8efc6b31aa8 +Create Date: 2026-07-28 20:40:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision: str = 'add_crm_customer_syncs_001' +down_revision: Union[str, None] = 'a8efc6b31aa8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'crm_customer_syncs', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('customer_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('provider', sa.String(length=32), nullable=False), + sa.Column('contact_id', sa.String(), nullable=True), + sa.Column('secondary_id', sa.String(), nullable=True), + sa.Column('record_url', sa.String(), nullable=True), + sa.Column('synced_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('customer_id', 'provider', name='uq_crm_customer_sync_customer_provider'), + ) + op.create_index('ix_crm_customer_syncs_organization_id', 'crm_customer_syncs', ['organization_id']) + op.create_index('ix_crm_customer_syncs_customer_id', 'crm_customer_syncs', ['customer_id']) + + +def downgrade() -> None: + op.drop_index('ix_crm_customer_syncs_customer_id', table_name='crm_customer_syncs') + op.drop_index('ix_crm_customer_syncs_organization_id', table_name='crm_customer_syncs') + op.drop_table('crm_customer_syncs') diff --git a/backend/alembic/versions/add_crm_tables_001_add_crm_tables.py b/backend/alembic/versions/add_crm_tables_001_add_crm_tables.py new file mode 100644 index 0000000..b287cf9 --- /dev/null +++ b/backend/alembic/versions/add_crm_tables_001_add_crm_tables.py @@ -0,0 +1,97 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add crm_connections and crm_sync_jobs (CRM lead push) + +Revision ID: add_crm_tables_001 +Revises: crm_sync_varchar_001 +Create Date: 2026-07-22 13:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision: str = 'add_crm_tables_001' +down_revision: Union[str, None] = 'crm_sync_varchar_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'crm_connections', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('provider', sa.String(length=32), nullable=False), + sa.Column('external_account_id', sa.String(), nullable=False), + sa.Column('display_name', sa.String(), nullable=True), + sa.Column('encrypted_credentials', sa.Text(), nullable=False), + sa.Column('status', sa.String(length=16), nullable=False, server_default='active'), + sa.Column('last_error', sa.Text(), nullable=True), + sa.Column('access_token_expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('refresh_token_expires_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_refreshed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('connected_by_user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['connected_by_user_id'], ['users.id']), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('organization_id', 'provider', name='uq_crm_connection_org_provider'), + sa.UniqueConstraint('provider', 'external_account_id', name='uq_crm_connection_external'), + ) + op.create_index('ix_crm_connections_organization_id', 'crm_connections', ['organization_id']) + op.create_index('ix_crm_connections_provider', 'crm_connections', ['provider']) + + op.create_table( + 'crm_sync_jobs', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('lead_response_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('provider', sa.String(length=32), nullable=False), + sa.Column('connection_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('status', sa.String(length=16), nullable=False, server_default='pending'), + sa.Column('attempts', sa.Integer(), nullable=False, server_default='0'), + sa.Column('next_attempt_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.text('now()')), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_error', sa.Text(), nullable=True), + sa.Column('result', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['lead_response_id'], ['lead_capture_responses.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['connection_id'], ['crm_connections.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('lead_response_id', 'provider', name='uq_crm_sync_job_lead_provider'), + ) + op.create_index('ix_crm_sync_jobs_organization_id', 'crm_sync_jobs', ['organization_id']) + op.create_index('ix_crm_sync_jobs_claim', 'crm_sync_jobs', ['status', 'next_attempt_at']) + + +def downgrade() -> None: + op.drop_index('ix_crm_sync_jobs_claim', table_name='crm_sync_jobs') + op.drop_index('ix_crm_sync_jobs_organization_id', table_name='crm_sync_jobs') + op.drop_table('crm_sync_jobs') + op.drop_index('ix_crm_connections_provider', table_name='crm_connections') + op.drop_index('ix_crm_connections_organization_id', table_name='crm_connections') + op.drop_table('crm_connections') diff --git a/backend/alembic/versions/add_customer_phone_001_add_customer_phone.py b/backend/alembic/versions/add_customer_phone_001_add_customer_phone.py new file mode 100644 index 0000000..ea9328f --- /dev/null +++ b/backend/alembic/versions/add_customer_phone_001_add_customer_phone.py @@ -0,0 +1,56 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +customers.phone: a second identity key beside email, normalized E.164 with a +leading '+'. Phone-bearing channels (WhatsApp, SMS, Telegram share-contact) +resolve people by it, so it must be unique per org — but only where present, +hence a partial unique index rather than a UniqueConstraint (rows without a +phone are the overwhelming majority and must never collide). The index is +declared in the model's __table_args__ as well as created here — so +autogenerate never sees it as unknown and drops it, and so the sqlite test +schema enforces the same uniqueness Postgres does. Keep the two in step. + +No backfill: WhatsApp/SMS aren't live yet, so no rows carry a phone anywhere +to backfill from. + +Revision ID: add_customer_phone_001 +Revises: add_hc_faq_feedback_001 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "add_customer_phone_001" +down_revision = "add_hc_faq_feedback_001" +branch_labels = None +depends_on = None + +INDEX = "uix_customer_phone_org" + + +def upgrade() -> None: + op.add_column("customers", sa.Column("phone", sa.String(), nullable=True)) + op.create_index( + INDEX, + "customers", + ["organization_id", "phone"], + unique=True, + postgresql_where=sa.text("phone IS NOT NULL"), + ) + + +def downgrade() -> None: + op.drop_index(INDEX, table_name="customers") + op.drop_column("customers", "phone") diff --git a/backend/alembic/versions/add_db_connector_row_scope.py b/backend/alembic/versions/add_db_connector_row_scope.py new file mode 100644 index 0000000..eec6740 --- /dev/null +++ b/backend/alembic/versions/add_db_connector_row_scope.py @@ -0,0 +1,40 @@ +"""Row-level scoping for the ticket DB connector + +Revision ID: add_tkt_row_scope_001 +Revises: a1dbf557bbb0 +Create Date: 2026-07-20 + +Adds the per-table customer-column map that restricts a connector's queries to +the ticket customer's own rows, and which customer value fills the predicate. +Existing connectors get NULL/'email' — no scoping until an org configures it, +so behaviour is unchanged on upgrade. +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSON + +revision = "add_tkt_row_scope_001" +down_revision = "a1dbf557bbb0" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "ticket_db_connectors", + sa.Column("row_scope", JSON(), nullable=True), + ) + op.add_column( + "ticket_db_connectors", + sa.Column( + "row_scope_key", + sa.String(length=20), + nullable=False, + server_default="email", + ), + ) + + +def downgrade() -> None: + op.drop_column("ticket_db_connectors", "row_scope_key") + op.drop_column("ticket_db_connectors", "row_scope") diff --git a/backend/alembic/versions/add_faq_seo_fields.py b/backend/alembic/versions/add_faq_seo_fields.py new file mode 100644 index 0000000..948c7f9 --- /dev/null +++ b/backend/alembic/versions/add_faq_seo_fields.py @@ -0,0 +1,27 @@ +"""Per-article SEO overrides on FAQs + +Revision ID: add_faq_seo_001 +Revises: rel_hc_uploads_001 +Create Date: 2026-07-25 + +Adds nullable meta_title / meta_description to faqs so an org can override the +public article page's and meta description instead of always deriving +them from the question and answer excerpt. NULL keeps today's derived behaviour. +""" +from alembic import op +import sqlalchemy as sa + +revision = "add_faq_seo_001" +down_revision = "rel_hc_uploads_001" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("faqs", sa.Column("meta_title", sa.String(length=120), nullable=True)) + op.add_column("faqs", sa.Column("meta_description", sa.String(length=300), nullable=True)) + + +def downgrade() -> None: + op.drop_column("faqs", "meta_description") + op.drop_column("faqs", "meta_title") diff --git a/backend/alembic/versions/add_guardrails_layer_001.py b/backend/alembic/versions/add_guardrails_layer_001.py new file mode 100644 index 0000000..b56d589 --- /dev/null +++ b/backend/alembic/versions/add_guardrails_layer_001.py @@ -0,0 +1,77 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add guardrail_events table and agents.topic_scope + +Revision ID: add_guardrails_layer_001 +Revises: add_crm_customer_syncs_001 +Create Date: 2026-07-30 + +Global guardrail layer for customer-facing agents. guardrail_events records +every trigger (injection signals, blocks, prompt-leak replacements, scope +refusals) so abuse is measurable and false positives reviewable before any +rule is promoted from counting to blocking. agents.topic_scope is the optional +per-agent remit override for the code-owned policy's topic-scope line; null +falls back to the agent description or the organization name/domain. +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'add_guardrails_layer_001' +down_revision = 'add_crm_customer_syncs_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'guardrail_events', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), + sa.ForeignKey('organizations.id', ondelete='CASCADE'), nullable=True), + sa.Column('agent_id', postgresql.UUID(as_uuid=True), + sa.ForeignKey('agents.id', ondelete='SET NULL'), nullable=True), + # No FK: a help-center /ask event has no chat session. + sa.Column('session_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('surface', sa.String(20), nullable=False), + sa.Column('layer', sa.String(16), nullable=False), + sa.Column('rule', sa.String(64), nullable=False), + sa.Column('action', sa.String(16), nullable=False), + sa.Column('score', sa.Float(), nullable=True), + # Rule ids / short match tokens only — never raw visitor text. + sa.Column('matched', sa.JSON(), nullable=True), + sa.Column('char_len', sa.Integer(), nullable=True), + # Encrypted at rest (EncryptedText decorates a Text column). + sa.Column('excerpt', sa.Text(), nullable=True), + sa.Column('reviewed', sa.Boolean(), server_default=sa.text('false'), nullable=False), + sa.Column('false_positive', sa.Boolean(), server_default=sa.text('false'), nullable=False), + sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.func.now()), + ) + op.create_index('ix_guardrail_events_org_created', 'guardrail_events', + ['organization_id', 'created_at']) + op.create_index('ix_guardrail_events_rule_created', 'guardrail_events', + ['rule', 'created_at']) + + op.add_column('agents', sa.Column('topic_scope', sa.Text(), nullable=True)) + + +def downgrade() -> None: + op.drop_column('agents', 'topic_scope') + op.drop_index('ix_guardrail_events_rule_created', table_name='guardrail_events') + op.drop_index('ix_guardrail_events_org_created', table_name='guardrail_events') + op.drop_table('guardrail_events') diff --git a/backend/alembic/versions/add_hc_chat_widget_001_add_chat_widget_enabled.py b/backend/alembic/versions/add_hc_chat_widget_001_add_chat_widget_enabled.py new file mode 100644 index 0000000..b29d472 --- /dev/null +++ b/backend/alembic/versions/add_hc_chat_widget_001_add_chat_widget_enabled.py @@ -0,0 +1,33 @@ +"""add chat_widget_enabled to help_center_settings + +Splits the public help center's two AI surfaces into independent toggles: the +inline AI quick-summary in search (ai_search_enabled) and the embedded chat +widget (chat_widget_enabled). Existing rows default to enabled to preserve the +prior behaviour, where a mapped agent's widget was shown alongside AI search. + +Revision ID: add_hc_chat_widget_001 +Revises: f7a1c2d3e4b5 +Create Date: 2026-07-13 +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'add_hc_chat_widget_001' +down_revision = 'f7a1c2d3e4b5' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + 'help_center_settings', + sa.Column('chat_widget_enabled', sa.Boolean(), nullable=False, server_default=sa.true()), + ) + # Drop the server default now that existing rows are backfilled — the model + # supplies the default on insert, matching ai_search_enabled. + op.alter_column('help_center_settings', 'chat_widget_enabled', server_default=None) + + +def downgrade() -> None: + op.drop_column('help_center_settings', 'chat_widget_enabled') diff --git a/backend/alembic/versions/add_hc_cta_enabled_001_add_cta_enabled.py b/backend/alembic/versions/add_hc_cta_enabled_001_add_cta_enabled.py new file mode 100644 index 0000000..8532b84 --- /dev/null +++ b/backend/alembic/versions/add_hc_cta_enabled_001_add_cta_enabled.py @@ -0,0 +1,30 @@ +"""add cta_enabled to help_center_settings + +Lets the primary button (CTA) in the public help-center header be toggled on/off +independently of whether its text/URL are filled in. Existing rows default to +enabled to preserve the prior behaviour (CTA shown when text and URL are set). + +Revision ID: add_hc_cta_enabled_001 +Revises: add_hc_chat_widget_001 +Create Date: 2026-07-13 +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'add_hc_cta_enabled_001' +down_revision = 'add_hc_chat_widget_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + 'help_center_settings', + sa.Column('cta_enabled', sa.Boolean(), nullable=False, server_default=sa.true()), + ) + op.alter_column('help_center_settings', 'cta_enabled', server_default=None) + + +def downgrade() -> None: + op.drop_column('help_center_settings', 'cta_enabled') diff --git a/backend/alembic/versions/add_hc_faq_feedback_001_add_faq_feedback.py b/backend/alembic/versions/add_hc_faq_feedback_001_add_faq_feedback.py new file mode 100644 index 0000000..076bb87 --- /dev/null +++ b/backend/alembic/versions/add_hc_faq_feedback_001_add_faq_feedback.py @@ -0,0 +1,42 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Per-article "Was this helpful?" counters on faqs: helpful_yes / helpful_no. +Votes are aggregated as counts (de-duplicated per IP/day at the API); no +per-vote rows are stored. + +Revision ID: add_hc_faq_feedback_001 +Revises: add_hc_job_meter_001 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "add_hc_faq_feedback_001" +down_revision = "add_hc_job_meter_001" +branch_labels = None +depends_on = None + +TABLE = "faqs" + + +def upgrade() -> None: + op.add_column(TABLE, sa.Column("helpful_yes", sa.Integer(), nullable=False, server_default="0")) + op.add_column(TABLE, sa.Column("helpful_no", sa.Integer(), nullable=False, server_default="0")) + + +def downgrade() -> None: + op.drop_column(TABLE, "helpful_no") + op.drop_column(TABLE, "helpful_yes") diff --git a/backend/alembic/versions/add_hc_faq_slug_001_add_faq_slug.py b/backend/alembic/versions/add_hc_faq_slug_001_add_faq_slug.py new file mode 100644 index 0000000..7fe0607 --- /dev/null +++ b/backend/alembic/versions/add_hc_faq_slug_001_add_faq_slug.py @@ -0,0 +1,61 @@ +"""add slug to faqs for public article-page URLs + +FAQs become full help-center articles served at /a/{slug}. Adds a nullable +per-org-unique slug column, then backfills existing rows from their question +(deduping collisions with -2, -3, … within each org). New/edited FAQs get their +slug assigned in the API; this backfill covers everything created before. + +Revision ID: add_hc_faq_slug_001 +Revises: add_hc_cta_enabled_001 +Create Date: 2026-07-13 +""" +import re + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'add_hc_faq_slug_001' +down_revision = 'add_hc_cta_enabled_001' +branch_labels = None +depends_on = None + +SLUG_MAX_LENGTH = 80 +_SLUG_CLEAN_RE = re.compile(r"[^a-z0-9]+") + + +def _slugify(text: str) -> str: + base = _SLUG_CLEAN_RE.sub("-", (text or "").casefold()).strip("-") + return base[:SLUG_MAX_LENGTH].strip("-") or "article" + + +def _backfill(conn) -> None: + faqs = conn.execute( + sa.text("SELECT id, organization_id, question FROM faqs ORDER BY created_at ASC, id ASC") + ).fetchall() + used: dict = {} # organization_id -> set(slug) + for faq_id, org_id, question in faqs: + seen = used.setdefault(org_id, set()) + base = _slugify(question) + candidate = base + suffix = 2 + while candidate in seen: + tail = f"-{suffix}" + candidate = f"{base[:SLUG_MAX_LENGTH - len(tail)]}{tail}" + suffix += 1 + seen.add(candidate) + conn.execute( + sa.text("UPDATE faqs SET slug = :slug WHERE id = :id"), + {"slug": candidate, "id": faq_id}, + ) + + +def upgrade() -> None: + op.add_column('faqs', sa.Column('slug', sa.String(length=80), nullable=True)) + _backfill(op.get_bind()) + op.create_index('ix_faqs_org_slug', 'faqs', ['organization_id', 'slug'], unique=True) + + +def downgrade() -> None: + op.drop_index('ix_faqs_org_slug', table_name='faqs') + op.drop_column('faqs', 'slug') diff --git a/backend/alembic/versions/add_hc_faq_url_path_001_add_preserved_paths.py b/backend/alembic/versions/add_hc_faq_url_path_001_add_preserved_paths.py new file mode 100644 index 0000000..50a29d2 --- /dev/null +++ b/backend/alembic/versions/add_hc_faq_url_path_001_add_preserved_paths.py @@ -0,0 +1,51 @@ +"""preserve original article URLs when migrating a help center + +An org importing an existing help center can now keep every article at the URL +it already ranks for: faqs.url_path is the root-relative path the article is +SERVED at, and /a/{slug} 301s to it. faqs.source_url records where an imported +article came from (provenance only, never routed on). The job flag carries the +import-time opt-in through to the worker. + +No backfill on purpose — NULL url_path is exactly today's behaviour (/a/{slug}), +so nothing already published moves. + +Revision ID: add_hc_faq_url_path_001 +Revises: repair_agent_role_seed_001 +Create Date: 2026-08-06 +""" +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'add_hc_faq_url_path_001' +down_revision = 'repair_agent_role_seed_001' +branch_labels = None +depends_on = None + +URL_PATH_MAX_LENGTH = 400 +SOURCE_URL_MAX_LENGTH = 2048 + + +def upgrade() -> None: + op.add_column('faqs', sa.Column('url_path', sa.String(length=URL_PATH_MAX_LENGTH), nullable=True)) + op.add_column('faqs', sa.Column('source_url', sa.String(length=SOURCE_URL_MAX_LENGTH), nullable=True)) + # Serves the preserved-path lookup and enforces per-org uniqueness. Every + # existing row is NULL here, and Postgres treats NULLs as distinct, so the + # unique index applies cleanly without a backfill. + op.create_index('ix_faqs_org_url_path', 'faqs', ['organization_id', 'url_path'], unique=True) + op.add_column( + 'faq_generation_jobs', + sa.Column( + 'preserve_source_urls', + sa.Boolean(), + nullable=False, + server_default=sa.false(), + ), + ) + + +def downgrade() -> None: + op.drop_column('faq_generation_jobs', 'preserve_source_urls') + op.drop_index('ix_faqs_org_url_path', table_name='faqs') + op.drop_column('faqs', 'source_url') + op.drop_column('faqs', 'url_path') diff --git a/backend/alembic/versions/add_hc_favicon_url.py b/backend/alembic/versions/add_hc_favicon_url.py new file mode 100644 index 0000000..9173659 --- /dev/null +++ b/backend/alembic/versions/add_hc_favicon_url.py @@ -0,0 +1,24 @@ +"""Help center favicon URL + +Revision ID: add_hc_favicon_001 +Revises: merge_tkt_csat_001 +Create Date: 2026-07-25 + +Adds a nullable favicon_url to help center settings, mirroring logo_url, so orgs +can set the browser-tab icon for their public help center from Customization. +""" +from alembic import op +import sqlalchemy as sa + +revision = "add_hc_favicon_001" +down_revision = "merge_tkt_csat_001" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("help_center_settings", sa.Column("favicon_url", sa.String(), nullable=True)) + + +def downgrade() -> None: + op.drop_column("help_center_settings", "favicon_url") diff --git a/backend/alembic/versions/add_hc_job_meter_001_add_job_metering.py b/backend/alembic/versions/add_hc_job_meter_001_add_job_metering.py new file mode 100644 index 0000000..96e205e --- /dev/null +++ b/backend/alembic/versions/add_hc_job_meter_001_add_job_metering.py @@ -0,0 +1,48 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +FAQ generation-job metering + targeting columns: +- llm_calls / metered: per-job LLM call counter for hosted-model message + metering (metered stamped at enqueue from the org's AI config). +- knowledge_ids: optional explicit source narrowing for GENERATE_ALL jobs. +- source_file_name: original filename for PDF imports. + +Revision ID: add_hc_job_meter_001 +Revises: add_hc_faq_slug_001 +""" + +import sqlalchemy as sa +from alembic import op + +revision = "add_hc_job_meter_001" +down_revision = "add_hc_faq_slug_001" +branch_labels = None +depends_on = None + +TABLE = "faq_generation_jobs" + + +def upgrade() -> None: + op.add_column(TABLE, sa.Column("llm_calls", sa.Integer(), nullable=False, server_default="0")) + op.add_column(TABLE, sa.Column("metered", sa.Boolean(), nullable=False, server_default=sa.text("true"))) + op.add_column(TABLE, sa.Column("knowledge_ids", sa.JSON(), nullable=True)) + op.add_column(TABLE, sa.Column("source_file_name", sa.String(length=255), nullable=True)) + + +def downgrade() -> None: + op.drop_column(TABLE, "source_file_name") + op.drop_column(TABLE, "knowledge_ids") + op.drop_column(TABLE, "metered") + op.drop_column(TABLE, "llm_calls") diff --git a/backend/alembic/versions/add_invest_glassbox_001_add_glassbox.py b/backend/alembic/versions/add_invest_glassbox_001_add_glassbox.py new file mode 100644 index 0000000..699ee3e --- /dev/null +++ b/backend/alembic/versions/add_invest_glassbox_001_add_glassbox.py @@ -0,0 +1,124 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add investigation glass-box tables (hypotheses, events, RCA documents) + +Revision ID: add_invest_glassbox_001 +Revises: merge_tkt_phone_001 +Create Date: 2026-07-17 + +Phase 3 of AI-first ticketing: hypothesis-driven investigation with evidence +captured as first-class rows (investigation_events) and structured, versioned +RCA documents. +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'add_invest_glassbox_001' +down_revision = 'merge_tkt_phone_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'investigation_hypotheses', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('run_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('idx', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=300), nullable=False), + sa.Column('rationale', sa.Text(), nullable=True), + sa.Column('status', sa.String(), nullable=False), + sa.Column('confidence', sa.Float(), nullable=True), + sa.Column('conclusion', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['run_id'], ['investigation_runs.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_investigation_hypotheses_run_id'), 'investigation_hypotheses', ['run_id']) + op.create_index(op.f('ix_investigation_hypotheses_ticket_id'), 'investigation_hypotheses', ['ticket_id']) + + op.create_table( + 'investigation_events', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('run_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('hypothesis_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('seq', sa.Integer(), nullable=False), + sa.Column('event_type', sa.String(), nullable=False), + sa.Column('label', sa.String(length=300), nullable=True), + sa.Column('tool_name', sa.String(length=200), nullable=True), + sa.Column('connector_name', sa.String(length=200), nullable=True), + sa.Column('tool_input', sa.Text(), nullable=True), + sa.Column('tool_result', sa.Text(), nullable=True), + sa.Column('duration_ms', sa.Integer(), nullable=True), + sa.Column('error', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['run_id'], ['investigation_runs.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['hypothesis_id'], ['investigation_hypotheses.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_investigation_events_run_id'), 'investigation_events', ['run_id']) + op.create_index(op.f('ix_investigation_events_ticket_id'), 'investigation_events', ['ticket_id']) + op.create_index('ix_investigation_events_run_seq', 'investigation_events', ['run_id', 'seq']) + + op.create_table( + 'rca_documents', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('run_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('version', sa.Integer(), nullable=False), + sa.Column('summary', sa.Text(), nullable=True), + sa.Column('impact', sa.Text(), nullable=True), + sa.Column('timeline', postgresql.JSON(astext_type=sa.Text()), nullable=True), + sa.Column('investigation_log', sa.Text(), nullable=True), + sa.Column('contributing_factors', postgresql.JSON(astext_type=sa.Text()), nullable=True), + sa.Column('conclusion', sa.Text(), nullable=True), + sa.Column('remediation', sa.Text(), nullable=True), + sa.Column('prevention', sa.Text(), nullable=True), + sa.Column('customer_summary', sa.Text(), nullable=True), + sa.Column('confidence', sa.Float(), nullable=True), + sa.Column('is_partial', sa.Boolean(), server_default=sa.text('false'), nullable=False), + sa.Column('generated_by', sa.String(), server_default='ai', nullable=False), + sa.Column('reviewed_by_user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('reviewed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['run_id'], ['investigation_runs.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['reviewed_by_user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('ticket_id', 'version', name='uq_rca_documents_ticket_version'), + ) + op.create_index(op.f('ix_rca_documents_ticket_id'), 'rca_documents', ['ticket_id']) + + +def downgrade() -> None: + op.drop_index(op.f('ix_rca_documents_ticket_id'), table_name='rca_documents') + op.drop_table('rca_documents') + op.drop_index('ix_investigation_events_run_seq', table_name='investigation_events') + op.drop_index(op.f('ix_investigation_events_ticket_id'), table_name='investigation_events') + op.drop_index(op.f('ix_investigation_events_run_id'), table_name='investigation_events') + op.drop_table('investigation_events') + op.drop_index(op.f('ix_investigation_hypotheses_ticket_id'), table_name='investigation_hypotheses') + op.drop_index(op.f('ix_investigation_hypotheses_run_id'), table_name='investigation_hypotheses') + op.drop_table('investigation_hypotheses') diff --git a/backend/alembic/versions/add_investigation_metered.py b/backend/alembic/versions/add_investigation_metered.py new file mode 100644 index 0000000..7870f32 --- /dev/null +++ b/backend/alembic/versions/add_investigation_metered.py @@ -0,0 +1,41 @@ +"""Add metered flag to investigation_runs + +Revision ID: add_tkt_metered_001 +Revises: add_tkt_row_scope_001 +Create Date: 2026-07-21 + +Marks whether an investigation run's LLM usage counts against the org's message +budget (hosted model only). Existing rows default to false — nothing is +retroactively metered on upgrade. +""" +from alembic import op +import sqlalchemy as sa + +revision = "add_tkt_metered_001" +down_revision = "add_tkt_row_scope_001" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + "investigation_runs", + sa.Column( + "metered", + sa.Boolean(), + nullable=False, + server_default=sa.text("false"), + ), + ) + # The enterprise message-limit check sums llm_calls of metered runs per + # billing window — an index on (org, metered, created_at) keeps that cheap. + op.create_index( + "ix_investigation_runs_metered_period", + "investigation_runs", + ["organization_id", "metered", "created_at"], + ) + + +def downgrade() -> None: + op.drop_index("ix_investigation_runs_metered_period", table_name="investigation_runs") + op.drop_column("investigation_runs", "metered") diff --git a/backend/alembic/versions/add_investigation_runs_001_add_runs.py b/backend/alembic/versions/add_investigation_runs_001_add_runs.py new file mode 100644 index 0000000..2de189f --- /dev/null +++ b/backend/alembic/versions/add_investigation_runs_001_add_runs.py @@ -0,0 +1,93 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add investigation_runs (AI triage/investigation queue) + +Revision ID: add_investigation_runs_001 +Revises: add_ticketing_core_001 +Create Date: 2026-07-16 + +Phase 2 of AI-first ticketing: the run table doubles as the worker queue +(ticket_investigator polls status='pending', mirroring faq_generation_jobs). +Budgets and token actuals live on the row for cost tracking. +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'add_investigation_runs_001' +down_revision = 'add_ticketing_core_001' +branch_labels = None +depends_on = None + +ACTIVE_RUN_STATUSES = ('pending', 'running') + + +def upgrade() -> None: + op.create_table( + 'investigation_runs', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('run_type', sa.String(), nullable=False), + sa.Column('status', sa.String(), nullable=False), + sa.Column('trigger', sa.String(), nullable=False), + sa.Column('requested_by_user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('context_note', sa.Text(), nullable=True), + sa.Column('max_tool_calls', sa.Integer(), server_default='25', nullable=False), + sa.Column('max_wall_seconds', sa.Integer(), server_default='600', nullable=False), + sa.Column('tool_calls_used', sa.Integer(), server_default='0', nullable=False), + sa.Column('llm_calls', sa.Integer(), server_default='0', nullable=False), + sa.Column('input_tokens', sa.BigInteger(), server_default='0', nullable=False), + sa.Column('output_tokens', sa.BigInteger(), server_default='0', nullable=False), + sa.Column('model_name', sa.String(), nullable=True), + sa.Column('error', sa.Text(), nullable=True), + sa.Column('started_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('finished_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['requested_by_user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_investigation_runs_ticket_id'), 'investigation_runs', ['ticket_id']) + op.create_index(op.f('ix_investigation_runs_organization_id'), 'investigation_runs', ['organization_id']) + # Keeps the worker's poll O(active rows) as terminal rows accumulate. + op.create_index( + 'ix_investigation_runs_active', + 'investigation_runs', + ['status'], + postgresql_where=sa.text( + "status IN ({})".format(", ".join(f"'{s}'" for s in ACTIVE_RUN_STATUSES)) + ), + ) + # Closes the enqueue check-then-insert race: at most one active run per + # ticket. Lives only in the migration (sqlite tests can't compile the + # partial WHERE). + op.execute( + "CREATE UNIQUE INDEX uq_investigation_runs_one_active ON investigation_runs " + "(ticket_id) WHERE status IN ('pending', 'running')" + ) + + +def downgrade() -> None: + op.execute("DROP INDEX IF EXISTS uq_investigation_runs_one_active") + op.drop_index('ix_investigation_runs_active', table_name='investigation_runs') + op.drop_index(op.f('ix_investigation_runs_organization_id'), table_name='investigation_runs') + op.drop_index(op.f('ix_investigation_runs_ticket_id'), table_name='investigation_runs') + op.drop_table('investigation_runs') diff --git a/backend/alembic/versions/add_mcp_usage_guidance_001.py b/backend/alembic/versions/add_mcp_usage_guidance_001.py new file mode 100644 index 0000000..fae6f1c --- /dev/null +++ b/backend/alembic/versions/add_mcp_usage_guidance_001.py @@ -0,0 +1,61 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add mcp_tools.usage_guidance + +Operator-written notes on what an MCP source holds and how to query it. +Reaches the model as agno per-toolkit instructions so investigations stop +inventing index and field names (#318). Nullable with no server default, so +existing rows stay NULL and their prompts are unchanged. + +Revision ID: add_mcp_usage_guidance_001 +Revises: add_openai_compat_ai_model_001 +Create Date: 2026-09-08 00:00:00.000000 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = 'add_mcp_usage_guidance_001' +down_revision: Union[str, None] = 'add_openai_compat_ai_model_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def _has_column() -> bool: + """Development databases are shared between feature branches, so the column + can already exist by the time this runs. Adding it twice would abort the + upgrade.""" + inspector = sa.inspect(op.get_bind()) + return any(c['name'] == 'usage_guidance' + for c in inspector.get_columns('mcp_tools')) + + +def upgrade() -> None: + if _has_column(): + return + op.add_column( + 'mcp_tools', + sa.Column('usage_guidance', sa.Text(), nullable=True), + ) + + +def downgrade() -> None: + if _has_column(): + op.drop_column('mcp_tools', 'usage_guidance') diff --git a/backend/alembic/versions/add_notif_prefs_001_user_notification_settings.py b/backend/alembic/versions/add_notif_prefs_001_user_notification_settings.py new file mode 100644 index 0000000..a62720d --- /dev/null +++ b/backend/alembic/versions/add_notif_prefs_001_user_notification_settings.py @@ -0,0 +1,57 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add per-user chat notification preferences + +Revision ID: add_notif_prefs_001 +Revises: add_faq_seo_001 +Create Date: 2026-07-22 + +Rows are created lazily on first read, so nothing is backfilled here — a user +without a row falls back to the column defaults (transfer/assignment on, +new-chat off), which keeps today's transfer notifications working unchanged. +The revision id is kept short — alembic_version.version_num is varchar(32). +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID + +# revision identifiers, used by Alembic. +revision = 'add_notif_prefs_001' +down_revision = 'add_faq_seo_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'user_notification_settings', + sa.Column('user_id', UUID(as_uuid=True), + sa.ForeignKey('users.id', ondelete='CASCADE'), primary_key=True), + sa.Column('notify_new_chat', sa.Boolean(), nullable=False, + server_default=sa.false()), + sa.Column('notify_chat_transfer', sa.Boolean(), nullable=False, + server_default=sa.true()), + sa.Column('notify_chat_assigned', sa.Boolean(), nullable=False, + server_default=sa.true()), + sa.Column('created_at', sa.DateTime(timezone=True), + server_default=sa.func.now()), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + ) + + +def downgrade() -> None: + op.drop_table('user_notification_settings') diff --git a/backend/alembic/versions/add_openai_compat_ai_model_001.py b/backend/alembic/versions/add_openai_compat_ai_model_001.py new file mode 100644 index 0000000..6e9fb29 --- /dev/null +++ b/backend/alembic/versions/add_openai_compat_ai_model_001.py @@ -0,0 +1,40 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add OPENAI_COMPATIBLE AI model type + +Revision ID: add_openai_compat_ai_model_001 +Revises: fcm_tokens_per_device_001 +Create Date: 2026-09-03 00:00:00.000000 + +""" +from typing import Sequence, Union +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = 'add_openai_compat_ai_model_001' +down_revision: Union[str, None] = 'fcm_tokens_per_device_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.execute("ALTER TYPE aimodeltype ADD VALUE IF NOT EXISTS 'OPENAI_COMPATIBLE'") + + +def downgrade() -> None: + # PostgreSQL does not support removing enum values + pass diff --git a/backend/alembic/versions/add_require_token_auth_001.py b/backend/alembic/versions/add_require_token_auth_001.py new file mode 100644 index 0000000..9d3a186 --- /dev/null +++ b/backend/alembic/versions/add_require_token_auth_001.py @@ -0,0 +1,44 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add require_token_auth to agents table + +Revision ID: add_require_token_auth_001 +Revises: add_allow_attachments_001 +Create Date: 2025-12-01 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'add_require_token_auth_001' +down_revision: Union[str, Sequence[str]] = 'add_allow_attachments_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('agents', + sa.Column('require_token_auth', sa.Boolean(), nullable=False, server_default='false') + ) + + +def downgrade() -> None: + op.drop_column('agents', 'require_token_auth') diff --git a/backend/alembic/versions/add_run_connector_status_001.py b/backend/alembic/versions/add_run_connector_status_001.py new file mode 100644 index 0000000..9650d50 --- /dev/null +++ b/backend/alembic/versions/add_run_connector_status_001.py @@ -0,0 +1,44 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add investigation_runs.connector_status + +Revision ID: add_run_connector_status_001 +Revises: add_ai_disclaimer_001 +Create Date: 2026-07-31 + +Records the MCP connector outcome of each investigation run ({"configured": N, +"loaded": M, "failed": [{"name", "error"}]}) so a run that quietly executed +with 0 of its configured tools (e.g. npx missing in the image, issue #271) is +visible on the run detail instead of only in worker logs. +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSON + +# revision identifiers, used by Alembic. +revision = 'add_run_connector_status_001' +down_revision = 'add_ai_disclaimer_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column('investigation_runs', sa.Column('connector_status', JSON(), nullable=True)) + + +def downgrade() -> None: + op.drop_column('investigation_runs', 'connector_status') diff --git a/backend/alembic/versions/add_sentiment_analysis_001.py b/backend/alembic/versions/add_sentiment_analysis_001.py new file mode 100644 index 0000000..0cea794 --- /dev/null +++ b/backend/alembic/versions/add_sentiment_analysis_001.py @@ -0,0 +1,67 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add sentiment analysis columns + +Revision ID: add_sentiment_analysis_001 +Revises: add_widget_apps_001 +Create Date: 2026-03-16 11:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'add_sentiment_analysis_001' +down_revision: Union[str, None] = 'add_widget_apps_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add sentiment columns to chat_history + op.add_column('chat_history', sa.Column('sentiment_label', sa.String(), nullable=True)) + op.add_column('chat_history', sa.Column('sentiment_score', sa.Float(), nullable=True)) + + # Add sentiment columns to session_to_agents + op.add_column('session_to_agents', sa.Column('sentiment_label', sa.String(), nullable=True)) + op.add_column('session_to_agents', sa.Column('sentiment_score', sa.Float(), nullable=True)) + + # Create indexes for efficient sentiment queries + op.create_index( + 'ix_chat_history_sentiment_label', + 'chat_history', + ['sentiment_label'], + unique=False + ) + op.create_index( + 'ix_session_to_agents_sentiment_label', + 'session_to_agents', + ['sentiment_label'], + unique=False + ) + + +def downgrade() -> None: + op.drop_index('ix_session_to_agents_sentiment_label', table_name='session_to_agents') + op.drop_index('ix_chat_history_sentiment_label', table_name='chat_history') + op.drop_column('session_to_agents', 'sentiment_score') + op.drop_column('session_to_agents', 'sentiment_label') + op.drop_column('chat_history', 'sentiment_score') + op.drop_column('chat_history', 'sentiment_label') diff --git a/backend/alembic/versions/add_slack_integration_001.py b/backend/alembic/versions/add_slack_integration_001.py new file mode 100644 index 0000000..7ab8933 --- /dev/null +++ b/backend/alembic/versions/add_slack_integration_001.py @@ -0,0 +1,165 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add Slack integration tables + +Revision ID: add_slack_integration_001 +Revises: add_allow_attachments_001 +Create Date: 2025-11-25 10:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision: str = 'add_slack_integration_001' +down_revision: Union[str, None] = 'add_allow_attachments_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create StorageMode enum type if it doesn't exist + connection = op.get_bind() + result = connection.execute(sa.text( + "SELECT 1 FROM pg_type WHERE typname = 'storagemode'" + )) + if not result.fetchone(): + op.execute("CREATE TYPE storagemode AS ENUM ('FULL_CONTENT', 'METADATA_ONLY', 'EMBEDDINGS_ONLY')") + + # Create slack_tokens table + op.create_table('slack_tokens', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('access_token', sa.String(), nullable=False), + sa.Column('bot_user_id', sa.String(), nullable=False), + sa.Column('team_id', sa.String(), nullable=False), + sa.Column('team_name', sa.String(), nullable=False), + sa.Column('authed_user_id', sa.String(), nullable=True), + sa.Column('scope', sa.String(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_slack_tokens_id'), 'slack_tokens', ['id'], unique=False) + op.create_index(op.f('ix_slack_tokens_organization_id'), 'slack_tokens', ['organization_id'], unique=False) + op.create_index(op.f('ix_slack_tokens_team_id'), 'slack_tokens', ['team_id'], unique=False) + + # Create slack_workspace_configs table + op.create_table('slack_workspace_configs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('team_id', sa.String(), nullable=False), + sa.Column('allowed_channel_ids', sa.JSON(), nullable=True), + sa.Column('storage_mode', postgresql.ENUM('FULL_CONTENT', 'METADATA_ONLY', 'EMBEDDINGS_ONLY', name='storagemode', create_type=False), nullable=False), + sa.Column('default_agent_id', sa.UUID(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['default_agent_id'], ['agents.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_slack_workspace_configs_id'), 'slack_workspace_configs', ['id'], unique=False) + op.create_index(op.f('ix_slack_workspace_configs_organization_id'), 'slack_workspace_configs', ['organization_id'], unique=False) + op.create_index(op.f('ix_slack_workspace_configs_team_id'), 'slack_workspace_configs', ['team_id'], unique=False) + + # Create agent_slack_configs table + op.create_table('agent_slack_configs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('team_id', sa.String(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=False), + sa.Column('channel_id', sa.String(), nullable=False), + sa.Column('channel_name', sa.String(), nullable=False), + sa.Column('enabled', sa.Boolean(), nullable=False, server_default='true'), + sa.Column('respond_to_mentions', sa.Boolean(), nullable=False, server_default='true'), + sa.Column('respond_to_reactions', sa.Boolean(), nullable=False, server_default='true'), + sa.Column('respond_to_commands', sa.Boolean(), nullable=False, server_default='true'), + sa.Column('reaction_emoji', sa.String(), nullable=False, server_default='robot_face'), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('organization_id', 'channel_id', name='uq_agent_slack_config_org_channel') + ) + op.create_index(op.f('ix_agent_slack_configs_id'), 'agent_slack_configs', ['id'], unique=False) + op.create_index(op.f('ix_agent_slack_configs_organization_id'), 'agent_slack_configs', ['organization_id'], unique=False) + op.create_index(op.f('ix_agent_slack_configs_team_id'), 'agent_slack_configs', ['team_id'], unique=False) + op.create_index(op.f('ix_agent_slack_configs_agent_id'), 'agent_slack_configs', ['agent_id'], unique=False) + op.create_index(op.f('ix_agent_slack_configs_channel_id'), 'agent_slack_configs', ['channel_id'], unique=False) + + # Create slack_conversations table + op.create_table('slack_conversations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('team_id', sa.String(), nullable=False), + sa.Column('channel_id', sa.String(), nullable=False), + sa.Column('thread_ts', sa.String(), nullable=False), + sa.Column('session_id', sa.UUID(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('slack_user_id', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('team_id', 'channel_id', 'thread_ts', name='uq_slack_conversation_thread') + ) + op.create_index(op.f('ix_slack_conversations_id'), 'slack_conversations', ['id'], unique=False) + op.create_index(op.f('ix_slack_conversations_team_id'), 'slack_conversations', ['team_id'], unique=False) + op.create_index(op.f('ix_slack_conversations_channel_id'), 'slack_conversations', ['channel_id'], unique=False) + op.create_index(op.f('ix_slack_conversations_thread_ts'), 'slack_conversations', ['thread_ts'], unique=False) + op.create_index(op.f('ix_slack_conversations_session_id'), 'slack_conversations', ['session_id'], unique=False) + + +def downgrade() -> None: + # Drop slack_conversations table + op.drop_index(op.f('ix_slack_conversations_session_id'), table_name='slack_conversations') + op.drop_index(op.f('ix_slack_conversations_thread_ts'), table_name='slack_conversations') + op.drop_index(op.f('ix_slack_conversations_channel_id'), table_name='slack_conversations') + op.drop_index(op.f('ix_slack_conversations_team_id'), table_name='slack_conversations') + op.drop_index(op.f('ix_slack_conversations_id'), table_name='slack_conversations') + op.drop_table('slack_conversations') + + # Drop agent_slack_configs table + op.drop_index(op.f('ix_agent_slack_configs_channel_id'), table_name='agent_slack_configs') + op.drop_index(op.f('ix_agent_slack_configs_agent_id'), table_name='agent_slack_configs') + op.drop_index(op.f('ix_agent_slack_configs_team_id'), table_name='agent_slack_configs') + op.drop_index(op.f('ix_agent_slack_configs_organization_id'), table_name='agent_slack_configs') + op.drop_index(op.f('ix_agent_slack_configs_id'), table_name='agent_slack_configs') + op.drop_table('agent_slack_configs') + + # Drop slack_workspace_configs table + op.drop_index(op.f('ix_slack_workspace_configs_team_id'), table_name='slack_workspace_configs') + op.drop_index(op.f('ix_slack_workspace_configs_organization_id'), table_name='slack_workspace_configs') + op.drop_index(op.f('ix_slack_workspace_configs_id'), table_name='slack_workspace_configs') + op.drop_table('slack_workspace_configs') + + # Drop slack_tokens table + op.drop_index(op.f('ix_slack_tokens_team_id'), table_name='slack_tokens') + op.drop_index(op.f('ix_slack_tokens_organization_id'), table_name='slack_tokens') + op.drop_index(op.f('ix_slack_tokens_id'), table_name='slack_tokens') + op.drop_table('slack_tokens') + + # Drop StorageMode enum type + sa.Enum('FULL_CONTENT', 'METADATA_ONLY', 'EMBEDDINGS_ONLY', name='storagemode').drop(op.get_bind(), checkfirst=True) diff --git a/backend/alembic/versions/add_ticket_csat.py b/backend/alembic/versions/add_ticket_csat.py new file mode 100644 index 0000000..60e0486 --- /dev/null +++ b/backend/alembic/versions/add_ticket_csat.py @@ -0,0 +1,43 @@ +"""CSAT score linkage on tickets + +Revision ID: add_tkt_csat_001 +Revises: add_tkt_row_scope_001 +Create Date: 2026-07-22 + +The CSAT ask already goes out on close; nothing carried the answer back. These +columns close the loop: when the customer rates the conversation linked to a +ticket, the score lands on the ticket itself, so AI-resolved vs human-resolved +CSAT is a plain query instead of a join through ratings/sessions. +""" +from alembic import op +import sqlalchemy as sa + +revision = "add_tkt_csat_001" +down_revision = "add_tkt_row_scope_001" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column("tickets", sa.Column("csat_requested_at", sa.DateTime(timezone=True), nullable=True)) + op.add_column("tickets", sa.Column("csat_score", sa.Integer(), nullable=True)) + op.add_column("tickets", sa.Column("csat_rating_id", sa.UUID(), nullable=True)) + op.add_column("tickets", sa.Column("csat_responded_at", sa.DateTime(timezone=True), nullable=True)) + op.create_foreign_key( + "fk_tickets_csat_rating_id", + "tickets", + "ratings", + ["csat_rating_id"], + ["id"], + ondelete="SET NULL", + ) + op.create_index("ix_tickets_org_csat", "tickets", ["organization_id", "csat_responded_at"]) + + +def downgrade() -> None: + op.drop_index("ix_tickets_org_csat", table_name="tickets") + op.drop_constraint("fk_tickets_csat_rating_id", "tickets", type_="foreignkey") + op.drop_column("tickets", "csat_responded_at") + op.drop_column("tickets", "csat_rating_id") + op.drop_column("tickets", "csat_score") + op.drop_column("tickets", "csat_requested_at") diff --git a/backend/alembic/versions/add_ticketing_core_001_add_native_ticketing.py b/backend/alembic/versions/add_ticketing_core_001_add_native_ticketing.py new file mode 100644 index 0000000..cdbf816 --- /dev/null +++ b/backend/alembic/versions/add_ticketing_core_001_add_native_ticketing.py @@ -0,0 +1,229 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add native AI-first ticketing core tables + +Revision ID: add_ticketing_core_001 +Revises: add_hc_faq_feedback_001 +Create Date: 2026-07-16 + +New feature: native AI-first ticketing (replaces the Jira-only flow). Tickets +are first-class aggregates decoupled from chat sessions; the legacy +session_to_agents.ticket_* columns remain as a denormalized mirror. + +Tables: +- tickets: the aggregate root (per-org TKT-{n} numbers, triage fields, + pgvector embedding for dedup, SLA timestamps, external escalation ref) +- ticket_sequences: per-org display-number allocator (SELECT ... FOR UPDATE) +- ticket_sessions: ticket <-> chat-session link table +- ticket_activities: append-only comments + audit trail +- organization_ticket_settings: autonomy level, SLA targets, comms templates, + investigation budgets + +Status/priority/source columns are plain strings (mirroring +faq_generation_jobs) so new values never need an ALTER TYPE migration. + +Also seeds view_tickets/manage_tickets permissions and attaches them to +existing roles that already hold manage_all_chats (new orgs get them from +Permission.default_permissions()). +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'add_ticketing_core_001' +down_revision = 'add_hc_faq_feedback_001' +branch_labels = None +depends_on = None + +TICKET_EMBEDDING_DIM = 384 + + +def upgrade() -> None: + # pgvector for the dedup embedding (idempotent; also created by + # scripts/init-vector.sql on fresh databases). + op.execute("CREATE EXTENSION IF NOT EXISTS vector") + + op.create_table( + 'tickets', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('ticket_number', sa.Integer(), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('customer_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('title', sa.String(length=500), nullable=False), + sa.Column('original_title', sa.String(length=500), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('status', sa.String(), nullable=False), + sa.Column('priority', sa.String(), nullable=False), + sa.Column('severity', sa.Integer(), nullable=True), + sa.Column('source', sa.String(), nullable=False), + sa.Column('intent', sa.String(), nullable=True), + sa.Column('triage_confidence', sa.Float(), nullable=True), + sa.Column('ai_summary', sa.Text(), nullable=True), + sa.Column('tags', sa.JSON(), nullable=True), + sa.Column('assignee_user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('group_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('duplicate_of_ticket_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('resolution_outcome', sa.String(), nullable=True), + sa.Column('resolution_summary', sa.Text(), nullable=True), + sa.Column('customer_resolution_message', sa.Text(), nullable=True), + sa.Column('resolved_by_actor', sa.String(), nullable=True), + sa.Column('first_response_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('resolved_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('closed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('confirmation_requested_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('reopened_count', sa.Integer(), server_default='0', nullable=False), + sa.Column('external_ref_type', sa.String(), nullable=True), + sa.Column('external_ref_id', sa.String(), nullable=True), + sa.Column('external_ref_url', sa.String(), nullable=True), + sa.Column('created_by_user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['assignee_user_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['duplicate_of_ticket_id'], ['tickets.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['created_by_user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('organization_id', 'ticket_number', name='uq_tickets_org_number'), + ) + # The vector column via raw SQL so the sqlalchemy migration stays free of + # the pgvector python type. + op.execute(f"ALTER TABLE tickets ADD COLUMN embedding vector({TICKET_EMBEDDING_DIM})") + op.create_index(op.f('ix_tickets_organization_id'), 'tickets', ['organization_id']) + op.create_index(op.f('ix_tickets_status'), 'tickets', ['status']) + op.create_index('ix_tickets_org_status', 'tickets', ['organization_id', 'status']) + op.create_index('ix_tickets_org_assignee', 'tickets', ['organization_id', 'assignee_user_id']) + op.create_index('ix_tickets_org_created', 'tickets', ['organization_id', 'created_at']) + # Approximate-NN index for dedup/similar-ticket recall (cosine). + op.execute( + "CREATE INDEX ix_tickets_embedding_hnsw ON tickets " + "USING hnsw (embedding vector_cosine_ops)" + ) + + op.create_table( + 'ticket_sequences', + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('next_number', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('organization_id'), + ) + + op.create_table( + 'ticket_sessions', + sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('session_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('linked_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('ticket_id', 'session_id'), + ) + op.create_index(op.f('ix_ticket_sessions_session_id'), 'ticket_sessions', ['session_id']) + + op.create_table( + 'ticket_activities', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('activity_type', sa.String(), nullable=False), + sa.Column('actor_type', sa.String(), nullable=False), + sa.Column('actor_user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('body', sa.Text(), nullable=True), + sa.Column('is_internal', sa.Boolean(), server_default=sa.true(), nullable=False), + sa.Column('activity_metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['actor_user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_ticket_activities_ticket_id'), 'ticket_activities', ['ticket_id']) + op.create_index('ix_ticket_activities_ticket_created', 'ticket_activities', ['ticket_id', 'created_at']) + + op.create_table( + 'organization_ticket_settings', + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('autonomy_level', sa.Integer(), server_default='1', nullable=False), + sa.Column('auto_investigate_on_create', sa.Boolean(), server_default=sa.true(), nullable=False), + sa.Column('min_confidence_to_auto_resolve', sa.Float(), server_default='0.85', nullable=False), + sa.Column('confirmation_timeout_hours', sa.Integer(), server_default='72', nullable=False), + sa.Column('csat_enabled', sa.Boolean(), server_default=sa.true(), nullable=False), + sa.Column('sla_targets', sa.JSON(), nullable=True), + sa.Column('created_template', sa.Text(), nullable=True), + sa.Column('resolved_template', sa.Text(), nullable=True), + sa.Column('jira_escalation_enabled', sa.Boolean(), server_default=sa.false(), nullable=False), + sa.Column('jira_escalation_priority', sa.String(), nullable=True), + sa.Column('investigation_mcp_tool_ids', sa.JSON(), nullable=True), + sa.Column('alert_webhook_enabled', sa.Boolean(), server_default=sa.false(), nullable=False), + sa.Column('max_tool_calls_per_run', sa.Integer(), server_default='25', nullable=False), + sa.Column('max_runs_per_ticket', sa.Integer(), server_default='3', nullable=False), + sa.Column('monthly_investigation_cap', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('organization_id'), + ) + + # Seed the new permissions and attach them to existing roles that already + # manage chats (new orgs seed via Permission.default_permissions()). + op.execute(""" + INSERT INTO permissions (name, description) + SELECT 'view_tickets', 'Can view tickets' + WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE name = 'view_tickets') + """) + op.execute(""" + INSERT INTO permissions (name, description) + SELECT 'manage_tickets', 'Can manage tickets' + WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE name = 'manage_tickets') + """) + op.execute(""" + INSERT INTO role_permissions (role_id, permission_id) + SELECT rp.role_id, p_new.id + FROM role_permissions rp + JOIN permissions p_old ON p_old.id = rp.permission_id AND p_old.name = 'manage_all_chats' + CROSS JOIN permissions p_new + WHERE p_new.name IN ('view_tickets', 'manage_tickets') + AND NOT EXISTS ( + SELECT 1 FROM role_permissions rp2 + WHERE rp2.role_id = rp.role_id AND rp2.permission_id = p_new.id + ) + """) + + +def downgrade() -> None: + op.execute(""" + DELETE FROM role_permissions + WHERE permission_id IN ( + SELECT id FROM permissions WHERE name IN ('view_tickets', 'manage_tickets') + ) + """) + op.execute("DELETE FROM permissions WHERE name IN ('view_tickets', 'manage_tickets')") + op.drop_table('organization_ticket_settings') + op.drop_index('ix_ticket_activities_ticket_created', table_name='ticket_activities') + op.drop_index(op.f('ix_ticket_activities_ticket_id'), table_name='ticket_activities') + op.drop_table('ticket_activities') + op.drop_index(op.f('ix_ticket_sessions_session_id'), table_name='ticket_sessions') + op.drop_table('ticket_sessions') + op.drop_table('ticket_sequences') + op.execute("DROP INDEX IF EXISTS ix_tickets_embedding_hnsw") + op.drop_index('ix_tickets_org_created', table_name='tickets') + op.drop_index('ix_tickets_org_assignee', table_name='tickets') + op.drop_index('ix_tickets_org_status', table_name='tickets') + op.drop_index(op.f('ix_tickets_status'), table_name='tickets') + op.drop_index(op.f('ix_tickets_organization_id'), table_name='tickets') + op.drop_table('tickets') diff --git a/backend/alembic/versions/add_tkt_db_ssh_001_add_ssh.py b/backend/alembic/versions/add_tkt_db_ssh_001_add_ssh.py new file mode 100644 index 0000000..2139f7c --- /dev/null +++ b/backend/alembic/versions/add_tkt_db_ssh_001_add_ssh.py @@ -0,0 +1,54 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add SSH tunnel fields to ticket_db_connectors + +Revision ID: add_tkt_db_ssh_001 +Revises: add_tkt_phase4_001 +Create Date: 2026-07-17 + +Production databases are usually reached through a bastion / jump host rather +than directly — this adds optional SSH-tunnel config to the guardrailed DB +connector (key/password encrypted at rest). +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'add_tkt_db_ssh_001' +down_revision = 'add_tkt_phase4_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column('ticket_db_connectors', sa.Column('ssh_enabled', sa.Boolean(), server_default=sa.text('false'), nullable=False)) + op.add_column('ticket_db_connectors', sa.Column('ssh_host', sa.String(length=500), nullable=True)) + op.add_column('ticket_db_connectors', sa.Column('ssh_port', sa.Integer(), server_default='22', nullable=False)) + op.add_column('ticket_db_connectors', sa.Column('ssh_username', sa.String(length=200), nullable=True)) + op.add_column('ticket_db_connectors', sa.Column('encrypted_ssh_password', sa.Text(), nullable=True)) + op.add_column('ticket_db_connectors', sa.Column('encrypted_ssh_private_key', sa.Text(), nullable=True)) + op.add_column('ticket_db_connectors', sa.Column('encrypted_ssh_key_passphrase', sa.Text(), nullable=True)) + + +def downgrade() -> None: + op.drop_column('ticket_db_connectors', 'encrypted_ssh_key_passphrase') + op.drop_column('ticket_db_connectors', 'encrypted_ssh_private_key') + op.drop_column('ticket_db_connectors', 'encrypted_ssh_password') + op.drop_column('ticket_db_connectors', 'ssh_username') + op.drop_column('ticket_db_connectors', 'ssh_port') + op.drop_column('ticket_db_connectors', 'ssh_host') + op.drop_column('ticket_db_connectors', 'ssh_enabled') diff --git a/backend/alembic/versions/add_tkt_phase4_001_add_phase4.py b/backend/alembic/versions/add_tkt_phase4_001_add_phase4.py new file mode 100644 index 0000000..9057344 --- /dev/null +++ b/backend/alembic/versions/add_tkt_phase4_001_add_phase4.py @@ -0,0 +1,165 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add ticketing Phase 4: guardrailed DB connectors, proposals, webhook secret + +Revision ID: add_tkt_phase4_001 +Revises: add_invest_glassbox_001 +Create Date: 2026-07-17 + +- ticket_db_connectors: read-only, allowlisted, masked DB access for the + investigation agent (creds encrypted at rest) +- db_connector_audit_logs: every attempted query (raw + canonical SQL, + outcome) — result rows are never stored +- ticket_proposals: autonomy L2 propose/approve records +- organization_ticket_settings.alert_webhook_secret for the alert intake +- seeds approve_ticket_actions (roles with manage_tickets) and + manage_ticket_connectors (roles with manage_organization) +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'add_tkt_phase4_001' +down_revision = 'add_invest_glassbox_001' +branch_labels = None +depends_on = None + +NEW_PERMISSIONS = { + 'approve_ticket_actions': ('Can approve AI-proposed ticket resolutions', 'manage_tickets'), + 'manage_ticket_connectors': ('Can manage ticket investigation connectors', 'manage_organization'), +} + + +def upgrade() -> None: + op.create_table( + 'ticket_db_connectors', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('name', sa.String(length=200), nullable=False), + sa.Column('engine', sa.String(), nullable=False), + sa.Column('host', sa.String(length=500), nullable=False), + sa.Column('port', sa.Integer(), nullable=False), + sa.Column('database', sa.String(length=200), nullable=False), + sa.Column('username', sa.String(length=200), nullable=False), + sa.Column('encrypted_password', sa.Text(), nullable=False), + sa.Column('enabled', sa.Boolean(), server_default=sa.text('true'), nullable=False), + sa.Column('allowed_tables', postgresql.JSON(astext_type=sa.Text()), nullable=True), + sa.Column('masked_columns', postgresql.JSON(astext_type=sa.Text()), nullable=True), + sa.Column('max_rows', sa.Integer(), server_default='100', nullable=False), + sa.Column('statement_timeout_ms', sa.Integer(), server_default='5000', nullable=False), + sa.Column('last_test_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_test_ok', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_ticket_db_connectors_organization_id'), 'ticket_db_connectors', ['organization_id']) + + op.create_table( + 'db_connector_audit_logs', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('connector_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('run_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('raw_sql', sa.Text(), nullable=False), + sa.Column('validated_sql', sa.Text(), nullable=True), + sa.Column('outcome', sa.String(), nullable=False), + sa.Column('block_reason', sa.Text(), nullable=True), + sa.Column('row_count', sa.Integer(), nullable=True), + sa.Column('duration_ms', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['connector_id'], ['ticket_db_connectors.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['run_id'], ['investigation_runs.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_db_connector_audit_logs_connector_id'), 'db_connector_audit_logs', ['connector_id']) + op.create_index(op.f('ix_db_connector_audit_logs_organization_id'), 'db_connector_audit_logs', ['organization_id']) + op.create_index('ix_db_connector_audit_logs_created', 'db_connector_audit_logs', ['organization_id', 'created_at']) + + op.create_table( + 'ticket_proposals', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('ticket_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('run_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('summary', sa.Text(), nullable=False), + sa.Column('customer_message', sa.Text(), nullable=True), + sa.Column('confidence', sa.Float(), nullable=True), + sa.Column('status', sa.String(), nullable=False), + sa.Column('decided_by_user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('decided_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('reject_reason', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['ticket_id'], ['tickets.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['run_id'], ['investigation_runs.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['decided_by_user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_ticket_proposals_ticket_id'), 'ticket_proposals', ['ticket_id']) + op.create_index(op.f('ix_ticket_proposals_organization_id'), 'ticket_proposals', ['organization_id']) + + op.add_column( + 'organization_ticket_settings', + sa.Column('alert_webhook_secret', sa.String(length=64), nullable=True), + ) + + # Seed the new permissions onto roles holding the anchor permission + # (new orgs seed via Permission.default_permissions()). + for name, (description, anchor) in NEW_PERMISSIONS.items(): + op.execute(f""" + INSERT INTO permissions (name, description) + SELECT '{name}', '{description}' + WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE name = '{name}') + """) + op.execute(f""" + INSERT INTO role_permissions (role_id, permission_id) + SELECT rp.role_id, p_new.id + FROM role_permissions rp + JOIN permissions p_old ON p_old.id = rp.permission_id AND p_old.name = '{anchor}' + CROSS JOIN permissions p_new + WHERE p_new.name = '{name}' + AND NOT EXISTS ( + SELECT 1 FROM role_permissions rp2 + WHERE rp2.role_id = rp.role_id AND rp2.permission_id = p_new.id + ) + """) + + +def downgrade() -> None: + names = "', '".join(NEW_PERMISSIONS) + op.execute(f""" + DELETE FROM role_permissions + WHERE permission_id IN (SELECT id FROM permissions WHERE name IN ('{names}')) + """) + op.execute(f"DELETE FROM permissions WHERE name IN ('{names}')") + op.drop_column('organization_ticket_settings', 'alert_webhook_secret') + op.drop_index(op.f('ix_ticket_proposals_organization_id'), table_name='ticket_proposals') + op.drop_index(op.f('ix_ticket_proposals_ticket_id'), table_name='ticket_proposals') + op.drop_table('ticket_proposals') + op.drop_index('ix_db_connector_audit_logs_created', table_name='db_connector_audit_logs') + op.drop_index(op.f('ix_db_connector_audit_logs_organization_id'), table_name='db_connector_audit_logs') + op.drop_index(op.f('ix_db_connector_audit_logs_connector_id'), table_name='db_connector_audit_logs') + op.drop_table('db_connector_audit_logs') + op.drop_index(op.f('ix_ticket_db_connectors_organization_id'), table_name='ticket_db_connectors') + op.drop_table('ticket_db_connectors') diff --git a/backend/alembic/versions/add_widget_apps_001.py b/backend/alembic/versions/add_widget_apps_001.py new file mode 100644 index 0000000..7f613a8 --- /dev/null +++ b/backend/alembic/versions/add_widget_apps_001.py @@ -0,0 +1,67 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add widget_apps table + +Revision ID: add_widget_apps_001 +Revises: 2381319791e8 +Create Date: 2026-01-13 00:00:00.000000 + +""" +from typing import Sequence, Union +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID + + +# revision identifiers, used by Alembic. +revision: str = 'add_widget_apps_001' +down_revision: Union[str, Sequence[str]] = '2381319791e8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Create widget_apps table + op.create_table( + 'widget_apps', + sa.Column('id', UUID(as_uuid=True), primary_key=True, nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('organization_id', UUID(as_uuid=True), nullable=False), + sa.Column('api_key_hash', sa.String(), nullable=False), + sa.Column('created_by', UUID(as_uuid=True), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true'), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.func.now(), onupdate=sa.func.now(), nullable=False), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id']), + sa.ForeignKeyConstraint(['created_by'], ['users.id']), + ) + + # Create indexes + op.create_index('ix_widget_apps_id', 'widget_apps', ['id']) + op.create_index('ix_widget_apps_organization_id', 'widget_apps', ['organization_id']) + op.create_index('ix_widget_apps_is_active', 'widget_apps', ['is_active']) + + +def downgrade() -> None: + # Drop indexes + op.drop_index('ix_widget_apps_is_active', table_name='widget_apps') + op.drop_index('ix_widget_apps_organization_id', table_name='widget_apps') + op.drop_index('ix_widget_apps_id', table_name='widget_apps') + + # Drop table + op.drop_table('widget_apps') diff --git a/backend/alembic/versions/agent_quick_replies_001.py b/backend/alembic/versions/agent_quick_replies_001.py new file mode 100644 index 0000000..35b9c1c --- /dev/null +++ b/backend/alembic/versions/agent_quick_replies_001.py @@ -0,0 +1,30 @@ +"""Let an agent offer reply buttons on messaging channels + +Off by default: an agent shows the AI's choices as tappable buttons on +WhatsApp, Messenger, Instagram and Telegram only once someone turns it on. + +Revision ID: agent_quick_replies_001 +Revises: chat_channel_status_001 +Create Date: 2026-10-11 +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +# Keep revision ids under 32 characters: alembic_version.version_num is +# varchar(32). +revision: str = 'agent_quick_replies_001' +down_revision: Union[str, None] = 'chat_channel_status_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column('agents', sa.Column('channel_quick_replies', sa.Boolean(), nullable=False, + server_default=sa.false()), if_not_exists=True) + + +def downgrade() -> None: + op.drop_column('agents', 'channel_quick_replies', if_exists=True) diff --git a/backend/alembic/versions/auth_sessions_001.py b/backend/alembic/versions/auth_sessions_001.py new file mode 100644 index 0000000..deb0576 --- /dev/null +++ b/backend/alembic/versions/auth_sessions_001.py @@ -0,0 +1,39 @@ +"""auth_sessions table, so sign-out and password changes revoke JWTs + +Revision ID: auth_sessions_001 +Revises: agent_quick_replies_001 +Create Date: 2026-10-12 + +Tokens issued before this revision carry no session id and stay valid until +they expire, so the deploy signs nobody out. +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'auth_sessions_001' +down_revision: Union[str, None] = 'agent_quick_replies_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + 'auth_sessions', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), + server_default=sa.text('now()'), nullable=True), + sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index('ix_auth_sessions_user_id', 'auth_sessions', ['user_id']) + + +def downgrade() -> None: + op.drop_index('ix_auth_sessions_user_id', table_name='auth_sessions') + op.drop_table('auth_sessions') diff --git a/backend/alembic/versions/b2c3d4e5f6a7_add_customer_meta_data.py b/backend/alembic/versions/b2c3d4e5f6a7_add_customer_meta_data.py new file mode 100644 index 0000000..3e78b21 --- /dev/null +++ b/backend/alembic/versions/b2c3d4e5f6a7_add_customer_meta_data.py @@ -0,0 +1,50 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add meta_data column to customers + +Adds: +- `meta_data` (JSON): arbitrary integrator-supplied fields (e.g. student_name, + center_name) passed via POST /generate-token's `custom_data`, surfaced to agents + in the chat inbox. + +Revision ID: b2c3d4e5f6a7 +Revises: f4a5b6c7d8e9 +Create Date: 2026-07-06 00:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b2c3d4e5f6a7' +down_revision: Union[str, None] = 'f4a5b6c7d8e9' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.add_column( + 'customers', + sa.Column('meta_data', sa.JSON(), nullable=True) + ) + + +def downgrade() -> None: + op.drop_column('customers', 'meta_data') diff --git a/backend/alembic/versions/b365b8447598_initial_version.py b/backend/alembic/versions/b365b8447598_initial_version.py new file mode 100644 index 0000000..4f212a7 --- /dev/null +++ b/backend/alembic/versions/b365b8447598_initial_version.py @@ -0,0 +1,310 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b365b8447598' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('organizations', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('domain', sa.String(length=100), nullable=False), + sa.Column('timezone', sa.String(length=50), nullable=False), + sa.Column('business_hours', sa.JSON(), nullable=True), + sa.Column('settings', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('domain') + ) + op.create_index(op.f('ix_organizations_id'), 'organizations', ['id'], unique=False) + op.create_table('permissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_index(op.f('ix_permissions_id'), 'permissions', ['id'], unique=False) + op.create_table('agents', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('display_name', sa.String(length=100), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('agent_type', sa.Enum('CUSTOMER_SUPPORT', 'SALES', 'TECH_SUPPORT', 'GENERAL', 'CUSTOM', name='agenttype'), nullable=False), + sa.Column('instructions', sa.Text(), nullable=False), + sa.Column('tools', sa.Text(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=True), + sa.Column('transfer_to_human', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('ai_configs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('model_type', sa.Enum('OPENAI', 'ANTHROPIC', 'DEEPSEEK', 'GOOGLE', 'GOOGLEVERTEX', 'GROQ', 'MISTRAL', 'HUGGINGFACE', 'OLLAMA', 'XAI', name='aimodeltype'), nullable=False), + sa.Column('model_name', sa.String(), nullable=False), + sa.Column('encrypted_api_key', sa.String(), nullable=False), + sa.Column('settings', sa.JSON(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_ai_configs_id'), 'ai_configs', ['id'], unique=False) + op.create_table('customers', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('email', sa.String(), nullable=False), + sa.Column('full_name', sa.String(), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email', 'organization_id', name='uix_customer_email_org') + ) + op.create_table('groups', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('knowledge', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('source', sa.String(), nullable=False), + sa.Column('source_type', sa.Enum('DATABASE', 'FILE', 'API', 'WEBSITE', 'CUSTOM', name='sourcetype'), nullable=False), + sa.Column('schema', sa.String(), nullable=True), + sa.Column('table_name', sa.String(), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_knowledge_id'), 'knowledge', ['id'], unique=False) + op.create_table('roles', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('description', sa.String(), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False) + op.create_table('agent_customizations', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=True), + sa.Column('photo_url', sa.String(), nullable=True), + sa.Column('chat_background_color', sa.String(), nullable=True), + sa.Column('chat_bubble_color', sa.String(), nullable=True), + sa.Column('chat_text_color', sa.String(), nullable=True), + sa.Column('icon_url', sa.String(), nullable=True), + sa.Column('icon_color', sa.String(), nullable=True), + sa.Column('accent_color', sa.String(), nullable=True), + sa.Column('font_family', sa.String(), nullable=True), + sa.Column('custom_css', sa.Text(), nullable=True), + sa.Column('customization_metadata', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_agent_customizations_id'), 'agent_customizations', ['id'], unique=False) + op.create_table('agent_usergroup', + sa.Column('agent_id', sa.UUID(), nullable=True), + sa.Column('group_id', sa.UUID(), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='CASCADE') + ) + op.create_table('knowledge_to_agents', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('knowledge_id', sa.Integer(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=False), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['knowledge_id'], ['knowledge.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_knowledge_to_agents_id'), 'knowledge_to_agents', ['id'], unique=False) + op.create_table('role_permissions', + sa.Column('role_id', sa.Integer(), nullable=True), + sa.Column('permission_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ) + ) + op.create_table('users', + sa.Column('id', sa.UUID(), nullable=False), + sa.Column('email', sa.String(), nullable=True), + sa.Column('full_name', sa.String(), nullable=True), + sa.Column('hashed_password', sa.String(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('profile_pic', sa.String(), nullable=True), + sa.Column('is_online', sa.Boolean(), nullable=True), + sa.Column('last_seen', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=True), + sa.Column('role_id', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('fcm_token_web', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_table('widgets', + sa.Column('id', sa.String(), nullable=False), + sa.Column('name', sa.String(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('knowledge_queue', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('agent_id', sa.UUID(), nullable=True), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('source_type', sa.String(), nullable=False), + sa.Column('source', sa.String(), nullable=False), + sa.Column('status', sa.String(), nullable=True), + sa.Column('error', sa.String(), nullable=True), + sa.Column('queue_metadata', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_knowledge_queue_id'), 'knowledge_queue', ['id'], unique=False) + op.create_table('notifications', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('type', sa.Enum('KNOWLEDGE_PROCESSED', 'KNOWLEDGE_FAILED', 'SYSTEM', 'CHAT', name='notificationtype'), nullable=False), + sa.Column('title', sa.String(), nullable=False), + sa.Column('message', sa.String(), nullable=False), + sa.Column('notification_metadata', sa.JSON(), nullable=True), + sa.Column('is_read', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_notifications_id'), 'notifications', ['id'], unique=False) + op.create_table('session_to_agents', + sa.Column('session_id', sa.UUID(), nullable=False), + sa.Column('user_id', sa.UUID(), nullable=True), + sa.Column('agent_id', sa.UUID(), nullable=True), + sa.Column('customer_id', sa.UUID(), nullable=False), + sa.Column('group_id', sa.UUID(), nullable=True), + sa.Column('organization_id', sa.UUID(), nullable=False), + sa.Column('status', sa.Enum('OPEN', 'CLOSED', 'TRANSFERRED', name='sessionstatus'), nullable=False), + sa.Column('assigned_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('closed_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('transfer_reason', sa.String(), nullable=True), + sa.Column('transfer_description', sa.String(), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('session_id') + ) + op.create_table('user_groups', + sa.Column('user_id', sa.UUID(), nullable=False), + sa.Column('group_id', sa.UUID(), nullable=False), + sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('user_id', 'group_id') + ) + op.create_table('chat_history', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', sa.UUID(), nullable=True), + sa.Column('user_id', sa.UUID(), nullable=True), + sa.Column('customer_id', sa.UUID(), nullable=True), + sa.Column('agent_id', sa.UUID(), nullable=True), + sa.Column('session_id', sa.UUID(), nullable=True), + sa.Column('message', sa.String(), nullable=False), + sa.Column('message_type', sa.String(), nullable=False), + sa.Column('attributes', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_chat_history_id'), 'chat_history', ['id'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_chat_history_id'), table_name='chat_history') + op.drop_table('chat_history') + op.drop_table('user_groups') + op.drop_table('session_to_agents') + op.drop_index(op.f('ix_notifications_id'), table_name='notifications') + op.drop_table('notifications') + op.drop_index(op.f('ix_knowledge_queue_id'), table_name='knowledge_queue') + op.drop_table('knowledge_queue') + op.drop_table('widgets') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') + op.drop_table('role_permissions') + op.drop_index(op.f('ix_knowledge_to_agents_id'), table_name='knowledge_to_agents') + op.drop_table('knowledge_to_agents') + op.drop_table('agent_usergroup') + op.drop_index(op.f('ix_agent_customizations_id'), table_name='agent_customizations') + op.drop_table('agent_customizations') + op.drop_index(op.f('ix_roles_id'), table_name='roles') + op.drop_table('roles') + op.drop_index(op.f('ix_knowledge_id'), table_name='knowledge') + op.drop_table('knowledge') + op.drop_table('groups') + op.drop_table('customers') + op.drop_index(op.f('ix_ai_configs_id'), table_name='ai_configs') + op.drop_table('ai_configs') + op.drop_table('agents') + op.drop_index(op.f('ix_permissions_id'), table_name='permissions') + op.drop_table('permissions') + op.drop_index(op.f('ix_organizations_id'), table_name='organizations') + op.drop_table('organizations') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/b7c4e91a2d38_add_agent_queue_and_people_permissions.py b/backend/alembic/versions/b7c4e91a2d38_add_agent_queue_and_people_permissions.py new file mode 100644 index 0000000..dbe2da6 --- /dev/null +++ b/backend/alembic/versions/b7c4e91a2d38_add_agent_queue_and_people_permissions.py @@ -0,0 +1,151 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add view_unassigned_chats + view_people permissions + +Revision ID: b7c4e91a2d38 +Revises: add_customer_phone_001 +Create Date: 2026-07-18 + +Agents could previously only see chats already assigned to them or their group, +so the AI queue was invisible and "Take over" was unreachable. Two new +permissions rather than widening view_assigned_chats, so that granting the +queue never also exposes another agent's conversations: + +- view_unassigned_chats: the unclaimed queue (session_to_agents.user_id IS NULL) +- view_people: read-only people directory, previously gated on view_all_chats + +Backfill targets roles by CAPABILITY, not by name, so every existing +organization is covered including ones whose agent role was renamed or +hand-built: + +- any role that can view chats at all (assigned, all, or super_admin) gains + view_unassigned_chats +- the same roles plus manage_all_chats gain view_people + +This deliberately widens existing roles: without it the feature would appear +broken for every deployment created before this release, and an admin who +wants an agent kept out can uncheck the permission in Roles. Re-runnable — +every insert is guarded by NOT EXISTS. + +Also repairs the seeding bug where BOTH "Admin" and "Agent" were created with +is_default=true, which made RoleRepository.get_default_role() return whichever +row the database happened to yield first — an invited user could land on Admin. +Nothing in the API enforces a single default, so the repair is data-only. + +The repair CLEARS Admin, which is what leaves Agent as the default. Setting +is_default=false on Agent instead would do the opposite of what is wanted: it +would leave Admin as the sole default and hand every newly invited user full +permissions. +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'b7c4e91a2d38' +down_revision = 'add_customer_phone_001' +branch_labels = None +depends_on = None + + +NEW_PERMISSIONS = ( + ('view_unassigned_chats', 'Can view unassigned AI chats'), + ('view_people', 'Can view the people directory'), +) + +# Which existing roles receive each new permission, expressed as the permissions +# a role must already hold. Capability, not role name: an org that renamed its +# agent role, or built its own, is still covered. +BACKFILL = ( + ('view_unassigned_chats', ('view_assigned_chats', 'view_all_chats', 'super_admin')), + ('view_people', ('view_assigned_chats', 'view_all_chats', 'manage_all_chats', 'super_admin')), +) + + +def _sql_list(values) -> str: + return ", ".join(f"'{value}'" for value in values) + + +def upgrade() -> None: + for name, description in NEW_PERMISSIONS: + op.execute( + f""" + INSERT INTO permissions (name, description) + SELECT '{name}', '{description}' + WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE name = '{name}') + """ + ) + + for new_permission, held_by in BACKFILL: + op.execute( + f""" + INSERT INTO role_permissions (role_id, permission_id) + SELECT DISTINCT r.id, target.id + FROM roles r + JOIN role_permissions rp ON rp.role_id = r.id + JOIN permissions held ON held.id = rp.permission_id + CROSS JOIN permissions target + WHERE target.name = '{new_permission}' + AND held.name IN ({_sql_list(held_by)}) + AND NOT EXISTS ( + SELECT 1 FROM role_permissions existing + WHERE existing.role_id = r.id + AND existing.permission_id = target.id + ) + """ + ) + + # One default role per organization. Clearing Admin is what PROMOTES Agent + # to sole default — the opposite spelling (clearing Agent) would leave + # Admin default and make every invited user an admin. + # + # The COUNT(*) > 1 guard is load-bearing: it means an org whose only + # default is Admin keeps it, rather than being left with no default role + # at all. An org that renamed its Admin role keeps both defaults — the + # pre-existing ambiguity, not a regression. + op.execute( + """ + UPDATE roles SET is_default = false + WHERE name = 'Admin' + AND is_default = true + AND organization_id IN ( + SELECT organization_id FROM roles + WHERE is_default = true + GROUP BY organization_id + HAVING COUNT(*) > 1 + ) + """ + ) + + +def downgrade() -> None: + # Drop the grants first (FK), then the permissions themselves. is_default is + # not restored: the pre-migration state was ambiguous by definition. + op.execute( + """ + DELETE FROM role_permissions + WHERE permission_id IN ( + SELECT id FROM permissions + WHERE name IN ('view_unassigned_chats', 'view_people') + ) + """ + ) + op.execute( + """ + DELETE FROM permissions + WHERE name IN ('view_unassigned_chats', 'view_people') + """ + ) diff --git a/backend/alembic/versions/bdaa7a5d6109_removed_node_specific_fields.py b/backend/alembic/versions/bdaa7a5d6109_removed_node_specific_fields.py new file mode 100644 index 0000000..fda661e --- /dev/null +++ b/backend/alembic/versions/bdaa7a5d6109_removed_node_specific_fields.py @@ -0,0 +1,72 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""removed node specific fields + +Revision ID: bdaa7a5d6109 +Revises: dd0e5ceefe4b +Create Date: 2025-07-23 12:06:47.901308 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = 'bdaa7a5d6109' +down_revision: Union[str, None] = 'dd0e5ceefe4b' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.drop_constraint('workflow_nodes_model_id_fkey', 'workflow_nodes', type_='foreignkey') + op.drop_column('workflow_nodes', 'action_type') + op.drop_column('workflow_nodes', 'form_fields') + op.drop_column('workflow_nodes', 'model_id') + op.drop_column('workflow_nodes', 'system_prompt') + op.drop_column('workflow_nodes', 'condition_expression') + op.drop_column('workflow_nodes', 'wait_until_condition') + op.drop_column('workflow_nodes', 'action_config') + op.drop_column('workflow_nodes', 'temperature') + op.drop_column('workflow_nodes', 'wait_duration') + op.drop_column('workflow_nodes', 'transfer_rules') + op.drop_column('workflow_nodes', 'landing_page_content') + op.drop_column('workflow_nodes', 'landing_page_heading') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('workflow_nodes', sa.Column('landing_page_heading', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('landing_page_content', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('transfer_rules', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('wait_duration', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('temperature', sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('action_config', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('wait_until_condition', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('condition_expression', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('system_prompt', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('model_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('form_fields', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True)) + op.add_column('workflow_nodes', sa.Column('action_type', sa.VARCHAR(), autoincrement=False, nullable=True)) + op.create_foreign_key('workflow_nodes_model_id_fkey', 'workflow_nodes', 'ai_configs', ['model_id'], ['id']) + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/c1d2e3f4a5b6_add_premium_chat_styles_and_show_citations.py b/backend/alembic/versions/c1d2e3f4a5b6_add_premium_chat_styles_and_show_citations.py new file mode 100644 index 0000000..5918491 --- /dev/null +++ b/backend/alembic/versions/c1d2e3f4a5b6_add_premium_chat_styles_and_show_citations.py @@ -0,0 +1,71 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add premium chat styles and show_citations to agent_customizations + +Adds the new GLASS/TERMINAL/PLAYFUL/CALM_MINT values to the `chatstyle` enum and a +`show_citations` boolean column. + +Revision ID: c1d2e3f4a5b6 +Revises: add_sentiment_analysis_001 +Create Date: 2026-06-26 00:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'c1d2e3f4a5b6' +down_revision: Union[str, None] = 'add_sentiment_analysis_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +NEW_CHAT_STYLES = ("GLASS", "TERMINAL", "PLAYFUL", "CALM_MINT") + + +def upgrade() -> None: + bind = op.get_bind() + + # Add new values to the native Postgres `chatstyle` enum. ALTER TYPE ... ADD VALUE + # cannot run inside a transaction block, so use an autocommit block. + if bind.dialect.name == "postgresql": + with op.get_context().autocommit_block(): + for value in NEW_CHAT_STYLES: + op.execute(f"ALTER TYPE chatstyle ADD VALUE IF NOT EXISTS '{value}'") + + # Add show_citations column (default OFF for now — citations need more work). + # Use IF NOT EXISTS on Postgres so the migration is safe to run on a DB where the + # column was already added out-of-band. + if bind.dialect.name == "postgresql": + op.execute( + "ALTER TABLE agent_customizations " + "ADD COLUMN IF NOT EXISTS show_citations boolean NOT NULL DEFAULT false" + ) + else: + op.add_column( + 'agent_customizations', + sa.Column('show_citations', sa.Boolean(), server_default=sa.false(), nullable=False), + ) + + +def downgrade() -> None: + op.drop_column('agent_customizations', 'show_citations') + # Postgres cannot cleanly drop individual enum values; the added chatstyle + # values are intentionally left in place. diff --git a/backend/alembic/versions/c3d4e5f6a7b8_add_lead_capture.py b/backend/alembic/versions/c3d4e5f6a7b8_add_lead_capture.py new file mode 100644 index 0000000..8b15aad --- /dev/null +++ b/backend/alembic/versions/c3d4e5f6a7b8_add_lead_capture.py @@ -0,0 +1,123 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add lead capture (configs, responses, customer lead stage) + +Revision ID: c3d4e5f6a7b8 +Revises: b2c3d4e5f6a7 +Create Date: 2026-07-07 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + + +# revision identifiers, used by Alembic. +revision: str = 'c3d4e5f6a7b8' +down_revision: Union[str, None] = 'b2c3d4e5f6a7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +# Enum member NAMES are what SQLAlchemy stores (matches agenttype/sessionstatus convention). +# create_type=False + an explicit .create(checkfirst=True) so create_table/add_column +# reference the type without emitting a second (empty) CREATE TYPE. +assign_col = postgresql.ENUM('NONE', 'SALES_TEAM', 'SPECIFIC_PERSON', 'ROUND_ROBIN', name='leadassignmentmode', create_type=False) +crm_col = postgresql.ENUM('NONE', 'HUBSPOT', 'SALESFORCE', name='crmsynctarget', create_type=False) +stage_col = postgresql.ENUM('VISITOR', 'LEAD', 'CUSTOMER', name='leadstage', create_type=False) + + +def upgrade() -> None: + bind = op.get_bind() + + # Create enum types up front (idempotent) so create_table / add_column can reference + # them without re-emitting CREATE TYPE. + assign_col.create(bind, checkfirst=True) + crm_col.create(bind, checkfirst=True) + stage_col.create(bind, checkfirst=True) + + # ---- lead_capture_configs (1:1 with agents) ---- + op.create_table( + 'lead_capture_configs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('enabled', sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column('require_consent', sa.Boolean(), nullable=False, server_default=sa.true()), + sa.Column('guidance', sa.Text(), nullable=True), + sa.Column('fields', sa.JSON(), nullable=True), + sa.Column('assignment_mode', assign_col, nullable=False, server_default='NONE'), + sa.Column('assignment_target_user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('crm_sync_target', crm_col, nullable=False, server_default='NONE'), + sa.Column('slack_notify_enabled', sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['assignment_target_user_id'], ['users.id']), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('agent_id', name='uix_lead_capture_config_agent'), + ) + op.create_index(op.f('ix_lead_capture_configs_id'), 'lead_capture_configs', ['id'], unique=False) + + # ---- lead_capture_responses (one row per capture attempt) ---- + op.create_table( + 'lead_capture_responses', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('customer_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('session_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('field_values', sa.JSON(), nullable=True), + sa.Column('summary', sa.Text(), nullable=True), + sa.Column('consent', sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column('qualified', sa.Boolean(), nullable=False, server_default=sa.false()), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.func.now(), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id']), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['session_id'], ['session_to_agents.session_id']), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_lead_capture_responses_customer_id'), 'lead_capture_responses', ['customer_id'], unique=False) + op.create_index(op.f('ix_lead_capture_responses_agent_id'), 'lead_capture_responses', ['agent_id'], unique=False) + + # ---- customers: lead lifecycle columns ---- + # Add lead_stage nullable first, backfill, then enforce NOT NULL (template pattern). + op.add_column('customers', sa.Column('lead_stage', stage_col, nullable=True)) + op.add_column('customers', sa.Column('lead_source', sa.JSON(), nullable=True)) + op.add_column('customers', sa.Column('lead_qualified_at', sa.DateTime(timezone=True), nullable=True)) + op.execute("UPDATE customers SET lead_stage = 'VISITOR' WHERE lead_stage IS NULL") + op.alter_column('customers', 'lead_stage', nullable=False, server_default='VISITOR') + + +def downgrade() -> None: + op.drop_column('customers', 'lead_qualified_at') + op.drop_column('customers', 'lead_source') + op.drop_column('customers', 'lead_stage') + + op.drop_index(op.f('ix_lead_capture_responses_agent_id'), table_name='lead_capture_responses') + op.drop_index(op.f('ix_lead_capture_responses_customer_id'), table_name='lead_capture_responses') + op.drop_table('lead_capture_responses') + + op.drop_index(op.f('ix_lead_capture_configs_id'), table_name='lead_capture_configs') + op.drop_table('lead_capture_configs') + + bind = op.get_bind() + stage_col.drop(bind, checkfirst=True) + crm_col.drop(bind, checkfirst=True) + assign_col.drop(bind, checkfirst=True) diff --git a/backend/alembic/versions/chat_channel_status_001.py b/backend/alembic/versions/chat_channel_status_001.py new file mode 100644 index 0000000..0142129 --- /dev/null +++ b/backend/alembic/versions/chat_channel_status_001.py @@ -0,0 +1,64 @@ +"""Track channel delivery receipts on chat_history + +WhatsApp reports sent / delivered / read / failed for every message we send, +keyed by its platform message id (wamid). Storing that id on the row is what +lets a receipt find its message; channel_status holds the latest receipt. + +channel_status is deliberately separate from attributes.delivery_status: the +inbox reads any delivery_status as "not delivered", so a 'read' written there +would show as a failure. + +Revision ID: chat_channel_status_001 +Revises: idx_kq_org_status_001 +Create Date: 2026-10-11 +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +# Keep revision ids under 32 characters: alembic_version.version_num is +# varchar(32). +revision: str = 'chat_channel_status_001' +down_revision: Union[str, None] = 'idx_kq_org_status_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +INDEX_NAME = 'ix_chat_history_org_external_message' + + +def upgrade() -> None: + # if_not_exists throughout: the columns commit before the index builds + # (CONCURRENTLY can't share their transaction), so a start that dies + # mid-build must be able to run this again. + op.add_column('chat_history', sa.Column('external_message_id', sa.String(), nullable=True), + if_not_exists=True) + op.add_column('chat_history', sa.Column('channel_status', sa.String(16), nullable=True), + if_not_exists=True) + op.add_column('chat_history', sa.Column('channel_status_at', sa.TIMESTAMP(timezone=True), + nullable=True), + if_not_exists=True) + # Partial: only channel messages carry an id, so widget rows stay out of it. + # Built CONCURRENTLY so a large chat_history keeps taking writes while it + # builds — migrations run on backend start. That can't run in a + # transaction, hence the autocommit block. A build that fails part-way + # leaves an INVALID index that IF NOT EXISTS would then skip: if that + # happens, DROP INDEX ix_chat_history_org_external_message and restart. + with op.get_context().autocommit_block(): + op.create_index( + INDEX_NAME, 'chat_history', ['organization_id', 'external_message_id'], + unique=False, + postgresql_where=sa.text('external_message_id IS NOT NULL'), + postgresql_concurrently=True, + if_not_exists=True, + ) + + +def downgrade() -> None: + with op.get_context().autocommit_block(): + op.drop_index(INDEX_NAME, table_name='chat_history', + postgresql_concurrently=True, if_exists=True) + op.drop_column('chat_history', 'channel_status_at', if_exists=True) + op.drop_column('chat_history', 'channel_status', if_exists=True) + op.drop_column('chat_history', 'external_message_id', if_exists=True) diff --git a/backend/alembic/versions/crm_sync_varchar_001_crm_target_varchar.py b/backend/alembic/versions/crm_sync_varchar_001_crm_target_varchar.py new file mode 100644 index 0000000..0587eac --- /dev/null +++ b/backend/alembic/versions/crm_sync_varchar_001_crm_target_varchar.py @@ -0,0 +1,62 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""convert crm_sync_target to varchar so new CRMs need no migration + +Revision ID: crm_sync_varchar_001 +Revises: merge_tkt_csat_001 +Create Date: 2026-07-22 12:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = 'crm_sync_varchar_001' +down_revision: Union[str, None] = 'merge_tkt_csat_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +# The original crmsynctarget enum stored member NAMES; the varchar column stores +# lowercase enum VALUES ('none', 'hubspot', ...) to match ChannelType convention. +_ORIGINAL_ENUM_LABELS = ('NONE', 'HUBSPOT', 'SALESFORCE') + + +def upgrade() -> None: + op.execute("ALTER TABLE lead_capture_configs ALTER COLUMN crm_sync_target DROP DEFAULT") + op.execute( + "ALTER TABLE lead_capture_configs " + "ALTER COLUMN crm_sync_target TYPE varchar(32) " + "USING lower(crm_sync_target::text)" + ) + op.execute("ALTER TABLE lead_capture_configs ALTER COLUMN crm_sync_target SET DEFAULT 'none'") + op.execute("DROP TYPE IF EXISTS crmsynctarget") + + +def downgrade() -> None: + labels = ", ".join(f"'{label}'" for label in _ORIGINAL_ENUM_LABELS) + op.execute(f"CREATE TYPE crmsynctarget AS ENUM ({labels})") + op.execute("ALTER TABLE lead_capture_configs ALTER COLUMN crm_sync_target DROP DEFAULT") + # Values unknown to the original enum (e.g. 'pipedrive') fold to 'NONE'. + op.execute( + "ALTER TABLE lead_capture_configs " + "ALTER COLUMN crm_sync_target TYPE crmsynctarget " + f"USING (CASE WHEN upper(crm_sync_target) IN ({labels}) " + "THEN upper(crm_sync_target) ELSE 'NONE' END)::crmsynctarget" + ) + op.execute("ALTER TABLE lead_capture_configs ALTER COLUMN crm_sync_target SET DEFAULT 'NONE'") diff --git a/backend/alembic/versions/d2e3f4a5b6c7_add_contact_collection_columns.py b/backend/alembic/versions/d2e3f4a5b6c7_add_contact_collection_columns.py new file mode 100644 index 0000000..8e3a551 --- /dev/null +++ b/backend/alembic/versions/d2e3f4a5b6c7_add_contact_collection_columns.py @@ -0,0 +1,67 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add contact-collection columns (collect_email + handoff_collect_email/name) + +Adds: + - agent_customizations.collect_email (default false) -> optional pre-chat email gate + - agents.handoff_collect_email (default true) -> ask email on human handoff + - agents.handoff_collect_name (default true) -> ask name on human handoff + +Revision ID: d2e3f4a5b6c7 +Revises: c1d2e3f4a5b6 +Create Date: 2026-06-29 00:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'd2e3f4a5b6c7' +down_revision: Union[str, None] = 'c1d2e3f4a5b6' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + bind = op.get_bind() + if bind.dialect.name == "postgresql": + # IF NOT EXISTS so the migration is safe on a DB where columns were added out-of-band + op.execute( + "ALTER TABLE agent_customizations " + "ADD COLUMN IF NOT EXISTS collect_email boolean NOT NULL DEFAULT false" + ) + op.execute( + "ALTER TABLE agents " + "ADD COLUMN IF NOT EXISTS handoff_collect_email boolean NOT NULL DEFAULT true" + ) + op.execute( + "ALTER TABLE agents " + "ADD COLUMN IF NOT EXISTS handoff_collect_name boolean NOT NULL DEFAULT true" + ) + else: + op.add_column('agent_customizations', sa.Column('collect_email', sa.Boolean(), server_default=sa.false(), nullable=False)) + op.add_column('agents', sa.Column('handoff_collect_email', sa.Boolean(), server_default=sa.true(), nullable=False)) + op.add_column('agents', sa.Column('handoff_collect_name', sa.Boolean(), server_default=sa.true(), nullable=False)) + + +def downgrade() -> None: + op.drop_column('agents', 'handoff_collect_name') + op.drop_column('agents', 'handoff_collect_email') + op.drop_column('agent_customizations', 'collect_email') diff --git a/backend/alembic/versions/d4e5f6a7b8c9_add_customer_merge.py b/backend/alembic/versions/d4e5f6a7b8c9_add_customer_merge.py new file mode 100644 index 0000000..7b0c786 --- /dev/null +++ b/backend/alembic/versions/d4e5f6a7b8c9_add_customer_merge.py @@ -0,0 +1,37 @@ +"""add customers.merged_into_customer_id for visitor→customer merge + +Revision ID: d4e5f6a7b8c9 +Revises: c3d4e5f6a7b8 +Create Date: 2026-07-08 + +When an anonymous widget visitor shares an email that already belongs to an +existing customer in the org, the visitor row is merged into that customer. +The old row keeps a pointer so stale device tokens resolve to the target. +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'd4e5f6a7b8c9' +down_revision = 'c3d4e5f6a7b8' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + 'customers', + sa.Column('merged_into_customer_id', postgresql.UUID(as_uuid=True), nullable=True), + ) + op.create_foreign_key( + 'fk_customers_merged_into', 'customers', 'customers', + ['merged_into_customer_id'], ['id'], + ) + op.create_index('ix_customers_merged_into', 'customers', ['merged_into_customer_id']) + + +def downgrade() -> None: + op.drop_index('ix_customers_merged_into', table_name='customers') + op.drop_constraint('fk_customers_merged_into', 'customers', type_='foreignkey') + op.drop_column('customers', 'merged_into_customer_id') diff --git a/backend/alembic/versions/d9a4b9902f80_add_attempts_to_otp_table.py b/backend/alembic/versions/d9a4b9902f80_add_attempts_to_otp_table.py new file mode 100644 index 0000000..ef0a854 --- /dev/null +++ b/backend/alembic/versions/d9a4b9902f80_add_attempts_to_otp_table.py @@ -0,0 +1,64 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add_attempts_to_otp_table + +Revision ID: d9a4b9902f80 +Revises: 013174e15242 +Create Date: 2025-10-07 19:24:50.790908 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'd9a4b9902f80' +down_revision: Union[str, None] = '013174e15242' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Check if enterprise schema exists before adding column + # In open-source version, enterprise schema doesn't exist + connection = op.get_bind() + + # Check if enterprise schema exists + schema_exists = connection.execute( + sa.text("SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = 'enterprise')") + ).scalar() + + if schema_exists: + # Add attempts column to otps table in enterprise schema + op.add_column('otps', sa.Column('attempts', sa.Integer(), nullable=False, server_default='0'), schema='enterprise') + + +def downgrade() -> None: + # Check if enterprise schema exists before removing column + # In open-source version, enterprise schema doesn't exist + connection = op.get_bind() + + # Check if enterprise schema exists + schema_exists = connection.execute( + sa.text("SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name = 'enterprise')") + ).scalar() + + if schema_exists: + # Remove attempts column from otps table in enterprise schema + op.drop_column('otps', 'attempts', schema='enterprise') diff --git a/backend/alembic/versions/dc827ab30bd4_add_progress_tracking_to_knowledge_queue.py b/backend/alembic/versions/dc827ab30bd4_add_progress_tracking_to_knowledge_queue.py new file mode 100644 index 0000000..eb8842f --- /dev/null +++ b/backend/alembic/versions/dc827ab30bd4_add_progress_tracking_to_knowledge_queue.py @@ -0,0 +1,52 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add_progress_tracking_to_knowledge_queue + +Revision ID: dc827ab30bd4 +Revises: 621095beb234 +Create Date: 2025-08-05 16:28:47.201062 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'dc827ab30bd4' +down_revision: Union[str, None] = '621095beb234' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add new progress tracking columns to knowledge_queue table + op.add_column('knowledge_queue', sa.Column('processing_stage', sa.String(), nullable=True, server_default='not_started')) + op.add_column('knowledge_queue', sa.Column('progress_percentage', sa.Float(), nullable=True, server_default='0.0')) + op.add_column('knowledge_queue', sa.Column('total_items', sa.Integer(), nullable=True, server_default='0')) + op.add_column('knowledge_queue', sa.Column('processed_items', sa.Integer(), nullable=True, server_default='0')) + op.add_column('knowledge_queue', sa.Column('crawled_urls', sa.JSON(), nullable=True)) + + +def downgrade() -> None: + # Remove the progress tracking columns + op.drop_column('knowledge_queue', 'crawled_urls') + op.drop_column('knowledge_queue', 'processing_stage') + op.drop_column('knowledge_queue', 'progress_percentage') + op.drop_column('knowledge_queue', 'total_items') + op.drop_column('knowledge_queue', 'processed_items') diff --git a/backend/alembic/versions/dd0e5ceefe4b_added_new_node_type.py b/backend/alembic/versions/dd0e5ceefe4b_added_new_node_type.py new file mode 100644 index 0000000..8958952 --- /dev/null +++ b/backend/alembic/versions/dd0e5ceefe4b_added_new_node_type.py @@ -0,0 +1,72 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""added new node type + +Revision ID: dd0e5ceefe4b +Revises: 7fd0447b3d45 +Create Date: 2025-07-18 12:33:17.762285 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'dd0e5ceefe4b' +down_revision: Union[str, None] = '7fd0447b3d45' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + # Update the NodeType enum to include the new 'landing_page' type + op.execute("ALTER TYPE nodetype ADD VALUE IF NOT EXISTS 'LANDING_PAGE'") + + # Add new columns for landing page node type + op.add_column('workflow_nodes', sa.Column('landing_page_heading', sa.Text(), nullable=True)) + op.add_column('workflow_nodes', sa.Column('landing_page_content', sa.Text(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + # First drop the columns that use the enum + op.drop_column('workflow_nodes', 'landing_page_content') + op.drop_column('workflow_nodes', 'landing_page_heading') + + # Create a new enum without the landing_page value + op.execute(""" + CREATE TYPE nodetype_new AS ENUM ( + 'MESSAGE','LLM','CONDITION','FORM','ACTION','HUMAN_TRANSFER','WAIT','END' + ) + """) + + # Update the column type to use the new enum + op.execute("ALTER TABLE workflow_nodes ALTER COLUMN node_type TYPE nodetype_new USING node_type::text::nodetype_new") + + # Drop the old enum + op.execute("DROP TYPE nodetype") + + # Rename the new enum to the original name + op.execute("ALTER TYPE nodetype_new RENAME TO nodetype") + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/drop_slack_tables_001.py b/backend/alembic/versions/drop_slack_tables_001.py new file mode 100644 index 0000000..3b3e149 --- /dev/null +++ b/backend/alembic/versions/drop_slack_tables_001.py @@ -0,0 +1,60 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Drop legacy Slack tables — Slack now runs on the unified channel stack + +Revision ID: drop_slack_tables_001 +Revises: add_channel_tables_001 +Create Date: 2026-07-10 22:00:00.000000 + +The old integration (slack_tokens with plaintext tokens, per-thread +slack_conversations, per-channel agent_slack_configs, workspace storage modes) +is replaced by channel_accounts / channel_conversations / agent_channel_configs. +The old integration had no production users, so no data is migrated. Downgrade +recreates empty tables via the original add_slack_integration_001 shapes. + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'drop_slack_tables_001' +down_revision: Union[str, None] = 'add_channel_tables_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +_TABLES = ('slack_conversations', 'agent_slack_configs', 'slack_workspace_configs', 'slack_tokens') + + +def upgrade() -> None: + for table in _TABLES: + op.execute(f'DROP TABLE IF EXISTS {table} CASCADE') + op.execute('DROP TYPE IF EXISTS storagemode') + + +def downgrade() -> None: + # Recreate empty legacy tables by re-running the original migration's DDL. + # Version scripts aren't importable by module name — load by file path. + import importlib.util + import os + legacy_path = os.path.join(os.path.dirname(__file__), 'add_slack_integration_001.py') + spec = importlib.util.spec_from_file_location('add_slack_integration_001', legacy_path) + legacy = importlib.util.module_from_spec(spec) + spec.loader.exec_module(legacy) + legacy.upgrade() diff --git a/backend/alembic/versions/e252efacd083_agent_createddate_added.py b/backend/alembic/versions/e252efacd083_agent_createddate_added.py new file mode 100644 index 0000000..6ea6eae --- /dev/null +++ b/backend/alembic/versions/e252efacd083_agent_createddate_added.py @@ -0,0 +1,51 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Agent created date added + +Revision ID: e252efacd083 +Revises: 9a01e6b653b2 +Create Date: 2025-07-09 10:30:11.970669 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e252efacd083' +down_revision: Union[str, None] = '9a01e6b653b2' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('agents', sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True)) + op.add_column('agents', sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.drop_column('agents', 'updated_at') + op.drop_column('agents', 'created_at') + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/e3f4a5b6c7d8_add_aurora_chat_style.py b/backend/alembic/versions/e3f4a5b6c7d8_add_aurora_chat_style.py new file mode 100644 index 0000000..6ba7196 --- /dev/null +++ b/backend/alembic/versions/e3f4a5b6c7d8_add_aurora_chat_style.py @@ -0,0 +1,54 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add AURORA chat style to agent_customizations + +Adds the new AURORA value to the `chatstyle` enum (a dark "ask-me-anything" +design with a gradient orb avatar). The orb-vs-photo avatar choice is stored in +the existing `customization_metadata` JSON column, so no schema change is needed +for that. + +Revision ID: e3f4a5b6c7d8 +Revises: d2e3f4a5b6c7 +Create Date: 2026-06-29 00:00:00.000000 + +""" +from typing import Sequence, Union + +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = 'e3f4a5b6c7d8' +down_revision: Union[str, None] = 'd2e3f4a5b6c7' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + bind = op.get_bind() + + # Add the new value to the native Postgres `chatstyle` enum. ALTER TYPE ... + # ADD VALUE cannot run inside a transaction block, so use an autocommit block. + if bind.dialect.name == "postgresql": + with op.get_context().autocommit_block(): + op.execute("ALTER TYPE chatstyle ADD VALUE IF NOT EXISTS 'AURORA'") + + +def downgrade() -> None: + # Postgres cannot cleanly drop individual enum values; the added chatstyle + # value is intentionally left in place. + pass diff --git a/backend/alembic/versions/e5f6a7b8c9d0_add_customer_is_authenticated.py b/backend/alembic/versions/e5f6a7b8c9d0_add_customer_is_authenticated.py new file mode 100644 index 0000000..034dd11 --- /dev/null +++ b/backend/alembic/versions/e5f6a7b8c9d0_add_customer_is_authenticated.py @@ -0,0 +1,32 @@ +"""add customers.is_authenticated to mark integration-authenticated people + +Revision ID: e5f6a7b8c9d0 +Revises: d4e5f6a7b8c9 +Create Date: 2026-07-09 + +People who were identified via POST /generate-token (the embedding app asserted a +known customer_email) are the business's existing customers, not organic leads, and +must be excluded from the People/leads views. This was previously inferred from +meta_data being NULL, but SQLAlchemy stores an empty JSON value as JSON null rather +than SQL NULL, so `meta_data IS NULL` matched nothing and hid every visitor/lead. +Replace that heuristic with an explicit boolean flag. +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = 'e5f6a7b8c9d0' +down_revision = 'd4e5f6a7b8c9' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.add_column( + 'customers', + sa.Column('is_authenticated', sa.Boolean(), nullable=False, server_default='false'), + ) + + +def downgrade() -> None: + op.drop_column('customers', 'is_authenticated') diff --git a/backend/alembic/versions/e65e1b8e7e3c_add_jira_agent_config.py b/backend/alembic/versions/e65e1b8e7e3c_add_jira_agent_config.py new file mode 100644 index 0000000..91e17ec --- /dev/null +++ b/backend/alembic/versions/e65e1b8e7e3c_add_jira_agent_config.py @@ -0,0 +1,61 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add jira agent config + +Revision ID: e65e1b8e7e3c +Revises: 30caca239191 +Create Date: 2025-03-06 21:51:54.214580 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'e65e1b8e7e3c' +down_revision: Union[str, None] = '30caca239191' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('agent_jira_configs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('agent_id', sa.String(), nullable=False), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('project_key', sa.String(), nullable=True), + sa.Column('issue_type_id', sa.String(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_agent_jira_configs_agent_id'), 'agent_jira_configs', ['agent_id'], unique=False) + op.create_index(op.f('ix_agent_jira_configs_id'), 'agent_jira_configs', ['id'], unique=False) + + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.drop_index(op.f('ix_agent_jira_configs_id'), table_name='agent_jira_configs') + op.drop_index(op.f('ix_agent_jira_configs_agent_id'), table_name='agent_jira_configs') + op.drop_table('agent_jira_configs') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/f3dad24474e0_add_rate_limiting_fields_to_agent_model.py b/backend/alembic/versions/f3dad24474e0_add_rate_limiting_fields_to_agent_model.py new file mode 100644 index 0000000..11450da --- /dev/null +++ b/backend/alembic/versions/f3dad24474e0_add_rate_limiting_fields_to_agent_model.py @@ -0,0 +1,52 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Add rate limiting fields to Agent model + +Revision ID: f3dad24474e0 +Revises: ff48f854bcbf +Create Date: 2025-03-15 20:44:17.465349 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f3dad24474e0' +down_revision: Union[str, None] = 'ff48f854bcbf' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.add_column('agents', sa.Column('enable_rate_limiting', sa.Boolean(), nullable=True, default=False)) + op.add_column('agents', sa.Column('overall_limit_per_ip', sa.Integer(), nullable=True, default=100)) + op.add_column('agents', sa.Column('requests_per_sec', sa.Float(), nullable=True, default=1)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('agents', 'requests_per_sec') + op.drop_column('agents', 'overall_limit_per_ip') + op.drop_column('agents', 'enable_rate_limiting') + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/f4a5b6c7d8e9_add_sunrise_style_and_quick_actions.py b/backend/alembic/versions/f4a5b6c7d8e9_add_sunrise_style_and_quick_actions.py new file mode 100644 index 0000000..e13a4fa --- /dev/null +++ b/backend/alembic/versions/f4a5b6c7d8e9_add_sunrise_style_and_quick_actions.py @@ -0,0 +1,65 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add SUNRISE chat style + welcome_message and quick_actions columns + +Adds: +- SUNRISE value to the `chatstyle` enum (a light theme). +- `welcome_message` (Text): first in-conversation agent bubble shown on open. +- `quick_actions` (JSON): predefined quick-action button labels. + +Revision ID: f4a5b6c7d8e9 +Revises: e3f4a5b6c7d8 +Create Date: 2026-06-30 00:00:00.000000 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = 'f4a5b6c7d8e9' +down_revision: Union[str, None] = 'e3f4a5b6c7d8' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + bind = op.get_bind() + + # Add the new value to the native Postgres `chatstyle` enum. ALTER TYPE ... + # ADD VALUE cannot run inside a transaction block, so use an autocommit block. + if bind.dialect.name == "postgresql": + with op.get_context().autocommit_block(): + op.execute("ALTER TYPE chatstyle ADD VALUE IF NOT EXISTS 'SUNRISE'") + + op.add_column( + 'agent_customizations', + sa.Column('welcome_message', sa.Text(), nullable=True) + ) + op.add_column( + 'agent_customizations', + sa.Column('quick_actions', sa.JSON(), nullable=True) + ) + + +def downgrade() -> None: + op.drop_column('agent_customizations', 'quick_actions') + op.drop_column('agent_customizations', 'welcome_message') + # Postgres cannot cleanly drop individual enum values; the added chatstyle + # value is intentionally left in place. diff --git a/backend/alembic/versions/f7a1c2d3e4b5_add_faq_help_center.py b/backend/alembic/versions/f7a1c2d3e4b5_add_faq_help_center.py new file mode 100644 index 0000000..35b3644 --- /dev/null +++ b/backend/alembic/versions/f7a1c2d3e4b5_add_faq_help_center.py @@ -0,0 +1,182 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add FAQ + public help center tables + +Revision ID: f7a1c2d3e4b5 +Revises: drop_slack_tables_001 +Create Date: 2026-07-12 + +New feature: AI-generated FAQs reviewed/published by the org and served as a +public help center on {slug}.help.replypier.site or a verified custom domain. + +Tables: +- faq_generation_jobs: async generate/import jobs (worker + UI polling) +- help_center_settings: per-org branding, slug, agent mapping, custom domain +- faqs: the question/answer pairs (draft -> published workflow) +- help_center_queries: public "Ask AI" log for metering/analytics + +Status/stage columns are plain strings (mirroring knowledge_queue) so new +values never need an ALTER TYPE migration. + +Also extends the notificationtype enum with FAQ job outcomes. Labels are the +uppercase member names, matching how the enum was created in +b365b8447598_initial_version. ALTER TYPE ... ADD VALUE requires PostgreSQL 12+ +to run inside a transaction; added values cannot be removed on downgrade. +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'f7a1c2d3e4b5' +down_revision = 'drop_slack_tables_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'faq_generation_jobs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('job_type', sa.String(), nullable=False), + sa.Column('knowledge_id', sa.Integer(), nullable=True), + sa.Column('source_url', sa.String(), nullable=True), + sa.Column('status', sa.String(), nullable=False), + sa.Column('stage', sa.String(), nullable=False), + sa.Column('progress_percentage', sa.Float(), nullable=False), + sa.Column('faqs_created', sa.Integer(), nullable=False), + sa.Column('error', sa.String(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['knowledge_id'], ['knowledge.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_faq_generation_jobs_id'), 'faq_generation_jobs', ['id']) + op.create_index(op.f('ix_faq_generation_jobs_organization_id'), 'faq_generation_jobs', ['organization_id']) + op.create_index(op.f('ix_faq_generation_jobs_knowledge_id'), 'faq_generation_jobs', ['knowledge_id']) + # The worker polls for pending/processing rows forever; keep that lookup + # O(active) as terminal rows accumulate. + op.create_index( + 'ix_faq_generation_jobs_active', + 'faq_generation_jobs', + ['status'], + postgresql_where=sa.text("status IN ('pending', 'processing')"), + ) + # DB-enforced one-active-job guard per (org, type, source): the API's + # check-then-insert would otherwise race on concurrent /generate calls. + # COALESCE keeps per-source auto jobs for different sources parallel. + op.create_index( + 'uq_faq_generation_jobs_one_active', + 'faq_generation_jobs', + ['organization_id', 'job_type', sa.text('COALESCE(knowledge_id, -1)')], + unique=True, + postgresql_where=sa.text("status IN ('pending', 'processing')"), + ) + + op.create_table( + 'help_center_settings', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('enabled', sa.Boolean(), nullable=False), + sa.Column('slug', sa.String(length=63), nullable=True), + sa.Column('title', sa.String(length=120), nullable=True), + sa.Column('description', sa.String(length=300), nullable=True), + sa.Column('logo_url', sa.String(), nullable=True), + sa.Column('brand_color', sa.String(length=9), nullable=False), + sa.Column('header_links', sa.JSON(), nullable=False), + sa.Column('cta_text', sa.String(length=40), nullable=True), + sa.Column('cta_url', sa.String(), nullable=True), + sa.Column('auto_generate', sa.Boolean(), nullable=False), + sa.Column('agent_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('ai_search_enabled', sa.Boolean(), nullable=False), + sa.Column('custom_domain', sa.String(length=255), nullable=True), + sa.Column('domain_verification_token', sa.String(length=64), nullable=True), + sa.Column('txt_record_verified', sa.Boolean(), nullable=False), + sa.Column('cname_record_verified', sa.Boolean(), nullable=False), + sa.Column('ssl_status', sa.String(), nullable=False), + sa.Column('domain_verified_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['agent_id'], ['agents.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('organization_id'), + sa.UniqueConstraint('slug'), + sa.UniqueConstraint('custom_domain'), + sa.CheckConstraint('slug = lower(slug)', name='ck_help_center_slug_lowercase'), + ) + op.create_index(op.f('ix_help_center_settings_slug'), 'help_center_settings', ['slug']) + op.create_index(op.f('ix_help_center_settings_custom_domain'), 'help_center_settings', ['custom_domain']) + + op.create_table( + 'faqs', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('question', sa.Text(), nullable=False), + sa.Column('answer', sa.Text(), nullable=False), + sa.Column('category', sa.String(length=100), nullable=False), + sa.Column('status', sa.String(), nullable=False), + sa.Column('knowledge_id', sa.Integer(), nullable=True), + sa.Column('source_label', sa.String(length=255), nullable=True), + sa.Column('generation_job_id', sa.Integer(), nullable=True), + sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['knowledge_id'], ['knowledge.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['generation_job_id'], ['faq_generation_jobs.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['created_by'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + ) + # Composite indexes lead with organization_id, so no single-column org index. + op.create_index('ix_faqs_org_status', 'faqs', ['organization_id', 'status']) + op.create_index('ix_faqs_org_category', 'faqs', ['organization_id', 'category']) + # FK indexes so knowledge/job deletions don't scan faqs to enforce SET NULL. + op.create_index(op.f('ix_faqs_knowledge_id'), 'faqs', ['knowledge_id']) + op.create_index(op.f('ix_faqs_generation_job_id'), 'faqs', ['generation_job_id']) + + op.create_table( + 'help_center_queries', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('organization_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('query', sa.Text(), nullable=False), + sa.Column('answered', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['organization_id'], ['organizations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + ) + op.create_index(op.f('ix_help_center_queries_organization_id'), 'help_center_queries', ['organization_id']) + op.create_index(op.f('ix_help_center_queries_created_at'), 'help_center_queries', ['created_at']) + + op.execute("ALTER TYPE notificationtype ADD VALUE IF NOT EXISTS 'FAQ_GENERATED'") + op.execute("ALTER TYPE notificationtype ADD VALUE IF NOT EXISTS 'FAQ_GENERATION_FAILED'") + + +def downgrade() -> None: + # The enum values added in upgrade() cannot be removed; delete the rows + # that reference them so the reverted application enum can still load + # every remaining notification. + op.execute("DELETE FROM notifications WHERE type IN ('FAQ_GENERATED', 'FAQ_GENERATION_FAILED')") + op.drop_table('help_center_queries') + op.drop_table('faqs') + op.drop_table('help_center_settings') + op.drop_table('faq_generation_jobs') diff --git a/backend/alembic/versions/fa04f644fc0c_added_user_id_in_rating_table.py b/backend/alembic/versions/fa04f644fc0c_added_user_id_in_rating_table.py new file mode 100644 index 0000000..b3b338f --- /dev/null +++ b/backend/alembic/versions/fa04f644fc0c_added_user_id_in_rating_table.py @@ -0,0 +1,50 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""added user id in rating table + +Revision ID: fa04f644fc0c +Revises: 186c65ae9406 +Create Date: 2025-03-05 15:38:12.395283 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'fa04f644fc0c' +down_revision: Union[str, None] = '186c65ae9406' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.add_column('ratings', sa.Column('user_id', sa.UUID(), nullable=True)) + op.create_foreign_key(None, 'ratings', 'users', ['user_id'], ['id'], ondelete='SET NULL') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'ratings', type_='foreignkey') + op.drop_column('ratings', 'user_id') + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/fcm_tokens_per_device_001.py b/backend/alembic/versions/fcm_tokens_per_device_001.py new file mode 100644 index 0000000..b89b5fd --- /dev/null +++ b/backend/alembic/versions/fcm_tokens_per_device_001.py @@ -0,0 +1,88 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""per-device fcm_tokens table, replacing users.fcm_token_web + +Revision ID: fcm_tokens_per_device_001 +Revises: add_agent_ai_replies_001 +Create Date: 2026-08-20 + +The old single column held one push token for the whole account, so signing in +on a second device overwrote the first one's token and signing out anywhere +cleared it for every device. Existing tokens are carried over, one row each, so +already-registered browsers keep receiving pushes across the deploy. +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'fcm_tokens_per_device_001' +down_revision = 'add_agent_ai_replies_001' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + 'fcm_tokens', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('token', sa.String(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), + server_default=sa.text('now()'), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), + server_default=sa.text('now()'), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('token', name='uq_fcm_tokens_token'), + ) + op.create_index('ix_fcm_tokens_user_id', 'fcm_tokens', ['user_id']) + + # Carry over the tokens already registered. DISTINCT ON keeps the unique + # constraint safe in the unlikely case two accounts share a token — the + # most recently updated row wins, which is the one FCM would deliver to. + op.execute( + """ + INSERT INTO fcm_tokens (id, user_id, token) + SELECT DISTINCT ON (fcm_token_web) gen_random_uuid(), id, fcm_token_web + FROM users + WHERE fcm_token_web IS NOT NULL AND fcm_token_web <> '' + ORDER BY fcm_token_web, updated_at DESC NULLS LAST + """ + ) + + op.drop_column('users', 'fcm_token_web') + + +def downgrade() -> None: + op.add_column('users', sa.Column( + 'fcm_token_web', sa.String(), nullable=True)) + # Only one token per user fits in the old column; keep the newest. + op.execute( + """ + UPDATE users u + SET fcm_token_web = t.token + FROM ( + SELECT DISTINCT ON (user_id) user_id, token + FROM fcm_tokens + ORDER BY user_id, created_at DESC + ) t + WHERE t.user_id = u.id + """ + ) + op.drop_index('ix_fcm_tokens_user_id', table_name='fcm_tokens') + op.drop_table('fcm_tokens') diff --git a/backend/alembic/versions/ff48f854bcbf_add_ticket_fields_to_session.py b/backend/alembic/versions/ff48f854bcbf_add_ticket_fields_to_session.py new file mode 100644 index 0000000..0e56da0 --- /dev/null +++ b/backend/alembic/versions/ff48f854bcbf_add_ticket_fields_to_session.py @@ -0,0 +1,58 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""add_ticket_fields_to_session + +Revision ID: ff48f854bcbf +Revises: e65e1b8e7e3c +Create Date: 2025-03-07 11:09:21.933786 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'ff48f854bcbf' +down_revision: Union[str, None] = 'e65e1b8e7e3c' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + + op.add_column('session_to_agents', sa.Column('ticket_id', sa.String(), nullable=True)) + op.add_column('session_to_agents', sa.Column('ticket_status', sa.String(), nullable=True)) + op.add_column('session_to_agents', sa.Column('ticket_summary', sa.String(), nullable=True)) + op.add_column('session_to_agents', sa.Column('ticket_description', sa.String(), nullable=True)) + op.add_column('session_to_agents', sa.Column('integration_type', sa.String(), nullable=True)) + op.add_column('session_to_agents', sa.Column('ticket_priority', sa.String(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('session_to_agents', 'ticket_priority') + op.drop_column('session_to_agents', 'integration_type') + op.drop_column('session_to_agents', 'ticket_description') + op.drop_column('session_to_agents', 'ticket_summary') + op.drop_column('session_to_agents', 'ticket_status') + op.drop_column('session_to_agents', 'ticket_id') + + # ### end Alembic commands ### diff --git a/backend/alembic/versions/hc_article_image_paths.py b/backend/alembic/versions/hc_article_image_paths.py new file mode 100644 index 0000000..cc2c5f5 --- /dev/null +++ b/backend/alembic/versions/hc_article_image_paths.py @@ -0,0 +1,44 @@ +"""Repoint baked absolute S3 article-image URLs at the delivery route + +Revision ID: hc_img_paths_001 +Revises: add_notif_prefs_001 +Create Date: 2026-07-28 + +Article images used to be baked into faqs.answer as absolute S3 URLs. Those +only ever worked while the bucket was publicly readable; on a correctly private +bucket they 403. Article Markdown cannot hold a signed URL either, since it is +stored permanently and signatures expire. Rewrite them to the stable +/api/v1/help-center/images/<name> path, which is resolved per request against +whichever storage backend is configured. Local /api/v1/uploads/ paths and +non-S3 URLs are left untouched. +Irreversible best-effort — downgrade is a no-op. +""" +from alembic import op +import sqlalchemy as sa + +from app.services.help_center_images import rewrite_baked_s3_image_urls + +revision = "hc_img_paths_001" +down_revision = "add_notif_prefs_001" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + bind = op.get_bind() + rows = bind.execute( + sa.text("SELECT id, answer FROM faqs WHERE answer LIKE '%amazonaws.com%'") + ).fetchall() + for row in rows: + new_answer = rewrite_baked_s3_image_urls(row.answer) + if new_answer != row.answer: + bind.execute( + sa.text("UPDATE faqs SET answer = :val WHERE id = :id"), + {"val": new_answer, "id": row.id}, + ) + + +def downgrade() -> None: + # The original bucket/region are not recoverable from the rewritten path, + # and the new path works on every backend anyway. + pass diff --git a/backend/alembic/versions/idx_kq_org_status_001.py b/backend/alembic/versions/idx_kq_org_status_001.py new file mode 100644 index 0000000..d078587 --- /dev/null +++ b/backend/alembic/versions/idx_kq_org_status_001.py @@ -0,0 +1,35 @@ +"""Index knowledge_queue by organization and status + +The plan's source limit now counts the crawls still queued or running, so +every file upload and URL add reads this table. It had no index beyond the +primary key and priority, and nothing purges completed rows, so that read +was a sequential scan of every tenant's history. + +Revision ID: idx_kq_org_status_001 +Revises: add_mcp_usage_guidance_001 +Create Date: 2026-09-18 +""" +from typing import Sequence, Union + +from alembic import op + +# revision identifiers, used by Alembic. +# Keep revision ids under 32 characters: alembic_version.version_num is +# varchar(32), and a longer id upgrades the schema then fails to record +# itself, so every later start retries and crashes. +revision: str = 'idx_kq_org_status_001' +down_revision: Union[str, None] = 'add_mcp_usage_guidance_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +INDEX_NAME = 'ix_knowledge_queue_org_status' + + +def upgrade() -> None: + op.create_index( + INDEX_NAME, 'knowledge_queue', ['organization_id', 'status'], unique=False + ) + + +def downgrade() -> None: + op.drop_index(INDEX_NAME, table_name='knowledge_queue') diff --git a/backend/alembic/versions/merge_hc_url_path_001.py b/backend/alembic/versions/merge_hc_url_path_001.py new file mode 100644 index 0000000..64d3f9a --- /dev/null +++ b/backend/alembic/versions/merge_hc_url_path_001.py @@ -0,0 +1,50 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""merge the preserved-URL, allow-new-chat and upload-prefix heads + +Revision ID: merge_hc_url_path_001 +Revises: add_hc_faq_url_path_001, add_allow_new_chat_001, normalise_upload_prefix_001 +Create Date: 2026-08-10 + +Three revisions branched off repair_agent_role_seed_001 independently: the +preserved help-center URLs on this branch, plus allow-new-chat and the upload +prefix normalisation, which had ALREADY left main with two heads before this +branch existed. `alembic upgrade head` in scripts/start.sh refuses to pick +between heads and fails, so the backend crash-loops on start until they are +rejoined. This no-op merge does that. The revision id is kept short — +alembic_version.version_num is varchar(32). +""" +from alembic import op # noqa: F401 +import sqlalchemy as sa # noqa: F401 + +# revision identifiers, used by Alembic. +revision = 'merge_hc_url_path_001' +down_revision = ( + 'add_hc_faq_url_path_001', + 'add_allow_new_chat_001', + 'normalise_upload_prefix_001', +) +branch_labels = None +depends_on = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/backend/alembic/versions/merge_tkt_csat_001.py b/backend/alembic/versions/merge_tkt_csat_001.py new file mode 100644 index 0000000..ee8978d --- /dev/null +++ b/backend/alembic/versions/merge_tkt_csat_001.py @@ -0,0 +1,44 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""merge ticket CSAT and investigation-metering migration heads + +Revision ID: merge_tkt_csat_001 +Revises: add_tkt_csat_001, add_tkt_metered_001 +Create Date: 2026-07-22 + +The CSAT work branched from add_tkt_row_scope_001 while investigation metering +branched from the same revision, leaving two alembic heads. Deploy runs +`alembic upgrade head`, which refuses to pick between them and fails the +container start, so this no-op merge rejoins them. The revision id is kept +short — alembic_version.version_num is varchar(32). +""" +from alembic import op # noqa: F401 +import sqlalchemy as sa # noqa: F401 + +# revision identifiers, used by Alembic. +revision = 'merge_tkt_csat_001' +down_revision = ('add_tkt_csat_001', 'add_tkt_metered_001') +branch_labels = None +depends_on = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/backend/alembic/versions/merge_tkt_phone_001.py b/backend/alembic/versions/merge_tkt_phone_001.py new file mode 100644 index 0000000..dfc2961 --- /dev/null +++ b/backend/alembic/versions/merge_tkt_phone_001.py @@ -0,0 +1,44 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""merge native-ticketing and customer-phone migration heads + +Revision ID: merge_tkt_phone_001 +Revises: add_investigation_runs_001, add_customer_phone_001 +Create Date: 2026-07-17 + +The ticketing feature branched from add_hc_faq_feedback_001 at the same time +main's WhatsApp phone-identity work did, leaving two alembic heads. This +no-op merge rejoins them so `alembic upgrade head` (used by the deploy) has a +single target. The revision id is kept short — alembic_version.version_num is +varchar(32). +""" +from alembic import op # noqa: F401 +import sqlalchemy as sa # noqa: F401 + +# revision identifiers, used by Alembic. +revision = 'merge_tkt_phone_001' +down_revision = ('add_investigation_runs_001', 'add_customer_phone_001') +branch_labels = None +depends_on = None + + +def upgrade() -> None: + pass + + +def downgrade() -> None: + pass diff --git a/backend/alembic/versions/normalise_upload_prefix_001.py b/backend/alembic/versions/normalise_upload_prefix_001.py new file mode 100644 index 0000000..063ba53 --- /dev/null +++ b/backend/alembic/versions/normalise_upload_prefix_001.py @@ -0,0 +1,79 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""prefix stored local upload paths with the static mount + +Revision ID: normalise_upload_prefix_001 +Revises: repair_agent_role_seed_001 +Create Date: 2026-08-08 + +The app had two conventions for a stored local upload. store_upload wrote +`/api/v1/uploads/...`, matching the static mount; agent photos and profile +pictures wrote a bare `/uploads/...`. The frontend resolves stored paths +against the API origin, so the bare ones came out as `https://host/uploads/...` +and 404'd — every agent avatar and teammate avatar was broken on a self-hosted +install using local file storage. (S3 deployments were unaffected: those store +absolute URLs, which pass through untouched.) + +The writers now emit the prefix. This brings the existing rows over. + +Only values starting exactly with `/uploads/` are touched, so absolute S3 URLs, +data URIs and already-prefixed rows are left alone and re-running is a no-op. + +`/api/v1` is written literally rather than read from settings.API_V1_STR: the +static mount in main.py hardcodes it too, and a migration should not change +shape with runtime config. +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'normalise_upload_prefix_001' +down_revision = 'repair_agent_role_seed_001' +branch_labels = None +depends_on = None + +# (table, column) pairs that hold a stored upload path written by the +# bare-/uploads/ writers. +_COLUMNS = [ + ('agent_customizations', 'photo_url'), + ('users', 'profile_pic'), +] + + +def upgrade() -> None: + for table, column in _COLUMNS: + op.execute( + f""" + UPDATE {table} + SET {column} = '/api/v1' || {column} + WHERE {column} LIKE '/uploads/%' + """ + ) + + +def downgrade() -> None: + """Strips the prefix off every local path in these two columns, including + rows this migration did not create. That is deliberate: the reverted code + writes bare paths and reads them the same way, so the column should end up + wholly on the old convention rather than half on each.""" + for table, column in _COLUMNS: + op.execute( + f""" + UPDATE {table} + SET {column} = substring({column} from length('/api/v1') + 1) + WHERE {column} LIKE '/api/v1/uploads/%' + """ + ) diff --git a/backend/alembic/versions/purge_mangled_crawl_pages_001.py b/backend/alembic/versions/purge_mangled_crawl_pages_001.py new file mode 100644 index 0000000..f0c3e32 --- /dev/null +++ b/backend/alembic/versions/purge_mangled_crawl_pages_001.py @@ -0,0 +1,103 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""delete crawl pages that were never pages (mangled mailto: URLs) + +Revision ID: purge_mangled_crawl_pages_001 +Revises: merge_hc_url_path_001 +Create Date: 2026-08-12 + +The website crawler followed `mailto:` hrefs: urljoin left them intact and +_normalize_url prefixed them, so `mailto:a@example.com` became +`https://mailto:a@example.com` — a URL whose host really is `example.com` (with +`mailto:a` as userinfo), which passed the same-domain check, was fetched (the +server ignores the credentials and serves the site root) and was stored as a +knowledge page. Its body then produced more of them: `https://mailto:a@example.com/pricing`. + +The crawler no longer produces these (app/knowledge/crawl_scope.py); this +removes the ones already stored, so they stop being retrieved as answers and +stop feeding FAQ generation. + +Deliberately narrow. Only chunk rows are deleted whose page id is + * an http(s) URL carrying userinfo (`https://<something>@host/...`), which a + crawl never legitimately produces, or + * a non-HTTP scheme (`mailto:`, `tel:`, …), which is not a page at all, +and only under a WEBSITE source. Everything else stays: PDF chunks keyed by +UUID, text pages keyed by their title (including text pages added under a +website source), and every real crawled URL — off-target ones included. Pages +that are real content but outside the new default crawl scope (a marketing site +picked up from a help-center seed) are NOT touched here: they were crawled by +design, deleting them is a judgement call per source, and +backend/scripts/prune_offsite_pages.py does that opt-in with a dry run. + +Vector tables are per-organisation and named in the `knowledge` rows, so the +loop reads them from there rather than guessing. Tables listed but missing are +skipped, and re-running deletes nothing. +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'purge_mangled_crawl_pages_001' +down_revision = 'merge_hc_url_path_001' +branch_labels = None +depends_on = None + +# A page id that is not a page: userinfo before the first path segment, or a +# scheme that is not http(s). Anchored, so a '/@handle' path does not match. +_USERINFO_PATTERN = '^https?://[^/]*@' +_NON_HTTP_SCHEME_PATTERN = '^(mailto|tel|sms|javascript|data|ftp):' + + +def upgrade() -> None: + op.execute( + f""" + DO $$ + DECLARE + r record; + removed bigint; + total bigint := 0; + BEGIN + FOR r IN + SELECT DISTINCT schema AS sch, table_name AS tbl + FROM knowledge + WHERE schema IS NOT NULL AND table_name IS NOT NULL + LOOP + BEGIN + -- %L quotes each pattern, so the regexes need no escaping. + EXECUTE format( + 'DELETE FROM %I.%I v WHERE (v.id ~ %L OR v.id ~* %L) ' + 'AND v.name IN (SELECT source FROM knowledge ' + 'WHERE source_type = %L)', + r.sch, r.tbl, + '{_USERINFO_PATTERN}', '{_NON_HTTP_SCHEME_PATTERN}', 'WEBSITE' + ); + GET DIAGNOSTICS removed = ROW_COUNT; + total := total + removed; + EXCEPTION + WHEN undefined_table OR invalid_schema_name OR undefined_column THEN + CONTINUE; + END; + END LOOP; + RAISE NOTICE 'Removed % mangled crawl page chunk(s)', total; + END $$; + """ + ) + + +def downgrade() -> None: + """No-op: the deleted rows were crawl artefacts of a bug, and their content + (a site's root page stored under a bogus URL) cannot be reconstructed. A + re-crawl repopulates whatever the source should legitimately contain.""" diff --git a/backend/alembic/versions/relative_hc_upload_urls.py b/backend/alembic/versions/relative_hc_upload_urls.py new file mode 100644 index 0000000..f483850 --- /dev/null +++ b/backend/alembic/versions/relative_hc_upload_urls.py @@ -0,0 +1,48 @@ +"""Make help-center upload URLs relative (strip baked-in absolute host) + +Revision ID: rel_hc_uploads_001 +Revises: add_hc_favicon_001 +Create Date: 2026-07-25 + +Article images (baked into faqs.answer markdown) and, historically, some +logo/favicon rows carried an absolute host in front of the /api/v1/uploads/ +path (e.g. http://localhost:8000/api/v1/uploads/...). That breaks the moment the +help center is viewed from any other origin. Convert those to relative paths so +they resolve against whichever origin serves the page. S3 URLs (no +/api/v1/uploads/ segment) and already-relative paths are left untouched. +Irreversible best-effort — downgrade is a no-op. +""" +from alembic import op +import sqlalchemy as sa + +from app.services.help_center_images import strip_upload_host + +revision = "rel_hc_uploads_001" +down_revision = "add_hc_favicon_001" +branch_labels = None +depends_on = None + + +def _rewrite(bind, table: str, column: str) -> None: + rows = bind.execute( + sa.text(f"SELECT id, {column} AS val FROM {table} WHERE {column} LIKE '%/api/v1/uploads/%'") + ).fetchall() + for row in rows: + new_val = strip_upload_host(row.val) + if new_val != row.val: + bind.execute( + sa.text(f"UPDATE {table} SET {column} = :val WHERE id = :id"), + {"val": new_val, "id": row.id}, + ) + + +def upgrade() -> None: + bind = op.get_bind() + _rewrite(bind, "faqs", "answer") + _rewrite(bind, "help_center_settings", "logo_url") + _rewrite(bind, "help_center_settings", "favicon_url") + + +def downgrade() -> None: + # Can't reliably re-absolutize (original host unknown); relative paths are correct anyway. + pass diff --git a/backend/alembic/versions/repair_agent_role_seed_001.py b/backend/alembic/versions/repair_agent_role_seed_001.py new file mode 100644 index 0000000..7ed15c8 --- /dev/null +++ b/backend/alembic/versions/repair_agent_role_seed_001.py @@ -0,0 +1,184 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""repair agent roles left behind by the hosted signup seeder + +Revision ID: repair_agent_role_seed_001 +Revises: add_run_connector_status_001 +Create Date: 2026-08-03 + +Two signup paths seed an organization's starter roles, and they disagreed. The +hosted one gave Agent only view_assigned_chats + manage_assigned_chats, and +marked BOTH Admin and Agent is_default=true. b7c4e91a2d38 repaired the orgs +that existed in July 2026; every org created since through hosted signup was +seeded wrong again. The seeders now share DEFAULT_AGENT_ROLE_PERMISSIONS, so +this is the last of it. + +Unlike b7c4e91a2d38 this does NOT backfill by capability. That migration +widened every chat-capable role and promised admins the grant was theirs to +revoke: + + "an admin who wants an agent kept out can uncheck the permission in Roles" + +Running a capability backfill again would break that promise — a role someone +deliberately narrowed would silently get its permissions back. So the repair +matches the bad seed EXACTLY: a role qualifies only if its permission set is +precisely {view_assigned_chats, manage_assigned_chats}, no more and no less. +An edited role has a different set and is left alone. A community-seeded org +has four permissions and never matches, which is what makes this a no-op +outside hosted. + +Also does NOT promote a role to is_default where an organization has none. +roles.py refuses to edit or delete a default role, so promoting Agent would +make it permanently uneditable through the API. + +Idempotent: every insert is guarded by NOT EXISTS (role_permissions has no +primary key or unique constraint, so that guard is the only thing standing +between a re-run and duplicate rows), and the is_default repair is a no-op +once an org is down to one default. +""" + +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'repair_agent_role_seed_001' +down_revision = 'add_run_connector_status_001' +branch_labels = None +depends_on = None + + +# The exact permission set the hosted seeder produced, and what it should have +# been. Sorted, because the match is done against a sorted aggregate. +BAD_SEED = ('manage_assigned_chats', 'view_assigned_chats') +MISSING_FROM_BAD_SEED = ('view_unassigned_chats', 'view_people') + +# view_agents and view_knowledge became real permissions in the same release +# that this migration ships with — they were in the catalogue and the role +# editor for months while being enforced nowhere. Anyone holding the manage_* +# twin already passes those checks (they are OR'd), so this grant changes no +# behaviour. It exists so the role editor stops showing an admin role with +# "manage knowledge" ticked and "view knowledge" blank. +IMPLIED_BY_MANAGE = ( + ('view_agents', 'manage_agents'), + ('view_knowledge', 'manage_knowledge'), +) + +# Permission rows themselves may be absent on a deployment whose last org was +# created before the name was added to Permission.default_permissions(). +REQUIRED_PERMISSIONS = ( + ('view_unassigned_chats', 'Can view unassigned AI chats'), + ('view_people', 'Can view the people directory'), + ('view_agents', 'Can view chat agents'), + ('view_knowledge', 'Can view knowledge base'), +) + + +BAD_SEED_SIGNATURE = ",".join(sorted(BAD_SEED)) + + +def _sql_list(values) -> str: + return ", ".join(f"'{value}'" for value in values) + + +def upgrade() -> None: + for name, description in REQUIRED_PERMISSIONS: + op.execute( + f""" + INSERT INTO permissions (name, description) + SELECT '{name}', '{description}' + WHERE NOT EXISTS (SELECT 1 FROM permissions WHERE name = '{name}') + """ + ) + + # Roles whose permission set is exactly the bad seed. HAVING on the sorted + # aggregate is what makes it exact — a role with one extra permission, or + # one fewer, produces a different string and is skipped. + # + # Both permissions go in ONE statement, deliberately. Granting them in + # separate statements does not work: the first grant changes the role's + # permission set, so it no longer matches the signature and the second + # statement finds nothing to do. The subquery here is evaluated once, + # against the pre-insert state. + op.execute( + f""" + INSERT INTO role_permissions (role_id, permission_id) + SELECT bad.role_id, target.id + FROM ( + SELECT rp.role_id + FROM role_permissions rp + JOIN permissions p ON p.id = rp.permission_id + GROUP BY rp.role_id + HAVING string_agg(p.name, ',' ORDER BY p.name) = '{BAD_SEED_SIGNATURE}' + ) bad + CROSS JOIN permissions target + WHERE target.name IN ({_sql_list(MISSING_FROM_BAD_SEED)}) + AND NOT EXISTS ( + SELECT 1 FROM role_permissions existing + WHERE existing.role_id = bad.role_id + AND existing.permission_id = target.id + ) + """ + ) + + for implied, held in IMPLIED_BY_MANAGE: + op.execute( + f""" + INSERT INTO role_permissions (role_id, permission_id) + SELECT DISTINCT rp.role_id, target.id + FROM role_permissions rp + JOIN permissions holder ON holder.id = rp.permission_id + CROSS JOIN permissions target + WHERE holder.name = '{held}' + AND target.name = '{implied}' + AND NOT EXISTS ( + SELECT 1 FROM role_permissions existing + WHERE existing.role_id = rp.role_id + AND existing.permission_id = target.id + ) + """ + ) + + # One default role per organization, same repair as b7c4e91a2d38. Clearing + # Admin is what leaves Agent as the default; clearing Agent instead would + # hand every newly invited user full permissions. + # + # The COUNT(*) > 1 guard means an org whose only default is Admin keeps it + # rather than ending up with no default at all. + op.execute( + """ + UPDATE roles SET is_default = false + WHERE name = 'Admin' + AND is_default = true + AND organization_id IN ( + SELECT organization_id FROM roles + WHERE is_default = true + GROUP BY organization_id + HAVING COUNT(*) > 1 + ) + """ + ) + + +def downgrade() -> None: + """Deliberately empty. + + There is nothing safe to undo. Revoking the four permissions would strip + them from roles that legitimately hold them — this migration cannot tell + the grants it made from the ones the seeder wrote. Deleting the permission + rows, as b7c4e91a2d38's downgrade does, would cascade across every + organization. And the pre-repair is_default state was ambiguous by + definition, which is the whole reason for the repair. + """ diff --git a/backend/alembic/versions/session_conversation_key_001.py b/backend/alembic/versions/session_conversation_key_001.py new file mode 100644 index 0000000..4da45a9 --- /dev/null +++ b/backend/alembic/versions/session_conversation_key_001.py @@ -0,0 +1,55 @@ +"""Scope widget conversations and customers to the token that created them + +A widget token the visitor obtained without proving who they are may name an +existing customer. session_to_agents.conversation_key records which token scope +created a session, so such a token only resumes its own conversations; existing +rows stay NULL, the scope of tokens the embedding app vouched for. +customers.conversation_key records which widget scope created a customer, so a +token may only edit or merge a customer it created itself. + +Every scope check filters session_to_agents by customer_id, which had no index; +the index is built CONCURRENTLY so the table keeps taking writes while the +backend starts. That can't run in a transaction, hence the autocommit block. A +build that fails part-way leaves an INVALID index that IF NOT EXISTS would then +skip: if that happens, DROP INDEX ix_session_to_agents_customer_id and restart. + +Revision ID: session_conversation_key_001 +Revises: auth_sessions_001 +Create Date: 2026-10-12 +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +# Keep revision ids under 32 characters: alembic_version.version_num is +# varchar(32). +revision: str = 'session_conversation_key_001' +down_revision: Union[str, None] = 'auth_sessions_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +INDEX_NAME = 'ix_session_to_agents_customer_id' + + +def upgrade() -> None: + op.add_column('session_to_agents', sa.Column('conversation_key', sa.String(64), nullable=True), + if_not_exists=True) + op.add_column('customers', sa.Column('conversation_key', sa.String(64), nullable=True), + if_not_exists=True) + with op.get_context().autocommit_block(): + op.create_index( + INDEX_NAME, 'session_to_agents', ['customer_id'], + unique=False, + postgresql_concurrently=True, + if_not_exists=True, + ) + + +def downgrade() -> None: + with op.get_context().autocommit_block(): + op.drop_index(INDEX_NAME, table_name='session_to_agents', + postgresql_concurrently=True, if_exists=True) + op.drop_column('customers', 'conversation_key', if_exists=True) + op.drop_column('session_to_agents', 'conversation_key', if_exists=True) diff --git a/backend/alembic/versions/widget_key_lookup_001.py b/backend/alembic/versions/widget_key_lookup_001.py new file mode 100644 index 0000000..f226113 --- /dev/null +++ b/backend/alembic/versions/widget_key_lookup_001.py @@ -0,0 +1,34 @@ +"""Look widget API keys up by a SHA-256 of the key + +Checking a key used to bcrypt it against every active widget app, about +100ms each, on the request's event loop, so each wrong key stalled the API +in proportion to the number of apps. widget_apps.api_key_lookup holds a +SHA-256 of the key (safe for a 256-bit random secret) so a key is one +indexed lookup. Keys issued earlier stay NULL until they are next used. + +Revision ID: widget_key_lookup_001 +Revises: session_conversation_key_001 +Create Date: 2026-10-19 +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = 'widget_key_lookup_001' +down_revision: Union[str, None] = 'session_conversation_key_001' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +INDEX_NAME = 'ix_widget_apps_api_key_lookup' + + +def upgrade() -> None: + op.add_column('widget_apps', sa.Column('api_key_lookup', sa.String(64), nullable=True), if_not_exists=True) + op.create_index(INDEX_NAME, 'widget_apps', ['api_key_lookup'], unique=True, if_not_exists=True) + + +def downgrade() -> None: + op.drop_index(INDEX_NAME, table_name='widget_apps', if_exists=True) + op.drop_column('widget_apps', 'api_key_lookup', if_exists=True) diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..f539b43 --- /dev/null +++ b/backend/app/__init__.py @@ -0,0 +1,27 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Must run before anything imports app.enterprise (see app/core/edition.py). +from .core import edition as _edition + +_edition.apply() + +# Before any agno agent runs (see app/core/agno_telemetry.py). +from .core import agno_telemetry as _agno_telemetry # noqa: E402 + +_agno_telemetry.apply() + +from .main import app # noqa: E402 diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py new file mode 100644 index 0000000..563a34a --- /dev/null +++ b/backend/app/api/__init__.py @@ -0,0 +1,17 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Empty init file to make the directory a package diff --git a/backend/app/config/.gitignore b/backend/app/config/.gitignore new file mode 100644 index 0000000..debec8d --- /dev/null +++ b/backend/app/config/.gitignore @@ -0,0 +1,3 @@ +# Local runtime credentials — never commit +* +!.gitignore diff --git a/backend/app/core/application.py b/backend/app/core/application.py new file mode 100644 index 0000000..aadc29e --- /dev/null +++ b/backend/app/core/application.py @@ -0,0 +1,43 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from fastapi import FastAPI +from app.core.error_handlers import install_error_handlers +from app.core.logger import get_logger + +logger = get_logger(__name__) + +# Create FastAPI app instance +app = FastAPI( + title="ReplyPier API", + description="AI-Powered Customer Support Platform", + version="1.0.0" +) + +# 5xx bodies must not carry our own error text; see error_handlers. +install_error_handlers(app) + +def initialize_cors_listener(): + """ + Initialize the CORS listener for multi-worker environments + This function is called during application startup + """ + try: + from app.core.cors import start_cors_listener + start_cors_listener(app) + logger.info("CORS listener initialized") + except Exception as e: + logger.error(f"Failed to initialize CORS listener: {str(e)}") \ No newline at end of file diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py new file mode 100644 index 0000000..e28d80f --- /dev/null +++ b/backend/app/core/auth.py @@ -0,0 +1,517 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from fastapi import Depends, HTTPException, status, Cookie, Request +from fastapi.security import HTTPBearer +from sqlalchemy.orm import Session +from typing import Callable, Iterable, Optional, List +from uuid import UUID + +from app.database import get_db +from app.models.user import User +from app.models.organization import Organization +from app.services.auth_sessions import ACCESS, live_payload +from app.core.logger import get_logger +from app.models.role import Role + +logger = get_logger(__name__) + +# Enterprise: Personal Access Tokens (PATs). PATs are an enterprise-only feature, so the +# resolver lives in the enterprise package. In the community edition that package is absent +# and this falls back to a no-op, meaning "rpat_" bearer tokens simply fail normal JWT +# verification like any other invalid token. See app/enterprise/services/pat.py. +# +# The enterprise resolver is imported lazily (on first use) rather than at module load: the +# enterprise package pulls in the API routers, which import this module, so an eager import +# here would create a circular import during application start-up. +PAT_TOKEN_PREFIX = "rpat_" +_pat_resolver = None +_pat_resolver_loaded = False + + +def _resolve_pat_user(token: str, db: Session) -> Optional[User]: + global _pat_resolver, _pat_resolver_loaded + if not _pat_resolver_loaded: + _pat_resolver_loaded = True + try: + from app.enterprise.services.pat import resolve_pat_user + _pat_resolver = resolve_pat_user + except ImportError: # community edition / enterprise package not installed + _pat_resolver = None + if _pat_resolver is None: + return None + return _pat_resolver(token, db) + +def check_permissions(user: User, required_permissions: List[str]) -> bool: + """Check if user has required permissions through their role""" + if not user.role or not user.role.permissions: + return False + user_permissions = [p.name for p in user.role.permissions] + # If user has super_admin permission, they have access to everything + if "super_admin" in user_permissions: + return True + return all(perm in user_permissions for perm in required_permissions) + + +def require_grantable(current_user: User, permission_names: Iterable[str]) -> None: + """You cannot grant a permission you do not hold yourself. + + Without this, manage_roles was equivalent to super_admin — and in fact so + was *any* authenticated session, since the /roles endpoints carried no + permission check at all: POST /roles/{my_role_id}/permissions/super_admin + was enough. check_permissions short-circuits on super_admin, so an org + owner granting anything is unaffected. + + Lives here rather than in api/roles.py because granting is not confined to + that router — the chat-scope toggles on the user form widen a role too. + """ + ungrantable = sorted( + name for name in permission_names + if not check_permissions(current_user, [name]) + ) + if ungrantable: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"Cannot grant a permission you do not hold: {', '.join(ungrantable)}" + ) + +def require_permissions(*required_permissions: str): + """Dependency to check user permissions""" + async def permission_checker( + current_user: User = Depends(get_current_user) + ) -> User: + if not check_permissions(current_user, list(required_permissions)): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions" + ) + return current_user + return permission_checker + + +# Seeing conversations. Either scope grants it — which require_permissions, +# whose semantics are AND, cannot express. +# +# manage_all_chats, not "manage_chats" — the latter appears in several hand- +# rolled checks but is not in Permission.default_permissions(), so it has +# never matched anyone. +CHAT_VIEW_PERMISSIONS = ( + "view_all_chats", + "view_assigned_chats", + "view_unassigned_chats", +) + +# Acting on a conversation: taking it over, reassigning it, sending outbound. +# Deliberately NOT derived from INBOX_PERMISSIONS — folding the view grants in +# here would let a read-only role reassign other people's conversations. +CHAT_MANAGE_PERMISSIONS = ("manage_all_chats", "manage_assigned_chats") + +# Working the inbox, in the sense the surfaces around it mean it: an agent who +# can open a conversation must also be able to read the channel accounts and +# message templates needed to answer one, or the feature exists only in theory. +# This used to be just the two org-wide grants, which excluded the very agents +# the comment described. +INBOX_PERMISSIONS = CHAT_VIEW_PERMISSIONS + CHAT_MANAGE_PERMISSIONS + +# The narrower org-wide scope, for the places that genuinely mean "can see the +# whole org's chats" rather than "works the inbox". +ORG_CHAT_PERMISSIONS = ("view_all_chats", "manage_all_chats") + +# The people directory. +PEOPLE_READ_PERMISSIONS = ("view_people",) + INBOX_PERMISSIONS + +# Correcting a person's details, marking them a customer and pushing them to a +# connected CRM are inbox work, not administration — people.py documents the +# PATCH as the agent's identification tool. So view_people grants both; there +# is no separate read-only directory role today. +PEOPLE_WRITE_PERMISSIONS = PEOPLE_READ_PERMISSIONS + + +def has_any_permission(user: User, permissions: Iterable[str]) -> bool: + """True when the user holds AT LEAST ONE of `permissions`. + + The OR counterpart to check_permissions, and it honours the same + super_admin bypass — hand-rolled `isdisjoint` checks kept forgetting it, + so a super_admin could send templates but got 403 from People. + """ + if not user.role or not user.role.permissions: + return False + held = {p.name for p in user.role.permissions} + return "super_admin" in held or not held.isdisjoint(set(permissions)) + + +def require_any_permission(*permissions: str): + """Dependency: the user needs any one of `permissions`.""" + async def permission_checker( + current_user: User = Depends(get_current_user) + ) -> User: + if not has_any_permission(current_user, permissions): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions" + ) + return current_user + return permission_checker + +def request_token(request: Request) -> Optional[str]: + """The bearer credential a request carries: the access_token cookie, else + an Authorization: Bearer header.""" + cookie_token = request.cookies.get('access_token') + # Ensure we got a valid string, not a mock or None + if cookie_token and isinstance(cookie_token, str): + return cookie_token + auth_header = request.headers.get('Authorization') + if auth_header and auth_header.startswith('Bearer '): + return auth_header.split(' ')[1] + return None + + +def _unauthorized(detail: str) -> HTTPException: + return HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail=detail, + headers={"WWW-Authenticate": "Bearer"}, + ) + + +def _jwt_user(access_token: str, db: Session) -> User: + """The active user a live access JWT belongs to, or a 401. + + live_payload rejects refresh tokens and tokens whose session was revoked + (signed out, or ended by a password change). + """ + payload = live_payload(db, access_token, ACCESS) + if payload is None: + raise _unauthorized("Invalid authentication token") + + user_id = payload.get("sub") + if user_id is None: + raise _unauthorized("Invalid token payload") + + user = db.query(User).filter(User.id == UUID(str(user_id))).first() + if user is None: + raise _unauthorized("User not found") + + if not user.is_active: + raise _unauthorized("User is inactive") + + return user + + +async def get_current_user( + request: Request, + access_token: Optional[str] = Cookie(None), + db: Session = Depends(get_db) +) -> User: + """Get current user from access token cookie""" + try: + if not access_token or not isinstance(access_token, str): + access_token = request_token(request) + if not access_token: + raise _unauthorized("Not authenticated") + + # Enterprise: a Personal Access Token resolves directly to its owning user. + if access_token.startswith(PAT_TOKEN_PREFIX): + pat_user = _resolve_pat_user(access_token, db) + if pat_user is not None and pat_user.is_active: + return pat_user + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or revoked access token", + headers={"WWW-Authenticate": "Bearer"}, + ) + + return _jwt_user(access_token, db) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Authentication error: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + +async def get_current_organization( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +) -> Organization: + """Get current user's organization""" + if not current_user.organization_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="User is not associated with any organization" + ) + + organization = db.query(Organization).filter( + Organization.id == current_user.organization_id, + Organization.is_active == True + ).first() + + if not organization: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Organization not found or inactive" + ) + + return organization + +def require_permission(permission: str): + """Dependency for checking if user has specific permission""" + async def permission_dependency( + current_user: User = Depends(get_current_user) + ) -> User: + if not current_user.role or not current_user.role.has_permission(permission): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail=f"User does not have required permission: {permission}" + ) + return current_user + return permission_dependency + +def require_subscription_management( + current_user: User = Depends(get_current_user) +) -> User: + """Dependency for checking if user can manage subscriptions""" + if not current_user.role or not current_user.role.can_manage_subscription(): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="User does not have permission to manage subscriptions" + ) + return current_user + + +# Unified Authentication Functions +def get_auth_info_from_request(request: Request) -> dict: + """Determine if this is a Shopify request based on the URL path""" + return {"is_shopify": "/shopify" in str(request.url)} + + +async def get_unified_auth(request: Request, db: Session = Depends(get_db)) -> dict: + """Authenticate a JWT, a PAT, or a Shopify session token. + + Authentication only — it deliberately holds no permission check. Wrap it in + require_unified_permissions / require_any_unified_permission to declare what + a route needs. + """ + from app.services.shopify_session import require_shopify_or_jwt_auth + + try: + # Check if this is a Shopify-related endpoint or has query params indicating Shopify context + url_str = str(request.url) + query_params = dict(request.query_params) + is_shopify_context = ( + "/shopify" in url_str or + url_str.endswith("/shopify") or + "shop" in query_params or + "host" in query_params + ) + + if is_shopify_context: + # For Shopify endpoints or context, use the shopify/JWT auth + auth_result = await require_shopify_or_jwt_auth(request, db) + return { + "auth_type": auth_result.get("auth_type", "shopify"), + "organization_id": auth_result["organization_id"], + "user_id": auth_result.get("user_id"), + "current_user": auth_result.get("current_user") + } + else: + # For regular endpoints, use JWT auth - inline the logic to avoid calling dependency as function + try: + # Get token from cookie or Authorization header + access_token = request_token(request) + if not access_token: + raise _unauthorized("Not authenticated") + + # Enterprise: a Personal Access Token resolves directly to its owning user. + if access_token.startswith(PAT_TOKEN_PREFIX): + current_user = _resolve_pat_user(access_token, db) + if current_user is None or not current_user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or revoked access token", + headers={"WWW-Authenticate": "Bearer"}, + ) + return { + "auth_type": "pat", + "organization_id": current_user.organization_id, + "user_id": current_user.id, + "current_user": current_user + } + + current_user = _jwt_user(access_token, db) + + return { + "auth_type": "jwt", + "organization_id": current_user.organization_id, # Keep as UUID + "user_id": current_user.id, + "current_user": current_user + } + except HTTPException: + raise + except Exception as e: + logger.error(f"JWT authentication error in unified auth: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + except HTTPException: + # Re-raise HTTP exceptions as-is (don't wrap them) + raise + except Exception as e: + logger.error(f"Authentication error: {str(e)}") + raise HTTPException(status_code=401, detail="Authentication failed") + + +def _authorize_unified(auth_info: dict, permitted: Callable[[User], bool]) -> dict: + """Apply a permission check to a get_unified_auth result. + + A Shopify shop session authenticates a *store*, not a person, so it carries + no `current_user` and there is no role to check — those requests pass + through exactly as they did before authorization moved out of the helper. + """ + current_user = auth_info.get("current_user") + if current_user is None: + return auth_info + if not permitted(current_user): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not enough permissions" + ) + return auth_info + + +def require_unified_permissions(*required_permissions: str): + """get_unified_auth + AND-semantics permissions, for JWT/PAT callers. + + get_unified_auth used to hardcode a manage_agents check, which meant every + route sharing the helper inherited it — including reading your own org's + subscription, which locked every non-admin out of the whole app. The helper + now only authenticates; each route declares what it needs. + """ + async def permission_checker( + auth_info: dict = Depends(get_unified_auth) + ) -> dict: + return _authorize_unified( + auth_info, + lambda user: check_permissions(user, list(required_permissions)), + ) + return permission_checker + + +def require_any_unified_permission(*permissions: str): + """The OR counterpart of require_unified_permissions.""" + async def permission_checker( + auth_info: dict = Depends(get_unified_auth) + ) -> dict: + return _authorize_unified( + auth_info, + lambda user: has_any_permission(user, permissions), + ) + return permission_checker + + +async def get_unified_chat_auth(request: Request, db: Session = Depends(get_db)) -> dict: + """Unified authentication that handles both regular JWT and Shopify session tokens for chat endpoints""" + from app.services.shopify_session import require_shopify_or_jwt_auth + + try: + if "/shopify" in str(request.url): + # For Shopify endpoints, use the shopify auth + auth_result = await require_shopify_or_jwt_auth(request, db) + return { + "auth_type": auth_result.get("auth_type", "shopify"), + "organization_id": auth_result["organization_id"], + "user_id": auth_result.get("user_id"), + "current_user": auth_result.get("current_user") + } + else: + # For regular endpoints, use JWT auth with chat permissions - inline the logic + try: + # Get token from cookie or Authorization header + access_token = request_token(request) + if not access_token: + raise _unauthorized("Not authenticated") + + # Enterprise: a Personal Access Token resolves directly to its owning user. + if access_token.startswith(PAT_TOKEN_PREFIX): + current_user = _resolve_pat_user(access_token, db) + if current_user is None or not current_user.is_active: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid or revoked access token", + headers={"WWW-Authenticate": "Bearer"}, + ) + pat_permissions = {p.name for p in current_user.role.permissions} + pat_is_super_admin = "super_admin" in pat_permissions + pat_can_view_all = pat_is_super_admin or "view_all_chats" in pat_permissions + pat_can_view_assigned = pat_is_super_admin or "view_assigned_chats" in pat_permissions + pat_can_view_unassigned = pat_is_super_admin or "view_unassigned_chats" in pat_permissions + if not (pat_can_view_all or pat_can_view_assigned or pat_can_view_unassigned): + raise HTTPException(status_code=403, detail="Not enough permissions") + return { + "auth_type": "pat", + "organization_id": current_user.organization_id, + "user_id": current_user.id, + "current_user": current_user, + "can_view_all": pat_can_view_all, + "can_view_assigned": pat_can_view_assigned, + "can_view_unassigned": pat_can_view_unassigned + } + + current_user = _jwt_user(access_token, db) + + # Check chat permissions + user_permissions = {p.name for p in current_user.role.permissions} + is_super_admin = "super_admin" in user_permissions + can_view_all = is_super_admin or "view_all_chats" in user_permissions + can_view_assigned = is_super_admin or "view_assigned_chats" in user_permissions + # The unclaimed AI queue — sessions no human has taken yet + can_view_unassigned = is_super_admin or "view_unassigned_chats" in user_permissions + + if not (can_view_all or can_view_assigned or can_view_unassigned): + raise HTTPException( + status_code=403, + detail="Not enough permissions" + ) + + return { + "auth_type": "jwt", + "organization_id": current_user.organization_id, # Keep as UUID + "user_id": current_user.id, + "current_user": current_user, + "can_view_all": can_view_all, + "can_view_assigned": can_view_assigned, + "can_view_unassigned": can_view_unassigned + } + except HTTPException: + raise + except Exception as e: + logger.error(f"JWT authentication error in unified chat auth: {str(e)}", exc_info=True) + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + except HTTPException: + # Re-raise HTTP exceptions as-is (don't wrap them) + raise + except Exception as e: + logger.error(f"Chat authentication error: {str(e)}") + raise HTTPException(status_code=401, detail="Authentication failed") diff --git a/backend/app/core/auth_utils.py b/backend/app/core/auth_utils.py new file mode 100644 index 0000000..4cc0d53 --- /dev/null +++ b/backend/app/core/auth_utils.py @@ -0,0 +1,224 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from typing import Optional, Tuple +from uuid import UUID +import http.cookies +from urllib.parse import parse_qs, urlparse +from app.core.security import verify_conversation_token +from app.core.logger import get_logger +from app.database import get_db, SessionLocal +from app.models.user import User +from sqlalchemy.orm import Session +from app.core.cors import is_first_party_origin, origin_of +from app.models.widget import Widget +from app.repositories.customer import CustomerRepository +from app.services.auth_sessions import ACCESS, REFRESH, issue_tokens, live_payload + +logger = get_logger(__name__) + + +async def refresh_access_token(refresh_token: str, db: Session) -> str | None: + """Refresh access token using refresh token""" + try: + payload = live_payload(db, refresh_token, REFRESH) + if not payload: + return None + + user_id = payload.get("sub") + org_id = payload.get("org") + + if not user_id or not org_id: + return None + + # Verify user exists and is active + user = db.query(User).filter(User.id == UUID(user_id)).first() + if not user or not user.is_active: + return None + + # Generate new access token for the same session (a legacy token + # without one gets a new session) + access_token, _ = issue_tokens(db, user_id, org_id, sid=payload.get("sid")) + db.commit() + return access_token + + except Exception as e: + logger.error(f"Token refresh failed: {str(e)}") + return None + + +def _is_own_origin(origin: str, host: str) -> bool: + """Whether Origin is the API's own origin (the host the request was sent to).""" + own = origin_of(origin) + return bool(own and host) and own == origin_of(f"{urlparse(origin).scheme}://{host}") + + +def cookie_socket_allowed(environ: dict) -> bool: + """Whether a socket handshake may sign in with the dashboard's cookies. + + Sockets get no CORS check and the cookies are SameSite=None, so a page on + another site (an org's signup domain is unverified) could otherwise open + the socket as whoever is signed in. socket.io authenticates a namespace + with the headers of the first handshake request, so all of it must have + come from our own pages: + - never a JSONP handshake: a <script> tag sends the cookies without an Origin + - with an Origin, a first-party one or the API's own origin + - without one, only a same-origin request (browsers omit Origin on + same-origin GETs, e.g. polling behind a single-host proxy) + """ + if "j" in parse_qs(environ.get("QUERY_STRING", "")): + return False + origin = environ.get("HTTP_ORIGIN") + if origin: + return is_first_party_origin(origin) or _is_own_origin(origin, environ.get("HTTP_HOST", "")) + return environ.get("HTTP_SEC_FETCH_SITE") == "same-origin" + + +async def authenticate_socket(sid: str, environ: dict) -> Tuple[Optional[str], Optional[int], Optional[int]]: + """ + Authenticate socket connection using cookies + Returns: (access_token, user_id, org_id) + """ + if not cookie_socket_allowed(environ): + logger.warning(f"Refused cookie socket auth for sid {sid} from origin {environ.get('HTTP_ORIGIN')}") + return None, None, None + + try: + # Extract cookies from environ + cookie_str = environ.get('HTTP_COOKIE', environ.get('headers', {}).get('Cookie', '')) + cookies = http.cookies.SimpleCookie() + cookies.load(cookie_str) + + # Get access token from cookies + access_token = None + if 'access_token' in cookies: + access_token = cookies['access_token'].value + + if not access_token: + logger.info("No access token found in cookies") + # Try to refresh using refresh token + if 'refresh_token' in cookies: + refresh_token = cookies['refresh_token'].value + with SessionLocal() as db: + access_token = await refresh_access_token(refresh_token, db) + + if not access_token: + return None, None, None + + # Verify the token is a live access token of a user who can still sign in + with SessionLocal() as db: + payload = live_payload(db, access_token, ACCESS) + if not payload: + return None, None, None + + user_id = payload.get('sub') + org_id = payload.get('org') + + user = db.query(User).filter(User.id == UUID(str(user_id))).first() + if not user or not user.is_active: + return None, None, None + + return access_token, user_id, org_id + + except Exception as e: + logger.error(f"Authentication error for sid {sid}: {str(e)}") + return None, None, None + + +async def authenticate_socket_conversation_token(sid: str, auth: dict) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]: + """ + Authenticate widget socket connection using conversation token. + Supports both token-required and optional-token authentication modes. + + Returns: (widget_id, org_id, customer_id, conversation_token) + Returns (None, None, None, None) if authentication fails + """ + try: + if not auth or not isinstance(auth, dict): + logger.warning(f"Socket auth failed: No auth data for sid {sid}") + return None, None, None, None + conversation_token = None + + # Get token from Socket.IO auth data + conversation_token = auth.get('conversation_token', '') + + + if not conversation_token: + logger.info("No conversation token found in auth data or cookies") + return None, None, None, None + + # Verify token and get info + token_data = verify_conversation_token(conversation_token) + if not token_data: + return None, None, None, None + + widget_id = token_data.get('widget_id') + customer_id = token_data.get('sub') + token_type = token_data.get('type') + + # Verify token type + if token_type != "conversation": + logger.info(f"Invalid token type: {token_type}") + return None, None, None, None + + logger.info(f"Authenticated widget {widget_id} for customer {customer_id}") + + # Get widget to verify and get org_id + with SessionLocal() as db: + widget = db.query(Widget).filter(Widget.id == widget_id).first() + if not widget: + return None, None, None, None + + org_id = widget.organization_id + + # Resolve merged customers: if this device's token still carries an + # anonymous customer that was merged into an identified one (lead + # capture matched an existing email), continue as the merged target so + # returning visitors keep their identity and history. + if customer_id: + survivor = CustomerRepository(db).resolve_merged(customer_id) + if survivor is not None: + customer_id = str(survivor.id) + return widget_id, org_id, customer_id, conversation_token + + except Exception as e: + logger.error(f"Widget authentication error for sid {sid}: {str(e)}") + return None, None, None, None + + +def widget_socket_identity(session: Optional[dict]) -> Tuple[Optional[str], Optional[str], Optional[str]]: + """Identity of an already-connected widget socket, from its server-side session. + + The conversation token authenticates the *connection*: the /widget connect + handler runs authenticate_socket_conversation_token once and stores what it + resolved. Events arriving on a live connection read that back instead of + re-verifying the token, because the token's lifetime is not the connection's + lifetime — re-checking tore down working chats the moment the TTL passed + (mid-conversation, and on end_chat when the visitor closed the widget), and + it did the same whenever Redis lost the token's JTI. Expiry and revocation + still take effect: the next connect is authenticated normally. + + Returns (None, None, None) when there is no authenticated session for the sid. + """ + if not session: + return None, None, None + + widget_id = session.get('widget_id') + org_id = session.get('org_id') + if not widget_id or not org_id: + return None, None, None + + return widget_id, org_id, session.get('customer_id') diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..703996c --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,471 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import os +import json +from pydantic_settings import BaseSettings +from typing import List, Literal, NamedTuple, Optional +from dotenv import load_dotenv +from pathlib import Path +from pydantic import field_validator +from app.core.logger import get_logger + +# Get the absolute path to the backend directory (parent of app directory) +BACKEND_DIR = Path(__file__).resolve().parent.parent.parent + +# ENV_FILE points the backend at a different env file. The e2e suite sets it to +# e2e/.env.e2e so nothing from a developer's backend/.env (dev DB, dev Redis, +# real provider keys) leaks into a test run. +ENV_FILE = Path(os.getenv("ENV_FILE") or BACKEND_DIR / ".env") +load_dotenv(ENV_FILE) + +DEFAULT_CORS = ["https://replypier.site", "http://localhost:5173", "http://localhost:8000"] + +class Settings(BaseSettings): + PROJECT_NAME: str = "ReplyPier" + VERSION: str = "0.1.0" + API_V1_STR: str = "/api/v1" + + # Database + DATABASE_URL: str = os.getenv( + "DATABASE_URL", "postgresql+psycopg://test:test@localhost:5432/replypier") + # Redis + REDIS_URL: str = os.getenv("REDIS_URL", "redis://localhost:6379/0") + REDIS_ENABLED: bool = os.getenv("REDIS_ENABLED", "false").lower() == "true" + + # JWT + SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "your-secret-key") + ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256") + CONVERSATION_SECRET_KEY: str = os.getenv( + "CONVERSATION_SECRET_KEY", "your-conversation-secret-key") + ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + + # CORS Configuration + CORS_ORIGINS: List[str] = os.getenv("CORS_ORIGINS", DEFAULT_CORS) + + + # Firebase config + FIREBASE_CREDENTIALS: str = os.getenv( + "FIREBASE_CREDENTIALS", "app/config/firebase-config.json") + + FRONTEND_URL: str = os.getenv("FRONTEND_URL", "http://localhost:5173") + BACKEND_URL: str = os.getenv("BACKEND_URL", "http://localhost:8000") + # Where the widget bundle (widget.js / widget.css) is served from at /assets. + # The e2e suite points this at a bundle built from the current source, so the + # committed production build in backend/assets is never rebuilt locally. + WIDGET_ASSETS_DIR: str = os.getenv("WIDGET_ASSETS_DIR", str(BACKEND_DIR / "assets")) + VITE_WIDGET_URL: str = os.getenv("VITE_WIDGET_URL", "http://localhost:5173") + + # app.core.encryption owns the actual key loading (reads the env var directly and + # refuses to start without it outside development). This mirror exists only so + # check_secret_configuration can audit it, hence no default: unset is itself + # one of the states that audit flags. + ENCRYPTION_KEY: str = os.getenv("ENCRYPTION_KEY", "") + # Downgrades the public-default ENCRYPTION_KEY startup error to a warning, for + # deployments whose existing data is encrypted under that key. See + # verify_secret_configuration. + ALLOW_INSECURE_ENCRYPTION_KEY: bool = os.getenv( + "ALLOW_INSECURE_ENCRYPTION_KEY", "false").lower() == "true" + + # SMTP Settings + SMTP_SERVER: str = os.getenv("SMTP_SERVER", "smtp.gmail.com") + SMTP_PORT: int = int(os.getenv("SMTP_PORT", "587")) + SMTP_USERNAME: str = os.getenv("SMTP_USERNAME", "your-email@gmail.com") + SMTP_PASSWORD: str = os.getenv("SMTP_PASSWORD", "your-password") + FROM_EMAIL: str = os.getenv("FROM_EMAIL", "noreply@replypier.site") + FROM_NAME: str = os.getenv("FROM_NAME", "ReplyPier") + + # Shopify + SHOPIFY_API_KEY: str = os.getenv("SHOPIFY_API_KEY", "") + SHOPIFY_API_SECRET: str = os.getenv("SHOPIFY_API_SECRET", "") + SHOPIFY_API_VERSION: str = os.getenv("SHOPIFY_API_VERSION", "2025-10") + + # Slack. The OAuth redirect URI is derived from BACKEND_URL, not configured. + SLACK_CLIENT_ID: str = os.getenv("SLACK_CLIENT_ID", "") + SLACK_CLIENT_SECRET: str = os.getenv("SLACK_CLIENT_SECRET", "") + SLACK_SIGNING_SECRET: str = os.getenv("SLACK_SIGNING_SECRET", "") + + # Jira. Unlike the others the redirect URI IS configured, because it must + # match the callback registered on the Atlassian app exactly. Self-hosters + # supply their own OAuth app creds. + JIRA_CLIENT_ID: str = os.getenv("JIRA_CLIENT_ID", "") + JIRA_CLIENT_SECRET: str = os.getenv("JIRA_CLIENT_SECRET", "") + JIRA_REDIRECT_URI: str = os.getenv("JIRA_REDIRECT_URI", "") + + # CRM lead push (HubSpot / Pipedrive). OAuth redirect URIs are derived from + # BACKEND_URL, not configured. Self-hosters supply their own OAuth app creds. + HUBSPOT_CLIENT_ID: str = os.getenv("HUBSPOT_CLIENT_ID", "") + HUBSPOT_CLIENT_SECRET: str = os.getenv("HUBSPOT_CLIENT_SECRET", "") + PIPEDRIVE_CLIENT_ID: str = os.getenv("PIPEDRIVE_CLIENT_ID", "") + PIPEDRIVE_CLIENT_SECRET: str = os.getenv("PIPEDRIVE_CLIENT_SECRET", "") + + # Meta (WhatsApp Cloud API, Messenger, Instagram) — one app, shared webhook. + # Self-hosters supply their own app; the cloud supplies its approved app. + META_APP_ID: str = os.getenv("META_APP_ID", "") + META_APP_SECRET: str = os.getenv("META_APP_SECRET", "") + # Our own random token echoed back during webhook GET verification + META_WEBHOOK_VERIFY_TOKEN: str = os.getenv("META_WEBHOOK_VERIFY_TOKEN", "") + META_GRAPH_VERSION: str = os.getenv("META_GRAPH_VERSION", "v21.0") + # WhatsApp Embedded Signup config id (cloud onboarding convenience) + META_CONFIG_ID: str = os.getenv("META_CONFIG_ID", "") + # Facebook Login for Business config id, for connecting a Page (Messenger and + # Instagram DM both ride on the Page's token). A separate configuration from + # META_CONFIG_ID: it requests pages_messaging + pages_show_list and returns a + # user token, not a WhatsApp signup. + META_MESSENGER_CONFIG_ID: str = os.getenv("META_MESSENGER_CONFIG_ID", "") + # Instagram API with Instagram Login: its own app id/secret, separate from + # the Facebook ones above. This flow needs no Facebook Page — the business + # signs in with Instagram and we get an Instagram user token. + INSTAGRAM_APP_ID: str = os.getenv("INSTAGRAM_APP_ID", "") + INSTAGRAM_APP_SECRET: str = os.getenv("INSTAGRAM_APP_SECRET", "") + # Comma-separated emails allowed to use one-click Meta signup. Empty means + # everyone, which is the end state — this exists so the flow can be exercised + # in production while the Meta app is still in App Review, since until it is + # approved the login only works for people with a role on the app anyway. + SIGNUP_ALLOWED_EMAILS: str = os.getenv("SIGNUP_ALLOWED_EMAILS", "") + + VERIFY_SSL_CERTIFICATES: bool = os.getenv("VERIFY_SSL_CERTIFICATES", "true").lower() == "true" + # Which addresses the AI base URL, MCP server URLs and ticket databases may + # point at (see app/core/outbound_policy.py). "any" suits a self-hosted + # install reaching its own network; "public_only" refuses private, + # loopback, link-local and reserved targets on a shared deployment. + OUTBOUND_URL_POLICY: Literal["any", "public_only"] = os.getenv("OUTBOUND_URL_POLICY", "any") + # STDIO MCP connectors start a process on this server from a command an org + # admin types. Fine on your own install; on a shared deployment it hands + # every customer a shell, so turn it off there. + MCP_STDIO_ENABLED: bool = os.getenv("MCP_STDIO_ENABLED", "true").lower() == "true" + ENVIRONMENT: str = os.getenv("ENVIRONMENT", "development") + TRIAL_DAYS: int = 7 # 7-day trial period + + # S3 Configuration + S3_FILE_STORAGE: bool = os.getenv("S3_FILE_STORAGE", "false").lower() == "true" + S3_BUCKET: str = os.getenv("S3_BUCKET", "replypier-uploads") + S3_REGION: str = os.getenv("S3_REGION", "us-east-1") + # None, not "", when unset: boto3 only falls back to its default credential + # chain (env → shared config → IAM instance/container role) if these are + # None. An empty string is treated as an explicit credential, so requests + # get signed with a blank access key and fail with InvalidAccessKeyId — + # which is what blocks EC2/ECS/EKS deployments from using an attached role. + AWS_ACCESS_KEY_ID: Optional[str] = os.getenv("AWS_ACCESS_KEY_ID") or None + AWS_SECRET_ACCESS_KEY: Optional[str] = os.getenv("AWS_SECRET_ACCESS_KEY") or None + # Presigned URLs are regenerated on every response, so this only needs to + # outlive a single page render. Hard-capped at S3_MAX_PRESIGN_SECONDS. + S3_PRESIGN_EXPIRY_SECONDS: int = int(os.getenv("S3_PRESIGN_EXPIRY_SECONDS", "3600")) + + # Enhanced Website Knowledge Base Configuration + KB_MAX_DEPTH: int = int(os.getenv("KB_MAX_DEPTH", "5")) + KB_MAX_LINKS: int = int(os.getenv("KB_MAX_LINKS", "25")) + KB_MIN_CONTENT_LENGTH: int = int(os.getenv("KB_MIN_CONTENT_LENGTH", "100")) + KB_TIMEOUT: int = int(os.getenv("KB_TIMEOUT", "30")) + KB_MAX_RETRIES: int = int(os.getenv("KB_MAX_RETRIES", "3")) + KB_MAX_WORKERS: int = int(os.getenv("KB_MAX_WORKERS", "5")) + KB_BATCH_SIZE: int = int(os.getenv("KB_BATCH_SIZE", "5")) + # A website source that has not finished reading in this long fails rather + # than running on (one hit the worker's memory limit after three hours). + KB_SOURCE_TIME_LIMIT_SECONDS: int = int(os.getenv("KB_SOURCE_TIME_LIMIT_SECONDS", "1800")) + # Pages a PDF may have (app/knowledge/pdf_limits.py): every page is + # embedded, and a scanned one OCR'd, so bytes alone don't bound the work. + KB_PDF_MAX_PAGES: int = int(os.getenv("KB_PDF_MAX_PAGES", "300")) + + # Hard ceiling on a single agent run (model + tool calls). A stuck run is + # cancelled instead of hanging the chat handler forever (issue #269). + AGENT_RUN_TIMEOUT: int = int(os.getenv("AGENT_RUN_TIMEOUT", "90")) + + # agno's own run reporting to api.agno.com (see app.core.agno_telemetry). + # Off unless an operator opts in. + AGNO_TELEMETRY: bool = os.getenv("AGNO_TELEMETRY", "false").lower() == "true" + AGNO_MONITOR: bool = os.getenv("AGNO_MONITOR", "false").lower() == "true" + + # Tool calls allowed in a single turn before the agent must answer from what + # it already has. Reaching this is no longer fatal — the agent answers without + # further tools rather than returning nothing (see app.utils.agno_patches) — + # so the budget can stay tight. AGENT_RUN_TIMEOUT is the real safety net. + AGENT_TOOL_CALL_LIMIT: int = int(os.getenv("AGENT_TOOL_CALL_LIMIT", "5")) + + # Chars of previous-turn history re-sent to the model each turn. Tool + # results from earlier turns are always stubbed out (see + # app.agents.history_memory); this bounds what remains by dropping the oldest + # turns. Stored history is never touched. + AGENT_HISTORY_MAX_CHARS: int = int(os.getenv("AGENT_HISTORY_MAX_CHARS", "40000")) + + # Knowledge chunking. The search embedder (bge-small, 512-token window) only + # sees roughly this much text per row, so a bigger chunk is not searchable + # past this point — and crawled text is URL-dense (~3 chars/token). + KNOWLEDGE_CHUNK_SIZE: int = int(os.getenv("KNOWLEDGE_CHUNK_SIZE", "1100")) + KNOWLEDGE_CHUNK_OVERLAP: int = int(os.getenv("KNOWLEDGE_CHUNK_OVERLAP", "120")) + # Hits returned per knowledge search, and the ceiling on the rendered result + # (whole pages are dropped past it, a page is never cut mid-chunk). + KNOWLEDGE_SEARCH_RESULTS: int = int(os.getenv("KNOWLEDGE_SEARCH_RESULTS", "6")) + KNOWLEDGE_SEARCH_MAX_CHARS: int = int(os.getenv("KNOWLEDGE_SEARCH_MAX_CHARS", "15000")) + + # Knowledge base content summarization settings + KNOWLEDGE_SUMMARY_ENABLED: bool = os.getenv("KNOWLEDGE_SUMMARY_ENABLED", "false").lower() == "true" + KNOWLEDGE_SUMMARY_MODEL_TYPE: str = os.getenv("KNOWLEDGE_SUMMARY_MODEL_TYPE", "GROQ") + KNOWLEDGE_SUMMARY_MODEL_NAME: str = os.getenv("KNOWLEDGE_SUMMARY_MODEL_NAME", "llama-3.1-8b-instant") + KNOWLEDGE_SUMMARY_API_KEY: str = os.getenv("KNOWLEDGE_SUMMARY_API_KEY", "") + KNOWLEDGE_SUMMARY_MAX_TOKENS: int = int(os.getenv("KNOWLEDGE_SUMMARY_MAX_TOKENS", "4000")) + # Required when KNOWLEDGE_SUMMARY_MODEL_TYPE=OPENAI_COMPATIBLE, ignored otherwise. + KNOWLEDGE_SUMMARY_BASE_URL: str = os.getenv("KNOWLEDGE_SUMMARY_BASE_URL", "") + + # Global chat guardrails (platform-owned; deliberately NOT tenant-configurable). + # GUARDRAIL_POLICY_ENABLED -> prepend the code-owned policy block to every + # chat agent system prompt. + # GUARDRAIL_INBOUND_ACTION -> pre-LLM injection check on visitor messages: + # "off" (never block), "template_only" (block + # only literal chat-template tokens a human + # never types), "strict" (block every strong + # injection signal). Non-blocking hits are + # always still counted. + # GUARDRAIL_OUTPUT_CHECK_ENABLED -> scan replies for leaked policy text and + # count scope refusals. + # GUARDRAIL_EVENTS_ENABLED -> persist trigger events to guardrail_events. + # GUARDRAIL_STORE_EXCERPT -> store an encrypted 300-char excerpt of the + # offending text on each event for review. + GUARDRAIL_POLICY_ENABLED: bool = os.getenv("GUARDRAIL_POLICY_ENABLED", "true").lower() == "true" + GUARDRAIL_INBOUND_ACTION: str = os.getenv("GUARDRAIL_INBOUND_ACTION", "template_only") + # Long self-contained exercise briefs with no business context. Defaults to + # "block": prompt text demonstrably failed to hold these in production, and + # blocking here also avoids paying for the inference. "count" or "off" to + # relax without a deploy. + GUARDRAIL_OFFTOPIC_ACTION: str = os.getenv("GUARDRAIL_OFFTOPIC_ACTION", "block") + GUARDRAIL_OUTPUT_CHECK_ENABLED: bool = os.getenv("GUARDRAIL_OUTPUT_CHECK_ENABLED", "true").lower() == "true" + GUARDRAIL_EVENTS_ENABLED: bool = os.getenv("GUARDRAIL_EVENTS_ENABLED", "true").lower() == "true" + GUARDRAIL_STORE_EXCERPT: bool = os.getenv("GUARDRAIL_STORE_EXCERPT", "true").lower() == "true" + + # Help center (public FAQ site) + # How the public help center URL is advertised (live_url): + # "path" -> {BACKEND_URL}/help/{slug}, served same-origin as the API. + # Works on localhost/self-host with no DNS/TLS/proxy. Default. + # "subdomain" -> https://{slug}.<HELP_CENTER_BASE_DOMAIN> (cloud). MUST be set + # on cloud so subdomain help centers keep their branded URL. + # A verified custom domain always takes precedence over both. Host-based dispatch + # (subdomains + custom domains) stays active regardless; only path dispatch is gated + # to "path" mode. + HELP_CENTER_PUBLIC_MODE: str = os.getenv("HELP_CENTER_PUBLIC_MODE", "path") + # Base domain serving {slug}.<base> help centers. + HELP_CENTER_BASE_DOMAIN: str = os.getenv("HELP_CENTER_BASE_DOMAIN", "help.replypier.site") + # CNAME target customers point their custom help-center domain at. + HELP_CENTER_CNAME_TARGET: str = os.getenv("HELP_CENTER_CNAME_TARGET", "cname.replypier.site") + # IPs the CNAME target resolves to — accepted when a provider flattens the + # CNAME into A/AAAA records (comma-separated). + HELP_CENTER_TARGET_IPS: frozenset = frozenset( + ip.strip() for ip in os.getenv("HELP_CENTER_TARGET_IPS", "").split(",") if ip.strip() + ) + # FAQ generation cost caps (per source / per LLM call) and import fetch limits. + FAQ_MAX_PAGES_PER_SOURCE: int = int(os.getenv("FAQ_MAX_PAGES_PER_SOURCE", "300")) + FAQ_MAX_BATCH_CHARS: int = int(os.getenv("FAQ_MAX_BATCH_CHARS", "15000")) + # Ceiling for context-window-derived batch sizing (see utils/model_context.py) + # — a quality guard for very-large-context models, not a token limit. + FAQ_MAX_BATCH_CHARS_CEILING: int = int(os.getenv("FAQ_MAX_BATCH_CHARS_CEILING", "60000")) + # Force a context-window size (tokens) for exotic/self-hosted models; 0 = auto. + FAQ_CONTEXT_TOKENS_OVERRIDE: int = int(os.getenv("FAQ_CONTEXT_TOKENS_OVERRIDE", "0")) + # Meter FAQ generation credits even for orgs on their own API key + # (default: hosted REPLYPIER model only). + FAQ_METER_OWN_KEY: bool = os.getenv("FAQ_METER_OWN_KEY", "false").lower() == "true" + FAQ_IMPORT_MAX_PAGE_CHARS: int = int(os.getenv("FAQ_IMPORT_MAX_PAGE_CHARS", "100000")) + FAQ_IMPORT_FETCH_TIMEOUT: int = int(os.getenv("FAQ_IMPORT_FETCH_TIMEOUT", "30")) + # Article-mode import (crawl linked pages, no LLM): crawl and re-host caps. + # High enough to pull a whole mid-size help center in one pass; a deliberate + # one-time migration in a background worker, so slowness is acceptable. + FAQ_ARTICLE_IMPORT_MAX_PAGES: int = int(os.getenv("FAQ_ARTICLE_IMPORT_MAX_PAGES", "200")) + FAQ_ARTICLE_IMPORT_MAX_IMAGES: int = int(os.getenv("FAQ_ARTICLE_IMPORT_MAX_IMAGES", "10")) + # Category/section listing pages to follow for the full per-category article + # list (help-center homepages truncate each section to a few articles). + FAQ_ARTICLE_IMPORT_MAX_CATEGORIES: int = int(os.getenv("FAQ_ARTICLE_IMPORT_MAX_CATEGORIES", "20")) + # A 'processing' FAQ job whose progress hasn't advanced in this long is + # treated as dead (worker crashed/killed): excluded from active-job polling + # and reaped on the next enqueue. Generous — must exceed the slowest single + # LLM batch / page fetch so a live-but-slow job is never killed. + FAQ_JOB_STALE_SECONDS: int = int(os.getenv("FAQ_JOB_STALE_SECONDS", "600")) + # FAQ generation/import jobs processed concurrently across ALL orgs. Default + # 1 = strictly one business at a time (each job does LLM calls + vector-DB + # reads); raise for more throughput on a bigger host. + MAX_CONCURRENT_FAQ_JOBS: int = int(os.getenv("MAX_CONCURRENT_FAQ_JOBS", "1")) + # AI ticket triage/investigation runs processed concurrently across ALL + # orgs (each run does LLM calls; investigations later add MCP subprocesses). + MAX_CONCURRENT_INVESTIGATIONS: int = int(os.getenv("MAX_CONCURRENT_INVESTIGATIONS", "2")) + # Subdomain labels reserved for infrastructure — must mirror the DNS/nginx + # records that exist on the base domain, hence env-configurable. + HELP_CENTER_RESERVED_SLUGS: frozenset = frozenset( + s.strip() for s in os.getenv( + "HELP_CENTER_RESERVED_SLUGS", + "www,api,app,help,mail,admin,staging,cname,status", + ).split(",") if s.strip() + ) + + # Embedding Model Configuration + EMBEDDING_BATCH_SIZE: int = int(os.getenv("EMBEDDING_BATCH_SIZE", "32")) + EMBEDDING_MAX_WORKERS: int = int(os.getenv("EMBEDDING_MAX_WORKERS", "4")) + + # FastEmbed Configuration + FASTEMBED_MODEL: str = os.getenv("FASTEMBED_MODEL", "BAAI/bge-small-en-v1.5") + + # Embedding Optimization Configuration + ENABLE_IMMEDIATE_EMBEDDING: bool = os.getenv("ENABLE_IMMEDIATE_EMBEDDING", "true").lower() == "true" + + # Embedding Safety Configuration (for Docker environments) + EMBEDDING_SINGLE_THREADED: bool = os.getenv("EMBEDDING_SINGLE_THREADED", "true").lower() == "true" + EMBEDDING_SEQUENTIAL_FALLBACK: bool = os.getenv("EMBEDDING_SEQUENTIAL_FALLBACK", "true").lower() == "true" + + # Explore View Configuration + EXPLORE_SOURCE_ORG_ID: str = os.getenv("EXPLORE_SOURCE_ORG_ID", "bab82aab-d095-46f8-bf16-da638671bcf4") + EXPLORE_AGENT_ID: str = os.getenv("EXPLORE_AGENT_ID", "b20188ee-2800-41d0-8bf1-8fc291ab0076") + EXPLORE_USER_ID: str = os.getenv("EXPLORE_USER_ID", "154540a3-6177-4b1b-aab2-f23f0ef74ac7") + EXPLORE_WIDGET_ID: str = os.getenv("EXPLORE_WIDGET_ID", "397046dc-0093-4499-ab45-a0afe3c3ee14") + + @field_validator("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", mode="after") + @classmethod + def _blank_credential_is_unset(cls, v: Optional[str]) -> Optional[str]: + """Coerce a blank credential to None so boto3 uses its default chain. + + The field default already does this, but BaseSettings reads the + environment itself and overrides it — a var that is present but empty + (`AWS_ACCESS_KEY_ID=` in .env, or an env_file entry with no value) + lands as "" and would be signed with as an explicit blank credential. + """ + return v or None + + model_config = { + "case_sensitive": True, + "env_file": ENV_FILE, + "extra": "allow", # This allows extra fields from .env + } + +settings = Settings() + + +def webhooks_base_url() -> str: + """Where platforms reach this backend's inbound webhooks.""" + return f"{settings.BACKEND_URL.rstrip('/')}{settings.API_V1_STR}/webhooks" + +logger = get_logger(__name__) + + +# Environments where the public defaults are acceptable, because nothing durable or +# reachable is protected by them. Anything else is treated as a real deployment. +_THROWAWAY_ENVIRONMENTS = frozenset({"development", "test", "testing"}) + +# Shell one-liners producing a real value for each kind of secret, quoted back in +# the startup error so the fix does not require going and finding the docs. +# ENCRYPTION_KEY_HINT is public because app.core.encryption reports the same fix +# when the key is missing at load time, and the two must not drift apart. +ENCRYPTION_KEY_HINT = ( + 'python -c "import base64;from cryptography.fernet import Fernet;' + 'print(base64.b64encode(Fernet.generate_key()).decode())"' +) +_HEX_SECRET_HINT = "openssl rand -hex 32" + + +class _ManagedSecret(NamedTuple): + """A secret the app refuses to serve traffic without. + + One record per secret rather than parallel dicts keyed by setting name: adding + a secret should be one line, and a half-filled entry should not surface as a + KeyError while building the very error meant to explain the problem. + """ + + # What the operator sets, which is not always the setting name. + env_var: str + # Value once shipped in this repo (config default / .env.example). Public, so + # it stays listed after being removed as a default — deployments copied it. + public_default: str + # Shell one-liner producing a real replacement. + generate: str + + +_MANAGED_SECRETS = { + "SECRET_KEY": _ManagedSecret( + "JWT_SECRET_KEY", "your-secret-key", _HEX_SECRET_HINT), + "CONVERSATION_SECRET_KEY": _ManagedSecret( + "CONVERSATION_SECRET_KEY", "your-conversation-secret-key", _HEX_SECRET_HINT), + "ENCRYPTION_KEY": _ManagedSecret( + "ENCRYPTION_KEY", + "RFQ4SzhyRTVYdGtsLUxsc25SaDB0QlZpbTdQRmlVRlpsZUlCaFRlU2Vxbz0=", + ENCRYPTION_KEY_HINT), +} + +# .env.example placeholders - not secret either, and they mean "never configured" +_PLACEHOLDER_VALUES = { + "your_jwt_secret_key_here", + "your_conversation_secret_key_here", + "your_fernet_encryption_key_here", +} + + +def is_throwaway_environment(config: Settings = settings) -> bool: + """True for local development and test runs, where the public defaults are + harmless. Defined here so every check of "is this a real deployment" agrees; + app.core.encryption asks the same question before generating a throwaway key. + """ + return config.ENVIRONMENT.lower() in _THROWAWAY_ENVIRONMENTS + + +def check_secret_configuration(config: Settings = settings) -> list[str]: + """Names of the auth/encryption secrets that are missing or still at a public + default/placeholder. Empty in development, which runs on the defaults on purpose. + + Pure audit: verify_secret_configuration decides what to do about the result. + """ + if is_throwaway_environment(config): + return [] + + return [ + name for name, secret in _MANAGED_SECRETS.items() + if not getattr(config, name, None) + or getattr(config, name) == secret.public_default + or getattr(config, name) in _PLACEHOLDER_VALUES + ] + + +def verify_secret_configuration(config: Settings = settings) -> None: + """Refuse to start on secrets anyone can look up in this repo. + + Every value in _MANAGED_SECRETS and _PLACEHOLDER_VALUES is public, so a + deployment using one has no auth boundary at all — tokens can be forged and + stored credentials decrypted by anyone. A warning was not enough: it scrolls + past in the boot log and the deployment stays exposed indefinitely. + + ENCRYPTION_KEY alone gets an escape hatch. An instance predating the required + key has real data encrypted under the public one, and refusing to boot would + lock it out of that data — worse than the exposure it already lives with. + Rotating a JWT secret only ends sessions, so those get no exemption. + """ + insecure = check_secret_configuration(config) + + if "ENCRYPTION_KEY" in insecure and config.ALLOW_INSECURE_ENCRYPTION_KEY: + logger.warning( + "ENCRYPTION_KEY is the public default and ALLOW_INSECURE_ENCRYPTION_KEY " + "is set, so startup continues. Conversations and stored credentials in " + "this database can be decrypted by anyone who has a copy of it. Generate " + "a real key (%s), re-encrypt what the old key wrote, then unset this.", + ENCRYPTION_KEY_HINT, + ) + insecure = [name for name in insecure if name != "ENCRYPTION_KEY"] + + if not insecure: + return + + names = ", ".join(insecure) + verb = "is" if len(insecure) == 1 else "are" + fixes = "\n".join( + f" {_MANAGED_SECRETS[name].env_var}=$({_MANAGED_SECRETS[name].generate})" + for name in insecure + ) + raise RuntimeError( + f"Refusing to start: {names} {verb} unset or still set to a value published " + "in this repository, so anyone can forge tokens or decrypt stored data. " + f"Set real values and restart:\n{fixes}" + ) diff --git a/backend/app/core/cors.py b/backend/app/core/cors.py new file mode 100644 index 0000000..5d4d5c3 --- /dev/null +++ b/backend/app/core/cors.py @@ -0,0 +1,387 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import json +import os +import time +import asyncio +import traceback +import re +from urllib.parse import urlparse +from typing import Iterable, List, Optional, Set +from functools import lru_cache +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from starlette.datastructures import Headers, MutableHeaders +from starlette.types import Message, Send + +from app.core.config import settings +from app.core.logger import get_logger +from app.utils.domain_check import site_host + +logger = get_logger(__name__) + +# Base CORS origins that are always allowed +BASE_CORS_ORIGINS: list = settings.CORS_ORIGINS + +_CREDENTIALS_HEADER = "access-control-allow-credentials" +_DEFAULT_PORTS = {"http": 80, "https": 443} + + +def origin_of(url: Optional[str]) -> Optional[str]: + """The origin of a URL the way a browser sends it: lowercase, no path and + no default port, so https://App.example.com:443/ is https://app.example.com.""" + parsed = urlparse((url or "").strip()) + if parsed.scheme not in _DEFAULT_PORTS or not parsed.hostname: + return None + try: + port = parsed.port + except ValueError: + return None + netloc = parsed.hostname if port in (None, _DEFAULT_PORTS[parsed.scheme]) else f"{parsed.hostname}:{port}" + return f"{parsed.scheme}://{netloc}".lower() + + +@lru_cache(maxsize=1) +def first_party_origins() -> Set[str]: + """Origins that may use the dashboard's sign-in cookies: the dashboard + (FRONTEND_URL) and what the operator listed in CORS_ORIGINS. + + Never an org's domain or a help centre. An org domain is whatever someone + typed at signup, unverified, so trusting it with credentials would let a + page on any signed-up domain read a signed-in user's account. Read from + settings once: every /agent socket connect asks. + """ + dashboard = origin_of(settings.FRONTEND_URL) + if not dashboard: + logger.error(f"FRONTEND_URL {settings.FRONTEND_URL!r} is not an http(s) URL; the dashboard can't sign in cross-origin") + origins = {dashboard, *(origin_of(url) for url in BASE_CORS_ORIGINS)} + return {o for o in origins if o} + + +def is_first_party_origin(origin: Optional[str]) -> bool: + return origin_of(origin) in first_party_origins() + + +def with_configured_origins(origins: Iterable[str]) -> List[str]: + """The given origins plus the operator's own and the API's, sorted. A + cached list from Redis was built from an earlier config, and must never drop + the dashboard. The API's own origin is where the widget's iframe snippet and + path-mode help centres run, and their sockets send it as Origin.""" + own = {origin_of(settings.BACKEND_URL)} - {None} + return sorted(set(origins) | set(BASE_CORS_ORIGINS) | first_party_origins() | own) + + +class FirstPartyCredentialsCORSMiddleware(CORSMiddleware): + """CORS for every allowed origin, credentials only for first-party ones. + + Org domains and help centres still call the API cross-origin (older + widget loaders fetch /data from customers' sites), but without cookies: + Access-Control-Allow-Credentials is only sent to `credentialed_origins`, + so a browser never lets another site read a cookie-authenticated response. + """ + + def __init__(self, app, credentialed_origins: Iterable[str] = (), **kwargs) -> None: + kwargs["allow_credentials"] = True + super().__init__(app, **kwargs) + self.credentialed_origins = frozenset(filter(None, map(origin_of, credentialed_origins))) + + def _credentialed(self, origin: str) -> bool: + return origin_of(origin) in self.credentialed_origins + + def preflight_response(self, request_headers: Headers): + response = super().preflight_response(request_headers) + if not self._credentialed(request_headers["origin"]) and _CREDENTIALS_HEADER in response.headers: + del response.headers[_CREDENTIALS_HEADER] + return response + + async def send(self, message: Message, send: Send, request_headers: Headers) -> None: + if self._credentialed(request_headers["Origin"]): + await super().send(message, send, request_headers) + return + + async def without_credentials(outgoing: Message) -> None: + if outgoing["type"] == "http.response.start": + headers = MutableHeaders(scope=outgoing) + if _CREDENTIALS_HEADER in headers: + del headers[_CREDENTIALS_HEADER] + await send(outgoing) + + await super().send(message, without_credentials, request_headers) + + +@lru_cache(maxsize=1) +def get_cors_origins() -> Set[str]: + """ + Get all allowed CORS origins including organization domains. + This function is cached to improve performance. + """ + from app.repositories.organization import OrganizationRepository + from app.database import SessionLocal + + # Start with base origins + all_origins = set(BASE_CORS_ORIGINS) + + # Add organization domains + try: + db = SessionLocal() + org_repo = OrganizationRepository(db) + orgs = org_repo.get_active_organizations() + + for org in orgs: + # The site with and without www., over http and https. Normalised: + # a domain typed as "www.Acme.com" must admit https://acme.com + # (browsers send Origin lowercase), not www.www.Acme.com. + host = site_host(org.domain or "") + if not host: + continue + for scheme in ("https", "http"): + all_origins.add(f"{scheme}://{host}") + all_origins.add(f"{scheme}://www.{host}") + + # Help-center hosts embed the org's chat widget, which fetches + # /widgets/{id}/data and opens a socket from the public help-center + # origin — so those origins must be allowed too. The base-domain + # wildcard is also covered by get_cors_origin_regex() for FastAPI, but + # python-socketio can't match a regex, so enumerate the live slugs here. + from app.repositories.help_center import HelpCenterRepository + hc_repo = HelpCenterRepository(db) + base = settings.HELP_CENTER_BASE_DOMAIN + # In local dev the help center is reached on a non-standard port (e.g. + # :8000), which the browser puts in the Origin header. socket.io matches + # origins exactly (no regex), so add that port variant when configured. + dev_port = urlparse(settings.BACKEND_URL).port + for slug in hc_repo.list_enabled_slugs(): + for scheme in ("https", "http"): + all_origins.add(f"{scheme}://{slug}.{base}") + if dev_port: + all_origins.add(f"{scheme}://{slug}.{base}:{dev_port}") + for domain in hc_repo.list_verified_domains(): + all_origins.add(f"https://{domain}") + all_origins.add(f"http://{domain}") + except Exception as e: + logger.error(f"Error fetching organization domains: {e}") + finally: + if 'db' in locals(): + db.close() + + return all_origins + + +@lru_cache(maxsize=1) +def get_cors_origin_regex() -> str: + """Regex allowing every help-center subdomain (`*.{base_domain}`) for the + FastAPI CORS middleware, so a new slug works without a cache refresh. + Socket.IO can't use this (no regex support) — it relies on the enumerated + origins in get_cors_origins().""" + base = re.escape(settings.HELP_CENTER_BASE_DOMAIN) + return rf"^https?://([a-z0-9-]+\.)?{base}(:\d+)?$" + +def refresh_cors_origins() -> None: + """ + Clear the CORS origins cache to force a refresh + """ + get_cors_origins.cache_clear() + +def update_local_cors(app: FastAPI, new_origins: List[str]) -> bool: + """ + Update local CORS middleware with new origins + + Args: + app: FastAPI application instance + new_origins: List of CORS origins to set + + Returns: + bool: True if CORS middleware was found and updated, False otherwise + """ + middleware_found = False + new_origins = with_configured_origins(new_origins) + + # Update the registered CORS middleware in place. `user_middleware` holds + # the class plus its kwargs, so match on the class itself; adding another + # layer instead would leave the old ones answering for removed origins. + for middleware in getattr(app, 'user_middleware', []): + if isinstance(middleware.cls, type) and issubclass(middleware.cls, CORSMiddleware): + # The publishing worker also hears its own Redis broadcast, and + # startup re-applies the cached list; skip the rebuild when nothing changed + if set(middleware.kwargs.get('allow_origins') or ()) != set(new_origins): + middleware.kwargs['allow_origins'] = new_origins + # Starlette rebuilds the stack from user_middleware on the next request + app.middleware_stack = None + logger.info(f"Updated FastAPI CORS origins: {new_origins}") + middleware_found = True + break + + # No CORS middleware registered yet: add one + if not middleware_found: + logger.warning("Could not find CORS middleware in app.user_middleware, applying global update") + try: + # Force FastAPI to rebuild the middleware stack + app.middleware_stack = None + # Re-add the middleware with updated origins. Keep the help-center + # subdomain regex here too, or a rebuild would silently drop the + # *.{base_domain} wildcard that the embedded widget relies on. + app.add_middleware( + FirstPartyCredentialsCORSMiddleware, + credentialed_origins=first_party_origins(), + allow_origins=new_origins, + allow_origin_regex=get_cors_origin_regex(), + allow_methods=["*"], + allow_headers=["*"], + ) + logger.info(f"Added new CORS middleware with origins: {new_origins}") + middleware_found = True + except Exception as e: + logger.error(f"Failed to rebuild middleware stack: {str(e)}") + + # Update Socket.IO CORS settings + try: + from app.core.socketio import sio + sio.eio.cors_allowed_origins = new_origins + logger.info(f"Updated Socket.IO CORS origins: {new_origins}") + except Exception as e: + logger.error(f"Failed to update Socket.IO CORS origins: {str(e)}") + + return middleware_found + +def update_cors_middleware(app: FastAPI) -> None: + """ + Update the CORS middleware with new origins and notify all workers + """ + logger.info("Refreshing CORS origins") + + # Get fresh CORS origins + refresh_cors_origins() + new_origins = list(get_cors_origins()) + + # Update local instance + update_local_cors(app, new_origins) + + # Store updated origins in Redis with timestamp for other workers + try: + from app.core.redis import get_redis + redis_client = get_redis() + + if redis_client: + # Store CORS origins with timestamp in Redis + redis_client.set( + "cors:origins", + json.dumps({"origins": new_origins, "updated_at": time.time()}) + ) + + # Publish event to notify other workers + redis_client.publish("cors:update", "refresh") + logger.info("Published CORS update to other workers via Redis") + else: + logger.warning("Redis not available, CORS update will only apply to this worker") + except Exception as e: + logger.error(f"Failed to publish CORS update to Redis: {str(e)}") + +async def listen_for_cors_updates(app: FastAPI): + """ + Listen for CORS update notifications from Redis using an efficient pattern + """ + from app.core.redis import get_redis + redis_client = get_redis() + + if not redis_client: + logger.warning("Redis not available, skipping CORS update listener") + return + + pubsub = None + try: + # Use ignore_subscribe_messages to reduce noise + pubsub = redis_client.pubsub(ignore_subscribe_messages=True) + pubsub.subscribe("cors:update") + logger.info("Started CORS update listener") + + # Apply any existing CORS config on startup + try: + cached = redis_client.get("cors:origins") + if cached: + cached_data = json.loads(cached) + if isinstance(cached_data, dict) and "origins" in cached_data: + update_local_cors(app, cached_data["origins"]) + except Exception: + # Best-effort; continue + pass + + # Non-blocking loop using get_message without blocking the event loop + while True: + try: + # timeout=0.0 ensures this call is non-blocking in redis-py + message = pubsub.get_message(timeout=0.0) + if message: + try: + origins_data = redis_client.get("cors:origins") + if origins_data: + origins_data = json.loads(origins_data) + if origins_data and "origins" in origins_data: + logger.info("Received CORS update notification") + update_local_cors(app, origins_data["origins"]) + except Exception as inner_e: + logger.error(f"Error processing CORS update: {str(inner_e)}") + # Yield control to the event loop and poll at a light cadence + await asyncio.sleep(0.5) + except Exception as loop_e: + # TimeoutError or transient connection issues: log at warning and continue + logger.warning(f"CORS listener transient error: {str(loop_e)}") + await asyncio.sleep(1.0) + + except Exception as e: + traceback.print_exc() + logger.error(f"Error in CORS update listener: {str(e)}") + finally: + # Try to clean up subscription + try: + if pubsub is not None: + pubsub.unsubscribe("cors:update") + pubsub.close() + except Exception: + pass + +def start_cors_listener(app: FastAPI): + """ + Start background task to listen for CORS updates + """ + try: + # Check if Redis is available before starting listener + from app.core.redis import get_redis + redis_client = get_redis() + + if redis_client: + # Create background task with proper error handling + background_task = asyncio.create_task(listen_for_cors_updates(app)) + + # Add exception handler to restart if it fails + def handle_exception(task): + if not task.cancelled() and task.exception(): + logger.error(f"CORS listener crashed: {task.exception()}") + # Wait a moment before restarting + asyncio.create_task(restart_listener(app)) + + async def restart_listener(app): + await asyncio.sleep(5) # Wait before restarting + logger.info("Restarting CORS listener") + new_task = asyncio.create_task(listen_for_cors_updates(app)) + new_task.add_done_callback(handle_exception) + + background_task.add_done_callback(handle_exception) + logger.info("Started CORS update listener task") + else: + logger.warning("Redis not available, CORS synchronization between workers disabled") + except Exception as e: + logger.error(f"Failed to start CORS listener: {str(e)}") \ No newline at end of file diff --git a/backend/app/core/default_templates.py b/backend/app/core/default_templates.py new file mode 100644 index 0000000..9edb966 --- /dev/null +++ b/backend/app/core/default_templates.py @@ -0,0 +1,116 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from typing import Dict, List +from app.models.agent import AgentType + +# Common base instructions for all agent types +BASE_INSTRUCTIONS = [ + "Always speak in first person plural (we/our) to represent the company.", + "Be professional yet approachable in your responses.", + "Take ownership of customer concerns and show empathy.", + "Use phrases like 'we understand', 'our team', 'we can help'.", + "If you don't know something, say 'Let me check with our team'.", + "Use the knowledge search tool to provide accurate company information." +] + +DEFAULT_BUSINESS_HOURS = { + "monday": { + "start": "09:00", + "end": "17:00", + "enabled": True + }, + "tuesday": { + "start": "09:00", + "end": "17:00", + "enabled": True + }, + "wednesday": { + "start": "09:00", + "end": "17:00", + "enabled": True + }, + "thursday": { + "start": "09:00", + "end": "17:00", + "enabled": True + }, + "friday": { + "start": "09:00", + "end": "17:00", + "enabled": True + }, + "saturday": { + "start": "09:00", + "end": "17:00", + "enabled": False + }, + "sunday": { + "start": "09:00", + "end": "17:00", + "enabled": False + } +} + +DEFAULT_TEMPLATES: Dict[AgentType, Dict] = { + AgentType.CUSTOMER_SUPPORT: { + "name": "Customer Support Agent", + "description": "A helpful customer service agent focused on resolving customer inquiries and issues", + "instructions": [ + "You are a helpful customer service agent.", + *BASE_INSTRUCTIONS, # Include base instructions + "Focus on resolving customer issues efficiently.", + "Ask clarifying questions when needed.", + "Provide clear step-by-step solutions.", + "Show empathy and understanding.", + "Follow up to ensure customer satisfaction.", + "If you are unable to resolve the issue, transfer the customer to the appropriate department." + ], + "tools": [] + }, + AgentType.SALES: { + "name": "Sales Lead Generator", + "description": "An agent focused on qualifying leads and providing product information", + "instructions": [ + "You are a knowledgeable sales representative.", + *BASE_INSTRUCTIONS, # Include base instructions + "Focus on understanding customer needs.", + "Provide relevant product information.", + "Highlight key benefits and features.", + "Ask qualifying questions.", + "Handle objections professionally.", + "Guide prospects through the sales process.", + "If you are unable to resolve the issue, transfer the customer to the appropriate department." + ], + "tools": [] + }, + AgentType.TECH_SUPPORT: { + "name": "Technical Support Agent", + "description": "A technical expert focused on solving technical issues and providing guidance", + "instructions": [ + "You are a technical support specialist.", + *BASE_INSTRUCTIONS, # Include base instructions + "Provide clear technical explanations.", + "Guide users through troubleshooting steps.", + "Use technical knowledge base when available.", + "Explain solutions in user-friendly terms.", + "Verify problem resolution.", + "Document solutions for future reference.", + "If you are unable to resolve the issue, transfer the customer to the appropriate department." + ], + "tools": [] + } +} diff --git a/backend/app/core/edition.py b/backend/app/core/edition.py new file mode 100644 index 0000000..b8171c6 --- /dev/null +++ b/backend/app/core/edition.py @@ -0,0 +1,54 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# NO_ENTERPRISE=1 runs the backend as the community edition even when the +# enterprise submodule is checked out. +# +# CI and self-hosted builds have app/enterprise empty, so every +# `try: import app.enterprise` falls back to community mode. A developer +# checkout has the submodule populated, which switches on plan and +# subscription gating. Setting this makes the import fail the same way, so the +# unit suite (`NO_ENTERPRISE=1 python -m pytest tests/`) and the e2e OSS stack +# behave exactly like a community install. +# +# An env var rather than a flag because the blocker must be installed before +# anything imports app.enterprise — app/__init__.py calls apply() first thing. + +import os +import sys + + +class _BlockEnterprise: + """Meta-path finder that makes app.enterprise look un-checked-out.""" + + def find_spec(self, fullname, path=None, target=None): + if fullname == "app.enterprise" or fullname.startswith("app.enterprise."): + # ModuleNotFoundError, not bare ImportError: that is what an + # un-checked-out submodule actually raises, and it is what + # pytest.importorskip and the app's own guards look for. + raise ModuleNotFoundError( + f"No module named {fullname!r} (disabled by NO_ENTERPRISE=1)", + name=fullname, + ) + return None + + +def apply() -> None: + """Install the enterprise blocker when NO_ENTERPRISE=1 (idempotent).""" + if os.getenv("NO_ENTERPRISE") != "1": + return + if not any(isinstance(finder, _BlockEnterprise) for finder in sys.meta_path): + sys.meta_path.insert(0, _BlockEnterprise()) diff --git a/backend/app/core/encryption.py b/backend/app/core/encryption.py new file mode 100644 index 0000000..212f65a --- /dev/null +++ b/backend/app/core/encryption.py @@ -0,0 +1,223 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Encryption of user content at rest. + +Conversation content (chat messages, AI-written session summaries, the agno agent +memory blob) is stored as ciphertext so that a database dump, a stolen backup or a +stray `psql` session does not expose it. The application decrypts transparently. + +Stored format is ``enc:<key_id>:<fernet-token>``. The key id is what makes key +rotation possible without a schema change: a future key is added to KEYS as ``v2``, +CURRENT_KEY_ID moves to it, and ``v1`` stays in the registry so old rows keep +reading while a backfill re-encrypts them. + +This module deliberately depends on nothing but the standard library, cryptography, +SQLAlchemy and app.core.{logger,config} (both light), so the backfill script can load +it by file path on the host without importing the `app` package — whose __init__ +pulls in the FastAPI app and the whole ML stack. +""" + +import base64 +import json +import os +from functools import lru_cache +from typing import Any, Dict, Optional + +from cryptography.fernet import Fernet +from sqlalchemy import Text +from sqlalchemy.types import TypeDecorator + +from app.core.config import ENCRYPTION_KEY_HINT, is_throwaway_environment +from app.core.logger import get_logger + +logger = get_logger(__name__) + +# Wire format. Nothing outside this module should restate these. +ENCRYPTED_PREFIX = "enc" +SEPARATOR = ":" +CURRENT_KEY_ID = "v1" +# Key under which an encrypted JSON blob is stored, so an encrypted dict is +# distinguishable from a plaintext one written before this change. +JSON_MARKER = "__enc__" + +ENCRYPTION_KEY_ENV = "ENCRYPTION_KEY" + + +# Indented for the multi-line error messages below; the command itself is defined +# once in app.core.config so this and the startup audit cannot drift apart. +GENERATE_KEY_HINT = f" {ENCRYPTION_KEY_HINT}" + + +def load_key() -> bytes: + """Return the raw Fernet key from ``ENCRYPTION_KEY``. + + The env var holds the Fernet key base64-encoded a second time, which is the + format already in use for API-key encryption — decoding must stay identical or + every existing encrypted API key becomes unreadable. + + Outside development and tests a missing or malformed key is fatal: chat + messages, session summaries and agent memory are all encrypted with it. + + Usability of the key is checked here rather than left to ``Fernet()`` in + get_keys(), so that the .env.example placeholder and the common near-miss of + pasting a raw ``Fernet.generate_key()`` (base64-encoded once, not twice) both + surface the message below instead of a bare "must be 32 url-safe base64-encoded + bytes" from deep inside the library. + """ + env_key = os.getenv(ENCRYPTION_KEY_ENV) + if env_key: + try: + key = base64.b64decode(env_key) + Fernet(key) + return key + except Exception as e: + logger.error(f"Invalid encryption key format: {str(e)}") + if not is_throwaway_environment(): + raise RuntimeError( + f"{ENCRYPTION_KEY_ENV} is set but is not a usable Fernet key. " + "Encrypted data cannot be read; refusing to start with a " + f"different key. Generate one with:\n{GENERATE_KEY_HINT}" + ) from e + + if not is_throwaway_environment(): + raise RuntimeError( + f"{ENCRYPTION_KEY_ENV} is not set. It encrypts chat messages, session " + "summaries and agent memory at rest, so starting without it would make " + f"existing conversations unreadable. Generate one with:\n{GENERATE_KEY_HINT}" + ) + + key = Fernet.generate_key() + logger.info( + f"Generated a throwaway encryption key. Set " + f"{ENCRYPTION_KEY_ENV}={base64.b64encode(key).decode()} to keep data readable" + ) + return key + + +@lru_cache(maxsize=1) +def get_keys() -> Dict[str, Fernet]: + """Every key we can still decrypt with, by key id. Add an id here to rotate. + + Loaded on first use rather than at import: a module import must not depend on + environment setup, and the explicit check belongs in verify_encryption_key() + at startup where the failure is legible. + """ + return {CURRENT_KEY_ID: Fernet(load_key())} + + +def verify_encryption_key() -> None: + """Fail fast at startup if the key is missing or unusable. + + Without this the first customer message would be the thing that discovers a + misconfigured deployment — after the app has already accepted traffic. + """ + get_keys() # raises with an actionable message if the key is missing or invalid + if decrypt_value(encrypt_value("verify")) != "verify": + raise RuntimeError(f"{ENCRYPTION_KEY_ENV} does not round-trip; refusing to start") + + +def is_encrypted(value: Any) -> bool: + """True when the value is already in the stored ciphertext format.""" + return isinstance(value, str) and value.startswith(ENCRYPTED_PREFIX + SEPARATOR) + + +def encrypt_value(value: Optional[str]) -> Optional[str]: + """Encrypt a string for storage. Idempotent: already-encrypted input is returned + untouched, so it is safe to call on a value of unknown provenance.""" + if value is None: + return None + if is_encrypted(value): + return value + # Outside the try: a missing key raises a RuntimeError explaining itself, and + # wrapping that in "failed to encrypt" would hide the actual problem. + fernet = get_keys()[CURRENT_KEY_ID] + try: + token = fernet.encrypt(str(value).encode()).decode() + except Exception as e: + logger.error(f"Encryption error: {str(e)}") + raise ValueError("Failed to encrypt value") + return SEPARATOR.join((ENCRYPTED_PREFIX, CURRENT_KEY_ID, token)) + + +def decrypt_value(value: Optional[str]) -> Optional[str]: + """Decrypt a stored string. + + Values without the ``enc:`` prefix are returned unchanged — that is a row the + backfill has not reached yet, and reads must keep working throughout the + migration. A key we no longer hold, or a corrupt token, logs and returns None + rather than raising: one damaged row must not fail the whole request. + """ + if not is_encrypted(value): + return value + + parts = value.split(SEPARATOR, 2) + if len(parts) != 3: + logger.error("Malformed ciphertext: missing key id or token") + return None + _, key_id, token = parts + + fernet = get_keys().get(key_id) + if fernet is None: + logger.error(f"No encryption key registered for key id '{key_id}'") + return None + + try: + return fernet.decrypt(token.encode()).decode() + except Exception as e: + logger.error(f"Decryption error: {str(e)}") + return None + + +def encrypt_json(value: Optional[Any]) -> Optional[Dict[str, str]]: + """Encrypt a JSON-serialisable object into a single-key marker dict.""" + if value is None: + return None + if isinstance(value, dict) and JSON_MARKER in value: + return value + return {JSON_MARKER: encrypt_value(json.dumps(value))} + + +def decrypt_json(value: Optional[Any]) -> Optional[Any]: + """Inverse of encrypt_json. A plaintext object written before this change is + returned as-is; an undecryptable blob degrades to None.""" + if not isinstance(value, dict) or JSON_MARKER not in value: + return value + + plaintext = decrypt_value(value[JSON_MARKER]) + if plaintext is None: + return None + try: + return json.loads(plaintext) + except ValueError as e: + logger.error(f"Decrypted blob is not valid JSON: {str(e)}") + return None + + +class EncryptedText(TypeDecorator): + """Text column whose value is encrypted at rest and decrypted on read. + + Underlying storage stays unbounded text, so applying this to an existing + String/Text column needs no schema migration. + """ + + impl = Text + cache_ok = True + + def process_bind_param(self, value, dialect): + return encrypt_value(value) + + def process_result_value(self, value, dialect): + return decrypt_value(value) diff --git a/backend/app/core/error_handlers.py b/backend/app/core/error_handlers.py new file mode 100644 index 0000000..739284c --- /dev/null +++ b/backend/app/core/error_handlers.py @@ -0,0 +1,73 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +from fastapi import FastAPI, HTTPException, Request +from fastapi.exception_handlers import http_exception_handler +from starlette.exceptions import HTTPException as StarletteHTTPException +from starlette.responses import Response + +from app.core.logger import get_logger + +logger = get_logger(__name__) + +# What a 5xx answers with instead of the server's own error text. +INTERNAL_ERROR_DETAIL = "Internal server error" + + +class ClientSafeHTTPException(HTTPException): + """A 5xx whose detail is meant for the caller and holds nothing internal. + + Raise this for a message we wrote ourselves, or an upstream provider's own + wording that the operator has to act on — "Recipient not on WhatsApp" is + useless in a log and essential on screen. Every other 5xx is assumed to be + carrying server internals and has its detail replaced, which is what makes + the widespread `detail=str(e)` safe by default. + """ + + +async def sanitized_http_exception_handler( + request: Request, exc: StarletteHTTPException +) -> Response: + """Answer 5xx with a fixed message and keep the real one in the log. + + Routes across the API raise `HTTPException(500, detail=str(e))`, which hands + the caller whatever the exception happened to carry — SQL fragments, file + paths, connection strings. None of it is actionable for a client, and a 5xx + means the fault is ours, so the body gets a constant and the original goes to + the log together with the route that produced it. + + 4xx detail is written for the caller ("Agent not found", "Invalid token") and + is passed through untouched, as is any `ClientSafeHTTPException`. + """ + if exc.status_code < 500 or isinstance(exc, ClientSafeHTTPException): + return await http_exception_handler(request, exc) + + logger.error( + f"{request.method} {request.url.path} -> {exc.status_code}: {exc.detail}" + ) + return await http_exception_handler( + request, + StarletteHTTPException( + status_code=exc.status_code, + detail=INTERNAL_ERROR_DETAIL, + headers=exc.headers, + ), + ) + + +def install_error_handlers(app: FastAPI) -> None: + """Register the sanitizer. Called for every FastAPI instance we serve.""" + app.add_exception_handler(StarletteHTTPException, sanitized_http_exception_handler) diff --git a/backend/app/core/exceptions.py b/backend/app/core/exceptions.py new file mode 100644 index 0000000..686242e --- /dev/null +++ b/backend/app/core/exceptions.py @@ -0,0 +1,19 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +class JiraAuthError(Exception): + """Exception raised for Jira authentication errors.""" + pass \ No newline at end of file diff --git a/backend/app/core/external_urls.py b/backend/app/core/external_urls.py new file mode 100644 index 0000000..552c172 --- /dev/null +++ b/backend/app/core/external_urls.py @@ -0,0 +1,29 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Base URLs of the third-party APIs the channel and CRM adapters call. +# +# Deployments never set these. The e2e suite points them at its recording fake +# (e2e/services/fake-externals) so channel journeys run end to end without +# reaching Telegram, Meta, Slack, … Read once at import, which is also when the +# adapters bind them (Meta's helpers take the base as a default argument). + +import os + + +def external_base(env_var: str, default: str) -> str: + """`default`, unless `env_var` overrides it; never with a trailing slash.""" + return (os.getenv(env_var) or default).rstrip("/") diff --git a/backend/app/core/file_validation.py b/backend/app/core/file_validation.py new file mode 100644 index 0000000..02cbd8a --- /dev/null +++ b/backend/app/core/file_validation.py @@ -0,0 +1,87 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Shared upload validation: content-type, size, and magic-byte checks. + +The frontend performs its own checks, but those are not a security boundary — the +CLI, AI agents, and raw API callers bypass them, so validate every upload here. +""" + +import os +from typing import Tuple +from uuid import uuid4 + +from fastapi import HTTPException, UploadFile, status + +# Magic-byte signatures used across the app. +PDF_MAGIC = b"%PDF-" + + +async def read_validated( + file: UploadFile, + *, + max_size: int, + allowed_content_types: Tuple[str, ...], + magic_prefix: bytes, + label: str = "file", +) -> bytes: + """Validate an ``UploadFile`` and return its content as bytes. + + Checks the declared content type, the actual byte size, and the leading magic + bytes (content type is advisory since clients can lie; the magic-byte check is + the real gate). Raises ``HTTPException`` (400/413) on any failure. + """ + name = file.filename or label + + if file.content_type not in allowed_content_types: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"{name}: unsupported content type", + ) + + # Determine size from the spooled temp file without buffering into our own memory. + file.file.seek(0, 2) + size = file.file.tell() + file.file.seek(0) + if size > max_size: + raise HTTPException( + status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, + detail=f"{name}: exceeds the {max_size // 1024 // 1024}MB limit", + ) + + content = await file.read() + if not content: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"{name}: empty file", + ) + if not content.startswith(magic_prefix): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"{name}: not a valid {label}", + ) + return content + + +def safe_filename(filename: str) -> str: + """Return a filesystem-safe, collision-resistant name from ``filename``. + + Strips directory components (prevents path traversal via ``../`` or absolute + paths) and prefixes a UUID so distinct uploads never overwrite each other. + """ + base = os.path.basename(filename or "").replace("\\", "").strip() + if not base or base in (".", ".."): + base = "upload" + return f"{uuid4().hex}_{base}" diff --git a/backend/app/core/help_center_host.py b/backend/app/core/help_center_host.py new file mode 100644 index 0000000..89167bf --- /dev/null +++ b/backend/app/core/help_center_host.py @@ -0,0 +1,149 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Host-based dispatch for the public help center. + +Requests whose Host is {slug}.<HELP_CENTER_BASE_DOMAIN> or a DB-verified +custom domain are routed to the public help-center app; everything else +passes through to the main API untouched. The Host header is never trusted +beyond an equality lookup against verified values. + +This module is also the single source of truth for host canonicalization and +slug parsing — the resolver in services/help_center_public imports from here +so dispatch and resolution can never disagree. +""" + +import asyncio +import re +import time +from typing import Optional + +from app.core.config import settings +from app.core.logger import get_logger + +logger = get_logger(__name__) + +DOMAIN_CACHE_TTL_SECONDS = 60 + +# Path-based dispatch (self-host): {BACKEND_URL}/help/{slug}/... on the main +# origin. Group 1 is the help-center slug, group 2 the remainder ("/", "/a/x"). +_HELP_PATH_RE = re.compile(r"^/help/([^/]+)(/.*)?$") + + +def normalize_host(raw_host: Optional[str]) -> str: + return (raw_host or "").split(":")[0].strip().lower().rstrip(".") + + +def slug_for_host(host: str) -> Optional[str]: + """The {slug} of {slug}.<HELP_CENTER_BASE_DOMAIN>, if this host is one.""" + suffix = f".{settings.HELP_CENTER_BASE_DOMAIN}" + if host.endswith(suffix): + label = host[: -len(suffix)] + if label and "." not in label: + return label + return None + + +class _VerifiedDomainCache: + """Snapshot of verified custom domains refreshed in the background, so + host dispatch is always a non-blocking set lookup — the middleware fronts + EVERY request, and a synchronous DB refresh here would let a slow database + stall the whole service. Stale-while-revalidate; fails to an empty set + (custom domains temporarily 404; API traffic unaffected).""" + + def __init__(self): + self._domains: frozenset = frozenset() + self._fetched_at: float = 0.0 + self._refreshing = False + + def contains(self, host: str) -> bool: + if time.monotonic() - self._fetched_at >= DOMAIN_CACHE_TTL_SECONDS and not self._refreshing: + self._refreshing = True + try: + asyncio.get_running_loop().create_task(self._refresh_async()) + except RuntimeError: + # No running loop (sync test context): refresh inline. + self._refresh_sync() + self._refreshing = False + return host in self._domains + + async def _refresh_async(self): + try: + await asyncio.to_thread(self._refresh_sync) + finally: + self._refreshing = False + + def _refresh_sync(self): + try: + from app.database import SessionLocal + from app.repositories.help_center import HelpCenterRepository + + with SessionLocal() as db: + self._domains = frozenset(HelpCenterRepository(db).list_verified_domains()) + self._fetched_at = time.monotonic() + except Exception as e: + # Back off a full TTL rather than re-querying a broken DB per request. + self._fetched_at = time.monotonic() + logger.error(f"Verified-domain cache refresh failed: {e}") + + +_domain_cache = _VerifiedDomainCache() + + +def _host_from_scope(scope) -> str: + for name, value in scope.get("headers") or []: + if name == b"host": + return normalize_host(value.decode("latin-1")) + return "" + + +def is_help_center_host(host: str) -> bool: + if not host: + return False + if slug_for_host(host) is not None: + return True + return _domain_cache.contains(host) + + +class HelpCenterHostMiddleware: + """Pure-ASGI dispatcher installed inside the socketio wrapper: help-center + hosts go to the public app, everything else to the main FastAPI app.""" + + def __init__(self, app, public_app): + self.app = app + self.public_app = public_app + + async def __call__(self, scope, receive, send): + if scope["type"] == "http": + # Path dispatch (self-host default): /help/{slug}/... on the main + # origin. Gated to path mode so cloud never serves the /help/ form. + if settings.HELP_CENTER_PUBLIC_MODE == "path": + match = _HELP_PATH_RE.match(scope.get("path", "")) + if match: + slug = match.group(1) + # Drop a trailing slash from the remainder ourselves: letting + # Starlette's redirect_slashes handle it would 307 to a + # Location built from the rewritten (prefix-stripped) path, + # sending the visitor to the origin root without /help/{slug}. + remainder = (match.group(2) or "").rstrip("/") or "/" + scope["help_center"] = {"slug": slug, "base_path": f"/help/{slug}"} + scope["path"] = remainder + scope["raw_path"] = remainder.encode("latin-1") + await self.public_app(scope, receive, send) + return + if is_help_center_host(_host_from_scope(scope)): + await self.public_app(scope, receive, send) + return + await self.app(scope, receive, send) diff --git a/backend/app/core/local_file_urls.py b/backend/app/core/local_file_urls.py new file mode 100644 index 0000000..1beecb5 --- /dev/null +++ b/backend/app/core/local_file_urls.py @@ -0,0 +1,58 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Short-lived signed URLs for private files in local (non-S3) storage. + +The local counterpart of an S3 presigned URL: an HMAC binds the storage key to +an expiry, so a link works for whoever holds it until it expires, and for no +other file. Bearer links rather than session auth because the widget renders +attachments with plain <img>/<a> tags, which cannot send a token header. +""" + +import hashlib +import hmac +import time +from typing import Optional +from urllib.parse import quote, urlencode + +from app.core.config import settings + +LOCAL_FILE_ROUTE = f"{settings.API_V1_STR}/files/local" + +# SECRET_KEY also signs JWTs; the purpose prefix keeps the two from ever +# validating each other. +_PURPOSE = b"replypier:local-file-url:v1\n" + + +def _signature(key: str, expires: int) -> str: + message = _PURPOSE + f"{key}\n{expires}".encode() + return hmac.new(settings.SECRET_KEY.encode(), message, hashlib.sha256).hexdigest() + + +def sign_local_key(key: str, expires_in: Optional[int] = None) -> str: + """Relative URL serving ``key`` (a path under uploads/) until it expires. + Lifetime follows S3_PRESIGN_EXPIRY_SECONDS, so both backends behave alike.""" + lifetime = settings.S3_PRESIGN_EXPIRY_SECONDS if expires_in is None else expires_in + expires = int(time.time()) + lifetime + query = urlencode({"exp": expires, "sig": _signature(key, expires)}) + return f"{LOCAL_FILE_ROUTE}/{quote(key)}?{query}" + + +def verify_local_signature(key: str, expires: Optional[int], signature: Optional[str]) -> bool: + """True when ``signature`` was issued for ``key`` and has not expired.""" + if expires is None or not signature or expires < time.time(): + return False + return hmac.compare_digest(_signature(key, expires), signature) diff --git a/backend/app/core/logger.py b/backend/app/core/logger.py new file mode 100644 index 0000000..469357a --- /dev/null +++ b/backend/app/core/logger.py @@ -0,0 +1,73 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import logging +import sys +from logging.handlers import RotatingFileHandler +import os + +# Dictionary to store loggers by name +_loggers = {} + + +def get_logger(name=None): + """ + Get or create a logger instance. + Args: + name: The name for the logger (typically __name__ from the calling module) + Returns: + A configured logger instance + """ + # Use the provided name or default to 'app' + logger_name = name or 'app' + + # Return existing logger if already created + if logger_name in _loggers: + return _loggers[logger_name] + + # Create new logger + logger = logging.getLogger(logger_name) + + # Only set up handlers if they haven't been set up already + if not logger.handlers: + logger.setLevel(logging.DEBUG) + + # Create formatters and handlers + formatter = logging.Formatter( + '%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # Console handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + # File handler + log_dir = 'logs' + if not os.path.exists(log_dir): + os.makedirs(log_dir) + + file_handler = RotatingFileHandler( + f'{log_dir}/app.log', + maxBytes=10485760, # 10MB + backupCount=5 + ) + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + # Store logger in dictionary + _loggers[logger_name] = logger + return logger diff --git a/backend/app/core/model_catalog.py b/backend/app/core/model_catalog.py new file mode 100644 index 0000000..fb18cbc --- /dev/null +++ b/backend/app/core/model_catalog.py @@ -0,0 +1,201 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Central catalog of BYO-key AI model providers and their suggested models. + +Single source of truth consumed by: + - the ``GET /ai/providers`` endpoint (which populates the frontend selectors), and + - ``validate_model_selection`` in ``app.api.ai_setup``. + +The listed models are *suggestions*, not a hard allowlist: every provider here has +``custom_allowed=True``, so an organization may type any model ID the provider +supports. Validation only checks that the provider is known and the model name is +non-empty — the live API-key test is what actually rejects a bad model ID. + +Provider model IDs churn and several have near-term shutdowns (see notes). Verify +against each provider's live ``GET /v1/models`` before relying on an ID long-term. + +Every provider key MUST match a value in ``app.models.ai_config.AIModelType`` and a +branch in ``app.utils.agno_utils.create_model``. +""" + +from typing import Dict, List, TypedDict + + +class CatalogModel(TypedDict): + value: str # exact model ID passed to the provider API + label: str # human-readable display name + + +class CatalogProvider(TypedDict): + label: str + requires_api_key: bool + custom_allowed: bool + # Console URL where the user creates/copies their API key for this provider. + api_key_url: str + models: List[CatalogModel] + # Whether this provider needs a user-supplied base URL (e.g. a self-hosted or + # third-party OpenAI-compatible endpoint). Defaults to False when absent. + requires_base_url: bool + + +def _m(value: str, label: str) -> CatalogModel: + return {"value": value, "label": label} + + +# Keyed by AIModelType value. REPLYPIER (managed) is intentionally excluded — it +# is handled as a special case in the setup flow and is not user-selectable here. +MODEL_CATALOG: Dict[str, CatalogProvider] = { + "OPENAI": { + "label": "OpenAI", + "requires_api_key": True, + "custom_allowed": True, + "api_key_url": "https://platform.openai.com/api-keys", + "models": [ + _m("gpt-4.1", "GPT-4.1"), + _m("gpt-4o", "GPT-4o"), + _m("gpt-4.1-mini", "GPT-4.1 Mini"), + _m("gpt-4o-mini", "GPT-4o Mini"), + _m("o4-mini", "o4-mini"), + ], + }, + "ANTHROPIC": { + "label": "Anthropic (Claude)", + "requires_api_key": True, + "custom_allowed": True, + "api_key_url": "https://console.anthropic.com/settings/keys", + # Current-gen Claude IDs are complete stable strings — do NOT append a date. + "models": [ + _m("claude-opus-4-8", "Claude Opus 4.8"), + _m("claude-sonnet-5", "Claude Sonnet 5"), + _m("claude-sonnet-4-6", "Claude Sonnet 4.6"), + _m("claude-haiku-4-5", "Claude Haiku 4.5"), + ], + }, + "GOOGLE": { + "label": "Google Gemini", + "requires_api_key": True, + "custom_allowed": True, + "api_key_url": "https://aistudio.google.com/app/apikey", + # gemini-2.0-flash is shut down; use bare 2.5 IDs (avoid -latest/-preview). + "models": [ + _m("gemini-2.5-pro", "Gemini 2.5 Pro"), + _m("gemini-2.5-flash", "Gemini 2.5 Flash"), + _m("gemini-2.5-flash-lite", "Gemini 2.5 Flash-Lite"), + _m("gemini-3.5-flash", "Gemini 3.5 Flash"), + ], + }, + "MISTRAL": { + "label": "Mistral", + "requires_api_key": True, + "custom_allowed": True, + "api_key_url": "https://console.mistral.ai/api-keys", + # -latest aliases auto-advance to the current dated snapshot. + "models": [ + _m("mistral-large-latest", "Mistral Large"), + _m("mistral-medium-latest", "Mistral Medium"), + _m("mistral-small-latest", "Mistral Small"), + ], + }, + "XAI": { + "label": "xAI (Grok)", + "requires_api_key": True, + "custom_allowed": True, + # The Grok inference API uses a single xai-... key from console.x.ai — NOT the + # X/Twitter OAuth app credentials (consumer key / access token / bearer token). + "api_key_url": "https://console.x.ai", + "models": [ + _m("grok-4", "Grok 4"), + _m("grok-4-fast-reasoning", "Grok 4 Fast (Reasoning)"), + _m("grok-4-fast-non-reasoning", "Grok 4 Fast (Non-Reasoning)"), + _m("grok-3", "Grok 3"), + ], + }, + "DEEPSEEK": { + "label": "DeepSeek", + "requires_api_key": True, + "custom_allowed": True, + "api_key_url": "https://platform.deepseek.com/api_keys", + # deepseek-chat/reasoner scheduled for EOL 2026-07-24; successors are + # deepseek-v4-flash / deepseek-v4-pro — add here once live. + "models": [ + _m("deepseek-chat", "DeepSeek Chat (V3)"), + _m("deepseek-reasoner", "DeepSeek Reasoner"), + ], + }, + "GROQ": { + "label": "Groq", + "requires_api_key": True, + "custom_allowed": True, + "api_key_url": "https://console.groq.com/keys", + # gpt-oss-120b is the durable pick. gpt-oss-20b is intentionally NOT listed: the + # smaller model leaks OpenAI "harmony" formatting (calls the response tool as + # `functions/json`), which Groq rejects, making structured output unreliable. + # llama-3.3-70b-versatile is kept for continuity but is deprecated (EOL + # 2026-08-16). Org-prefixed IDs must be passed exactly. + "models": [ + _m("openai/gpt-oss-120b", "GPT-OSS 120B"), + _m("llama-3.3-70b-versatile", "Llama 3.3 70B Versatile"), + ], + }, + "OPENAI_COMPATIBLE": { + # Names the wire protocol, not the field: the endpoint must speak the + # OpenAI chat-completions API (OpenRouter, vLLM, llama.cpp, Ollama, LiteLLM + # and friends all do). The parenthetical is what self-hosters recognise. + "label": "OpenAI-compatible (self-hosted, OpenRouter, Ollama…)", + "requires_api_key": True, + "custom_allowed": True, + "requires_base_url": True, + # No hosted console — points at the provider's own docs for getting a key. + "api_key_url": "", + # No suggested models: the endpoint and model catalog are whatever the + # user's server exposes, so the model name must always be typed in. + "models": [], + }, +} + + +def is_known_provider(provider: str) -> bool: + """Return True if the provider is a selectable BYO-key provider in the catalog.""" + return bool(provider) and provider.upper() in MODEL_CATALOG + + +def requires_base_url(provider: str) -> bool: + """Return True if the provider needs a user-supplied base URL. + + The catalog is the single source of truth for this, so a new self-hosted or + gateway provider only has to set ``requires_base_url`` on its entry — callers + must not string-compare against a specific provider name. + """ + if not is_known_provider(provider): + return False + return MODEL_CATALOG[provider.upper()].get("requires_base_url", False) + + +def list_providers() -> List[dict]: + """Return the catalog as a serializable list for the /ai/providers endpoint.""" + return [ + { + "value": provider_value, + "label": entry["label"], + "requires_api_key": entry["requires_api_key"], + "custom_allowed": entry["custom_allowed"], + "api_key_url": entry["api_key_url"], + "models": entry["models"], + "requires_base_url": entry.get("requires_base_url", False), + } + for provider_value, entry in MODEL_CATALOG.items() + ] diff --git a/backend/app/core/oauth_state.py b/backend/app/core/oauth_state.py new file mode 100644 index 0000000..3c1aeb3 --- /dev/null +++ b/backend/app/core/oauth_state.py @@ -0,0 +1,63 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# One-time OAuth `state` values for the install flows (Jira, CRMs, Slack). +# +# The provider sends the browser back to whichever backend worker it lands on, so +# the state lives in Redis when it is enabled. Without Redis (single-worker dev +# setups) an in-process dict stands in. Either way a state expires after +# STATE_TTL_SECONDS and can be redeemed once. + +import time +from typing import Optional + +from app.core.redis import get_redis + +STATE_TTL_SECONDS = 60 * 10 + +# (kind, state) -> (value, expires_at) +_fallback: dict = {} + + +def _redis_key(kind: str, state: str) -> str: + return f"{kind}_oauth_state:{state}" + + +def store_state(kind: str, state: str, value: str) -> None: + """Remember `value` (usually the org id) for `state` until it is popped or expires.""" + redis_client = get_redis() + if redis_client is not None: + redis_client.set(_redis_key(kind, state), value, ex=STATE_TTL_SECONDS) + return + now = time.time() + for key in [k for k, (_, expires_at) in _fallback.items() if expires_at < now]: + _fallback.pop(key, None) + _fallback[(kind, state)] = (value, now + STATE_TTL_SECONDS) + + +def pop_state(kind: str, state: str) -> Optional[str]: + """The value stored for `state`, removed so it can't be replayed; None if unknown or expired.""" + if not state: + return None + redis_client = get_redis() + if redis_client is not None: + pipe = redis_client.pipeline() + pipe.get(_redis_key(kind, state)) + pipe.delete(_redis_key(kind, state)) + value, _ = pipe.execute() + return value or None + entry = _fallback.pop((kind, state), None) + return entry[0] if entry and entry[1] >= time.time() else None diff --git a/backend/app/core/outbound_http.py b/backend/app/core/outbound_http.py new file mode 100644 index 0000000..e50fa6a --- /dev/null +++ b/backend/app/core/outbound_http.py @@ -0,0 +1,139 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""httpx clients that only dial public addresses, for OUTBOUND_URL_POLICY. + +Checking a URL before handing it to an HTTP client leaves two holes: the +client resolves the name again (a 0-TTL record can answer public to the check +and 127.0.0.1 to the client), and it follows redirects nobody checked. The +guard here sits in httpcore's network backend instead, so every TCP connect — +first request, each redirect hop, every reconnect — resolves the host once, +refuses it unless every address is public, and connects to that vetted +address. TLS SNI, certificate checks and the Host header still use the +hostname: httpcore passes it to start_tls and the request separately. + +Under policy "any" the factories return None, so callers keep their clients +exactly as before. +""" + +from typing import Any, Optional + +import anyio +import httpcore +import httpx + +from app.core.logger import get_logger +from app.core.outbound_policy import OutboundTargetBlocked, public_only, vetted_addresses + +logger = get_logger(__name__) + + +class OutboundConnectRefused(httpcore.ConnectError): + """The guard refused an address. Raised as a connect error so httpx, and + every SDK built on it (whatever its version), reports an ordinary + connection failure rather than letting an unknown exception escape; the + policy's OutboundTargetBlocked is its __cause__.""" + + +def _vet(host: str, port: int): + try: + return vetted_addresses(host, port, host) + except OutboundTargetBlocked as e: + logger.warning(f"Refused outbound connection to internal address via {host}") + raise OutboundConnectRefused(str(e)) from e + except OSError as e: # DNS failure: report it the way httpx would + raise httpcore.ConnectError(str(e)) from e + + +class GuardedBackend(httpcore.NetworkBackend): + """Sync backend: connect only to vetted public addresses of the host.""" + + def __init__(self, inner: httpcore.NetworkBackend): + self._inner = inner + + def connect_tcp(self, host, port, timeout=None, local_address=None, socket_options=None): + last_error: Optional[Exception] = None + for address in _vet(host, port): + try: + return self._inner.connect_tcp( + address, port, timeout=timeout, + local_address=local_address, socket_options=socket_options, + ) + except (httpcore.ConnectError, httpcore.ConnectTimeout) as e: + last_error = e # try the host's next address, like create_connection + raise last_error + + def connect_unix_socket(self, *args, **kwargs): + raise OutboundConnectRefused("Unix sockets are not allowed for outbound targets.") + + def sleep(self, seconds: float) -> None: + self._inner.sleep(seconds) + + +class AsyncGuardedBackend(httpcore.AsyncNetworkBackend): + """Async counterpart of GuardedBackend.""" + + def __init__(self, inner: httpcore.AsyncNetworkBackend): + self._inner = inner + + async def connect_tcp(self, host, port, timeout=None, local_address=None, socket_options=None): + last_error: Optional[Exception] = None + for address in await anyio.to_thread.run_sync(_vet, host, port): + try: + return await self._inner.connect_tcp( + address, port, timeout=timeout, + local_address=local_address, socket_options=socket_options, + ) + except (httpcore.ConnectError, httpcore.ConnectTimeout) as e: + last_error = e + raise last_error + + async def connect_unix_socket(self, *args, **kwargs): + raise OutboundConnectRefused("Unix sockets are not allowed for outbound targets.") + + async def sleep(self, seconds: float) -> None: + await self._inner.sleep(seconds) + + +def _guard(transport, backend_cls): + """Swap the transport's connection-pool backend for the guarded one. + httpx has no public hook for this; fail loudly rather than hand out an + unguarded client if a library upgrade moves it.""" + pool = getattr(transport, "_pool", None) + if not isinstance(pool, (httpcore.ConnectionPool, httpcore.AsyncConnectionPool)) \ + or not hasattr(pool, "_network_backend"): + raise RuntimeError("Cannot install the outbound address guard on this httpx version") + pool._network_backend = backend_cls(pool._network_backend) + return transport + + +def guarded_http_client(**kwargs: Any) -> Optional[httpx.Client]: + """A sync httpx.Client that dials only public addresses, or None when the + policy is "any". ``kwargs`` go to httpx.Client (not the transport). An + explicit transport also turns off env proxies, which would otherwise + connect on the target's behalf unchecked.""" + if not public_only(): + return None + return httpx.Client(transport=_guard(httpx.HTTPTransport(), GuardedBackend), **kwargs) + + +def guarded_async_http_client(**kwargs: Any) -> Optional[httpx.AsyncClient]: + """Async counterpart of guarded_http_client.""" + if not public_only(): + return None + return httpx.AsyncClient( + transport=_guard(httpx.AsyncHTTPTransport(), AsyncGuardedBackend), **kwargs + ) diff --git a/backend/app/core/outbound_policy.py b/backend/app/core/outbound_policy.py new file mode 100644 index 0000000..0b61d87 --- /dev/null +++ b/backend/app/core/outbound_policy.py @@ -0,0 +1,119 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +"""Opt-in refusal of user-configured outbound targets on internal networks. + +AI base URLs, MCP server URLs and ticket databases are dialled by this server. +Self-hosters legitimately point them at their own network (Ollama on +localhost, a private MCP server or database), so the default policy "any" +allows everything. A shared, multi-tenant deployment sets +OUTBOUND_URL_POLICY=public_only so no tenant can use these features to reach +the server's own network or the cloud metadata endpoint. + +Targets are checked when saved and again before connecting. The connection +itself is then pinned: the address that passed the check is the one dialled +(app.core.outbound_http for HTTP clients, pin_outbound_target for databases), +so a hostname that re-resolves elsewhere (DNS rebinding) or a redirect to an +internal host is refused at connect time too. +""" + +import ipaddress +import socket +from typing import List, Optional + +from fastapi import HTTPException, status + +from app.core.config import settings +from app.knowledge.url_safety import is_public_ip, resolves_to_blocked_host + +POLICY_ANY = "any" +POLICY_PUBLIC_ONLY = "public_only" + + +class OutboundTargetBlocked(ValueError): + """A configured target is internal and the policy is public_only.""" + + +def public_only() -> bool: + return settings.OUTBOUND_URL_POLICY == POLICY_PUBLIC_ONLY + + +def _blocked(label: str) -> OutboundTargetBlocked: + return OutboundTargetBlocked( + f"{label} must be a public address. This server does not allow " + "private, loopback, link-local or reserved network targets." + ) + + +def _as_url(target: str) -> str: + """resolves_to_blocked_host parses a URL; give a bare host (a database or + SSH host) the shape it expects. A scheme-less "localhost:11434/v1" would + otherwise parse with no host at all and slip through.""" + if "://" in target: + return target + try: + if ipaddress.ip_address(target).version == 6: + return f"//[{target}]" + except ValueError: + pass + return f"//{target}" + + +def check_outbound_target(target: Optional[str], label: str) -> None: + """Raise OutboundTargetBlocked when the policy is public_only and + ``target`` (a URL or a bare host) is, or resolves to, a private, loopback, + link-local or reserved address. ``label`` names the field in the error.""" + if not public_only() or not target: + return + if resolves_to_blocked_host(_as_url(target.strip())): + raise _blocked(label) + + +def vetted_addresses(host: str, port: Optional[int], label: str) -> List[str]: + """Resolve ``host`` once and return its addresses for dialling, refusing + it if ANY of them is not public. Unlike the save-time check this fails + closed: an address that can't be parsed is refused, and a name that + doesn't resolve raises socket.gaierror. Callers connect to the returned + addresses directly, so nothing resolves the name a second time.""" + infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM) + addresses = list(dict.fromkeys(info[4][0] for info in infos)) + if not addresses: + raise socket.gaierror(f"{host} did not resolve to any address") + for address in addresses: + try: + public = is_public_ip(ipaddress.ip_address(address)) + except ValueError: + public = False + if not public: + raise _blocked(label) + return addresses + + +def pin_outbound_target(host: Optional[str], label: str) -> Optional[str]: + """For drivers that dial a host themselves (databases, SSH): under + public_only, vet ``host`` and return the address to connect to instead of + the name. None under policy "any", meaning connect as before.""" + if not public_only() or not host: + return None + return vetted_addresses(host.strip(), None, label)[0] + + +def require_public_target(target: Optional[str], label: str) -> None: + """check_outbound_target for request handlers: a refusal is a 400.""" + try: + check_outbound_target(target, label) + except OutboundTargetBlocked as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) diff --git a/backend/app/core/params.py b/backend/app/core/params.py new file mode 100644 index 0000000..717e267 --- /dev/null +++ b/backend/app/core/params.py @@ -0,0 +1,32 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Request parameters with the bounds the database implies. An integer id past +# Postgres INTEGER, or a negative LIMIT, makes the query itself fail — a 500 — +# where the caller should get a 422 (or simply nothing found). + +from typing import Annotated + +from fastapi import Path, Query + +MAX_DB_INT = 2_147_483_647 + +# An integer primary key in the URL path. +DbIntId = Annotated[int, Path(ge=1, le=MAX_DB_INT)] + +# A 1-based page number; far past any real list, short of overflowing the OFFSET. +MAX_PAGE = 100_000 +PageNumber = Annotated[int, Query(ge=1, le=MAX_PAGE)] diff --git a/backend/app/core/redis.py b/backend/app/core/redis.py new file mode 100644 index 0000000..615c931 --- /dev/null +++ b/backend/app/core/redis.py @@ -0,0 +1,61 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import redis +from app.core.config import settings +from app.core.logger import get_logger + +logger = get_logger(__name__) + +redis_client = None + +def init_redis(): + """Initialize Redis connection""" + global redis_client + + if not settings.REDIS_ENABLED: + logger.info("Redis is disabled") + return None + + redis_url = settings.REDIS_URL + if redis_url and redis_url.startswith("redis://") and ".cache.amazonaws.com" in redis_url: + redis_url = "rediss://" + redis_url[8:] + logger.info(f"Using TLS for Redis connection: {redis_url}") + + try: + redis_client = redis.from_url( + redis_url, + socket_timeout=5.0, + socket_connect_timeout=5.0, + decode_responses=True # Automatically decode responses to Python strings + ) + + # Test connection immediately + redis_client.ping() + logger.info("Redis connection successful") + return redis_client + except Exception as e: + logger.error(f"Failed to initialize Redis client: {str(e)}") + return None + +def get_redis(): + """Get Redis client instance""" + global redis_client + + if not redis_client and settings.REDIS_ENABLED: + redis_client = init_redis() + + return redis_client \ No newline at end of file diff --git a/backend/app/core/s3.py b/backend/app/core/s3.py new file mode 100644 index 0000000..c09b19b --- /dev/null +++ b/backend/app/core/s3.py @@ -0,0 +1,310 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +""" +S3 Storage Utilities +""" +import asyncio +import re +from functools import lru_cache, partial +import boto3 +from app.core.config import settings +from app.core.logger import get_logger +from fastapi import HTTPException +from typing import Optional +from urllib.parse import urlparse, urlunparse + +logger = get_logger(__name__) + +# AWS rejects any SigV4 presigned URL whose X-Amz-Expires exceeds one week with +# AuthorizationQueryParametersError. Only the legacy SigV2 signer (which +# botocore still selects for us-east-1 and eu-west-1) has no such limit, so a +# longer value happens to work in those regions and 403s everywhere else. +S3_MAX_PRESIGN_SECONDS = 604800 + +# head_object retries after put_object, to ride out transient read-after-write +# failures rather than failing the whole upload. +_UPLOAD_VERIFY_ATTEMPTS = 3 +_UPLOAD_VERIFY_BACKOFF_SECONDS = 0.5 + + +def strip_s3_signature(url: Optional[str]) -> Optional[str]: + """Reduce a presigned S3 URL back to the bare object URL. + + Signed URLs must never reach the database: they expire, and a stored + signature outlives its validity. Clients round-trip response values into + update payloads (the agent customization form does exactly this), so + stripping on the way in is the only reliable guard. + """ + if not url or 'amazonaws.com' not in url: + return url + return urlunparse(urlparse(url)._replace(query='', fragment='')) + + +# Matches an S3 endpoint in the HOST position, in every shape AWS serves: the +# global endpoint, regional ones (dot or legacy dash), and the virtual-hosted +# forms that put the bucket in front, including dotted bucket names. Anchored at +# both ends so "evil.com/s3.amazonaws.com/x" and "s3.amazonaws.com.attacker.com" +# do not match, and neither does "foos3.amazonaws.com". +_S3_HOST_RE = re.compile(r"(^|\.)s3([.-][a-z0-9-]+)?\.amazonaws\.com$", re.IGNORECASE) + + +def is_s3_url(url: Optional[str]) -> bool: + """True when this URL's HOST is an S3 endpoint. + + Checks the parsed hostname, never a substring of the whole URL: any + attacker-supplied URL can carry "s3.amazonaws.com" in its path, and treating + that as one of ours is how a delete or a key extraction ends up pointed + somewhere it shouldn't be. A substring test is also INCOMPLETE — it misses + the regional virtual-hosted form url_for_s3_key actually produces + (bucket.s3.<region>.amazonaws.com), which contains no "s3.amazonaws.com". + """ + if not url: + return False + return bool(_S3_HOST_RE.search(urlparse(url).hostname or "")) + + +def url_for_s3_key(s3_key: str) -> str: + """The canonical stored URL for an object key. Single source of truth for + the URL shape — _key_from_url must be able to round-trip whatever this + produces. + + Buckets whose name contains a dot get path-style: the wildcard on + *.s3.<region>.amazonaws.com only covers one label, so the virtual-hosted + form (my.bucket.s3.<region>.amazonaws.com) fails TLS verification in any + strict client. + """ + if '.' in settings.S3_BUCKET: + return f"https://s3.{settings.S3_REGION}.amazonaws.com/{settings.S3_BUCKET}/{s3_key}" + return f"https://{settings.S3_BUCKET}.s3.{settings.S3_REGION}.amazonaws.com/{s3_key}" + + +def _key_from_url(s3_url: str) -> str: + """Extract the object key from either S3 URL format. + + Virtual-hosted puts the bucket in the host (bucket.s3...amazonaws.com/key); + path-style puts it in the first path segment, against either the legacy + global endpoint or a regional one (s3.<region>.amazonaws.com/bucket/key). + """ + parsed_url = urlparse(s3_url) + path_parts = parsed_url.path.strip('/').split('/') + if parsed_url.netloc.startswith(f"{settings.S3_BUCKET}."): + return '/'.join(path_parts) + # Path-style: drop the leading bucket segment. + return '/'.join(path_parts[1:]) + + +def owned_s3_key(url: Optional[str]) -> Optional[str]: + """The object key when ``url`` names an object in OUR bucket, else None. + + _key_from_url assumes the URL is ours and reads any S3 URL as a key in our + bucket; use this instead before acting on a URL a user supplied. + """ + if not is_s3_url(url): + return None + parsed = urlparse(url) + host = (parsed.hostname or "").lower() + parts = parsed.path.strip('/').split('/') + bucket = settings.S3_BUCKET.lower() + if host.startswith(f"{bucket}."): + return '/'.join(parts) + if host.startswith(("s3.", "s3-")) and parts[0] == settings.S3_BUCKET: + return '/'.join(parts[1:]) + return None + + +@lru_cache(maxsize=1) +def get_s3_client(): + """Get boto3 S3 client. + + Cached: constructing a client resolves session, endpoint and credential + config and costs ~1.3ms, which dwarfs the ~0.05ms presign it is usually + built for. boto3 clients are thread-safe for method calls. Call + get_s3_client.cache_clear() if settings change at runtime (tests). + + The credential settings are None (never "") when unset, which is what makes + boto3 fall through to its default chain and pick up an IAM instance or + container role. See the note on AWS_ACCESS_KEY_ID in core/config.py. + """ + return boto3.client( + 's3', + aws_access_key_id=settings.AWS_ACCESS_KEY_ID, + aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY, + region_name=settings.S3_REGION + ) + + +def sign_s3_url(s3_url: str, expiration: Optional[int] = None) -> str: + """ + Generate a signed URL for an S3 object. + + Synchronous on purpose: generate_presigned_url computes an HMAC locally and + makes no network call, so this is safe to call from response serialization + (see the field_serializers on the response schemas) without an event loop. + + Args: + s3_url: The S3 URL of the object + expiration: Lifetime in seconds; defaults to settings.S3_PRESIGN_EXPIRY_SECONDS + and is clamped to S3_MAX_PRESIGN_SECONDS + Returns: + Signed URL for the object, or the input unchanged if it is not signable + """ + try: + if not settings.S3_FILE_STORAGE or not s3_url: + return s3_url + + # Inline data URIs (e.g. generated orb avatars stored in photo_url) and any + # non-S3 absolute URL are not S3 objects — return them unchanged. + if s3_url.startswith('data:') or 'amazonaws.com' not in s3_url: + return s3_url + + if expiration is None: + expiration = settings.S3_PRESIGN_EXPIRY_SECONDS + expiration = min(expiration, S3_MAX_PRESIGN_SECONDS) + + s3_client = get_s3_client() + signed_url = s3_client.generate_presigned_url( + 'get_object', + Params={ + 'Bucket': settings.S3_BUCKET, + 'Key': _key_from_url(s3_url) + }, + ExpiresIn=expiration + ) + return signed_url + except Exception as e: + logger.exception(f"Error generating signed URL: {str(e)}") + return s3_url + + +def sign_s3_key(s3_key: str, expiration: Optional[int] = None) -> str: + """Signed URL for an object key, for callers that hold a key rather than a + stored URL (help-center article images resolve by key).""" + return sign_s3_url(url_for_s3_key(s3_key), expiration) + + +async def get_s3_signed_url(s3_url: str, expiration: Optional[int] = None) -> str: + """Awaitable shim over sign_s3_url for the existing `await` call sites. + + Deprecated: signing never touched the network, so prefer sign_s3_url. + """ + return sign_s3_url(s3_url, expiration) + +async def upload_file_to_s3( + file_content: bytes, + folder: str, + filename: str, + content_type: Optional[str] = None +) -> str: + """ + Upload file to S3 bucket + Args: + file_content: The file content as bytes + folder: The S3 folder path + filename: The filename to save as + content_type: Optional MIME type + Returns: + The S3 URL of the uploaded file + Raises: + HTTPException: if the object cannot be stored or verified + + The bucket is expected to exist already — provisioning it belongs in + deployment, not on the request path, and the runtime identity should not + hold s3:CreateBucket. + """ + try: + s3_client = get_s3_client() + + # Construct S3 key (path) + s3_key = f"{folder}/{filename}" + + # Upload to S3 + extra_args = {} + if content_type: + extra_args['ContentType'] = content_type + + logger.info(f"Putting object to S3: bucket={settings.S3_BUCKET}, key={s3_key}, size={len(file_content)} bytes") + # boto3 is synchronous — offload so a large body never blocks the loop. + await asyncio.to_thread( + partial( + s3_client.put_object, + Bucket=settings.S3_BUCKET, + Key=s3_key, + Body=file_content, + **extra_args + ) + ) + # Verify file was written + for attempt in range(_UPLOAD_VERIFY_ATTEMPTS): + try: + await asyncio.to_thread( + partial(s3_client.head_object, Bucket=settings.S3_BUCKET, Key=s3_key) + ) + break + except Exception as verify_err: + if attempt < _UPLOAD_VERIFY_ATTEMPTS - 1: + await asyncio.sleep(_UPLOAD_VERIFY_BACKOFF_SECONDS) + else: + logger.error(f"File verification failed after retries: {s3_key} - {str(verify_err)}") + raise HTTPException( + status_code=500, + detail="File upload verification failed" + ) + + url = url_for_s3_key(s3_key) + + logger.info(f"Successfully uploaded file to S3: {s3_key}") + return url + + except HTTPException: + raise + except Exception as e: + # Deliberately no local-disk fallback: on a containerised deploy the + # uploads dir is not durable, so falling back silently returns a URL + # that works once and 404s after the next restart, with the caller + # none the wiser. Fail the request instead. + logger.exception(f"S3 upload failed: {str(e)}") + raise HTTPException(status_code=500, detail="File upload failed") + + +async def download_file_from_s3(s3_url: str) -> bytes: + """Download an object's bytes (worker-side readback of stored uploads). + boto3 is synchronous — offload so the transfer never blocks the event loop.""" + + def _download() -> bytes: + s3_client = get_s3_client() + response = s3_client.get_object(Bucket=settings.S3_BUCKET, Key=_key_from_url(s3_url)) + return response['Body'].read() + + return await asyncio.to_thread(_download) + + +async def delete_file_from_s3(s3_url: str) -> bool: + """Delete file from S3 bucket""" + try: + s3_client = get_s3_client() + await asyncio.to_thread( + partial( + s3_client.delete_object, + Bucket=settings.S3_BUCKET, + Key=_key_from_url(s3_url) + ) + ) + return True + + except Exception as e: + logger.error(f"Error deleting from S3: {str(e)}") + return False \ No newline at end of file diff --git a/backend/app/core/security.py b/backend/app/core/security.py new file mode 100644 index 0000000..1b24071 --- /dev/null +++ b/backend/app/core/security.py @@ -0,0 +1,489 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import json +from datetime import datetime, timedelta, timezone +from typing import Optional, Dict +import re +import uuid +from jose import JWTError, jwt +from passlib.context import CryptContext +from app.core.config import settings +from app.core.encryption import CURRENT_KEY_ID, get_keys +from app.core.logger import get_logger +import base64 +import hashlib + +logger = get_logger(__name__) + +# Constants +SECRET_KEY = settings.SECRET_KEY +CONVERSATION_SECRET_KEY = settings.CONVERSATION_SECRET_KEY +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 +REFRESH_TOKEN_EXPIRE_DAYS = 7 + +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# Password policy for passwords one person sets on behalf of another (today: +# an admin resetting an agent's password). Mirrors the strength meter the UI +# draws — minimum length plus three of the four character classes — so the +# form and the API agree on what "strong enough" means. +MIN_PASSWORD_LENGTH = 8 +MIN_PASSWORD_CHARACTER_CLASSES = 3 + +_PASSWORD_CHARACTER_CLASSES = (r"[A-Z]", r"[a-z]", r"[0-9]", r"[^A-Za-z0-9]") + + +def validate_password_strength(password: str) -> str: + """Return the password, or raise ValueError describing what it is missing.""" + if len(password or "") < MIN_PASSWORD_LENGTH: + raise ValueError( + f"Password must be at least {MIN_PASSWORD_LENGTH} characters long" + ) + + classes = sum(1 for pattern in _PASSWORD_CHARACTER_CLASSES if re.search(pattern, password)) + if classes < MIN_PASSWORD_CHARACTER_CLASSES: + raise ValueError( + f"Password must include at least {MIN_PASSWORD_CHARACTER_CLASSES} of: " + "uppercase letter, lowercase letter, number, special character" + ) + + return password + + +def _fernet(): + """The shared encryption-at-rest key, loaded by app.core.encryption. + + API keys predate the ``enc:v1:`` wire format and stay bare Fernet tokens, so + they are encrypted here rather than through encrypt_value/decrypt_value — but + the key itself has exactly one owner. + """ + return get_keys()[CURRENT_KEY_ID] + + +def create_access_token(data: dict) -> str: + to_encode = data.copy() + expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire, "type": "access"}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + +def create_refresh_token(data: dict) -> str: + to_encode = data.copy() + expire = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS) + to_encode.update({"exp": expire, "type": "refresh"}) + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + + +def verify_token(token: str) -> Optional[dict]: + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return payload + except JWTError: + return None + +def encrypt_api_key(api_key: str) -> str: + """Encrypt API key before storing in database""" + try: + return _fernet().encrypt(api_key.encode()).decode() + except Exception as e: + logger.error(f"Encryption error: {str(e)}") + raise ValueError("Failed to encrypt API key") + + +def decrypt_credentials(encrypted: str) -> dict: + """Decrypt a JSON credential blob stored with encrypt_api_key.""" + return json.loads(decrypt_api_key(encrypted)) + + +def decrypt_api_key(encrypted_key: str) -> str: + """Decrypt API key from database""" + try: + return _fernet().decrypt(encrypted_key.encode()).decode() + except Exception as e: + logger.error(f"Decryption error: {str(e)}") + raise ValueError("Failed to decrypt API key") + + +def create_conversation_token(widget_id: str, customer_id: Optional[str] = None, customer_email: Optional[str] = None, **extra_data) -> str: + """ + Create a JWT token for widget conversations with revocation support via Redis + + Includes a JTI (JWT ID) claim for revocation. Token is stored in Redis with TTL. + Optionally stores customer email for session tracking and bulk revocation. + + Args: + widget_id: The widget ID + customer_id: Optional customer ID + customer_email: Optional customer email for session management + **extra_data: Additional data to include in token + """ + jti = str(uuid.uuid4()) + data = { + "widget_id": widget_id, + "type": "conversation", + "jti": jti + } + + # Only include `sub` when a customer_id is present. RFC 7519 (and python-jose) + # require `sub` to be a string — encoding it as JSON null causes decode to fail + # with JWTError("Subject must be a string") for anonymous widget visitors + # (e.g. Explore flow), which silently drops all downstream claims (like `source`). + if customer_id is not None: + data["sub"] = customer_id + + # Add email to token if provided + if customer_email: + data["email"] = customer_email + + # Add any extra data to the token + data.update(extra_data) + + # Set expiration to 30 days + now = datetime.now(timezone.utc) + expire = now + timedelta(days=30) + data.update({"exp": expire}) + + token = jwt.encode(data, CONVERSATION_SECRET_KEY, algorithm=ALGORITHM) + + # Store token JTI in Redis with TTL and optional email mapping (with widget_id) + _store_token_in_redis(jti, int((expire - now).total_seconds()), customer_email, widget_id) + + return token + + +def verify_conversation_token(token: str) -> Optional[dict]: + """ + Verify conversation token signature and check if revoked + + Returns: + Token payload dict if valid and not revoked, None otherwise + """ + try: + # verify_sub=False tolerates existing tokens (already in browsers' localStorage) + # that were issued with `sub: null`. New tokens from create_conversation_token + # omit `sub` entirely when anonymous, so this option only matters for in-flight + # tokens until the old 30-day TTLs expire. + payload = jwt.decode(token, CONVERSATION_SECRET_KEY, + algorithms=[ALGORITHM], + options={"verify_sub": False}) + + if payload.get("type") != "conversation": + return None + + # Check if token JTI exists in Redis (if revoked, it won't exist) + jti = payload.get("jti") + if jti and not _is_token_in_redis(jti): + logger.warning(f"Token revoked or expired: jti={jti[:8]}...") + return None + + return payload + except JWTError: + return None + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash""" + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + """Hash a password""" + return pwd_context.hash(password) + + +# ============================================================================ +# TOKEN STORAGE IN REDIS - Minimal approach +# ============================================================================ + +def _store_token_in_redis(jti: str, ttl_seconds: int, email: Optional[str] = None, widget_id: Optional[str] = None) -> bool: + """ + Store token JTI in Redis with TTL and optionally store email/widget_id mapping + + Args: + jti: JWT ID for the token + ttl_seconds: Time to live in seconds + email: Optional email to store for session tracking + widget_id: Optional widget ID for widget-specific session tracking + """ + try: + from app.core.redis import redis_client + if redis_client: + # Store the token JTI + redis_client.setex(f"token:{jti}", ttl_seconds, "1") + + # Store email mapping if provided (key: user_sessions:email, value: set of JTIs) + if email: + redis_client.sadd(f"user_sessions:{email}", jti) + # Set same TTL for email->JTI mapping + redis_client.expire(f"user_sessions:{email}", ttl_seconds) + + # If widget_id provided, also store widget-specific sessions + if widget_id: + widget_key = f"user_sessions:{email}:widget:{widget_id}" + redis_client.sadd(widget_key, jti) + redis_client.expire(widget_key, ttl_seconds) + + # Maintain reverse index: track all widget keys for this email + # for efficient bulk deletion without scanning + redis_client.sadd(f"user_sessions:{email}:widget_keys", widget_key) + redis_client.expire(f"user_sessions:{email}:widget_keys", ttl_seconds) + + return True + except Exception as e: + logger.warning(f"Failed to store token in Redis: {str(e)}") + return False + + +def _is_token_in_redis(jti: str) -> bool: + """Check if token JTI exists in Redis""" + try: + from app.core.redis import redis_client + if redis_client: + return redis_client.exists(f"token:{jti}") > 0 + except Exception as e: + logger.debug(f"Failed to check token in Redis: {str(e)}") + # If Redis unavailable, assume valid (don't block requests) + return True + + +def revoke_token(jti: str) -> bool: + """Revoke token by deleting JTI from Redis""" + try: + from app.core.redis import redis_client + if redis_client: + redis_client.delete(f"token:{jti}") + logger.info(f"Token revoked: {jti[:8]}...") + return True + except Exception as e: + logger.error(f"Failed to revoke token: {str(e)}") + return False + + +def revoke_user_sessions(email: str) -> bool: + """ + Revoke all sessions for a user by email. + + Args: + email: The user's email address + + Returns: + True if successful, False otherwise + """ + try: + from app.core.redis import redis_client + if redis_client: + # Get all JTIs for this email across all widgets + jti_set = redis_client.smembers(f"user_sessions:{email}") + + if jti_set: + # Delete each JTI + for jti in jti_set: + redis_client.delete(f"token:{jti.decode() if isinstance(jti, bytes) else jti}") + + # Delete the email mapping + redis_client.delete(f"user_sessions:{email}") + + # Also delete all widget-specific mappings using the reverse index set + widget_keys_set = f"user_sessions:{email}:widget_keys" + widget_keys = redis_client.smembers(widget_keys_set) + if widget_keys: + for key in widget_keys: + redis_client.delete(key.decode() if isinstance(key, bytes) else key) + redis_client.delete(widget_keys_set) + # NOTE: When creating or deleting widget-specific session keys elsewhere, + # be sure to add/remove the key from the widget_keys_set for this user. + + logger.info(f"Revoked {len(jti_set)} sessions for user: {email}") + return True + else: + logger.info(f"No active sessions found for user: {email}") + return True + except Exception as e: + logger.error(f"Failed to revoke user sessions: {str(e)}") + return False + + +def revoke_user_sessions_by_widget(email: str, widget_id: str) -> bool: + """ + Revoke all sessions for a user in a specific widget. + + Args: + email: The user's email address + widget_id: The widget ID to revoke sessions for + + Returns: + True if successful, False otherwise + """ + try: + from app.core.redis import redis_client + if redis_client: + # Get all JTIs for this email in this specific widget + jti_set = redis_client.smembers(f"user_sessions:{email}:widget:{widget_id}") + + if jti_set: + # Delete each JTI + for jti in jti_set: + jti_str = jti.decode() if isinstance(jti, bytes) else jti + redis_client.delete(f"token:{jti_str}") + # Also remove from general user_sessions set + redis_client.srem(f"user_sessions:{email}", jti_str) + + # Delete the widget-specific mapping + redis_client.delete(f"user_sessions:{email}:widget:{widget_id}") + + logger.info(f"Revoked {len(jti_set)} sessions for user: {email} in widget: {widget_id}") + return True + else: + logger.info(f"No active sessions found for user: {email} in widget: {widget_id}") + return True + except Exception as e: + logger.error(f"Failed to revoke user sessions by widget: {str(e)}") + return False + + +def get_user_active_sessions(email: str, widget_id: Optional[str] = None) -> list: + """ + Get all active session JTIs for a user by email. + + Args: + email: The user's email address + widget_id: Optional widget ID to get sessions only for that widget + + Returns: + List of active JTI strings for the user (filtered by widget if provided) + """ + try: + from app.core.redis import redis_client + if redis_client: + if widget_id: + # Get sessions only for this specific widget + jti_set = redis_client.smembers(f"user_sessions:{email}:widget:{widget_id}") + else: + # Get all sessions for this user across all widgets + jti_set = redis_client.smembers(f"user_sessions:{email}") + + return [jti.decode() if isinstance(jti, bytes) else jti for jti in jti_set] + except Exception as e: + logger.error(f"Failed to get user sessions: {str(e)}") + return [] + + +def get_existing_valid_token_jti(email: str, widget_id: str, customer_id: str) -> Optional[str]: + """ + Check if there's an existing valid token JTI for this customer/widget combination. + + Returns the JTI (JWT ID) of an existing valid token, not the token itself. + This prevents token multiplication when the same user refreshes the page multiple times. + + Args: + email: Customer email + widget_id: Widget ID + customer_id: Customer ID + + Returns: + The JTI (JWT ID) string of an existing valid token if found, None otherwise + """ + try: + from app.core.redis import redis_client + if not redis_client: + return None + + # Get all active JTIs for this email in this widget + jti_set = redis_client.smembers(f"user_sessions:{email}:widget:{widget_id}") + + if not jti_set: + logger.debug(f"No existing sessions found for {email} in widget {widget_id}") + return None + + # If we have active tokens, return the first valid one + # In a typical flow, there should only be one per widget per user + for jti in jti_set: + jti_str = jti.decode() if isinstance(jti, bytes) else jti + # Check if the token still exists in Redis (not revoked) + if redis_client.exists(f"token:{jti_str}") > 0: + logger.info(f"Found existing valid token for {email} in widget {widget_id}, reusing it") + return jti_str + + logger.debug(f"Active sessions found for {email} in widget {widget_id}, but none are valid in Redis") + return None + + except Exception as e: + logger.error(f"Error checking existing tokens: {str(e)}") + return None + + +# ============================================================================ +# WIDGET API KEY MANAGEMENT +# ============================================================================ + +def generate_widget_api_key() -> str: + """ + Generate a secure random API key with 'wak_' prefix. + + Format: wak_<base64url> (e.g., wak_Xu8f3kLp9mN2...) + Entropy: 256 bits (32 random bytes) + Length: ~47 characters total (4 + 43) + + Returns: + str: API key in format 'wak_<base64url>' + """ + import secrets + + # Generate 32 random bytes (256 bits of entropy) + random_bytes = secrets.token_bytes(32) + # Encode as URL-safe base64 without padding + key_suffix = base64.urlsafe_b64encode(random_bytes).decode('utf-8').rstrip('=') + return f"wak_{key_suffix}" + + +def hash_widget_api_key(api_key: str) -> str: + """ + Hash API key using bcrypt (same as password hashing). + + Args: + api_key: Plain text API key + + Returns: + str: bcrypt hash of the API key + """ + return get_password_hash(api_key) + + +def verify_widget_api_key(plain_key: str, hashed_key: str) -> bool: + """ + Verify API key against its hash. + + Args: + plain_key: Plain text API key from request + hashed_key: Stored bcrypt hash + + Returns: + bool: True if key matches hash + """ + return verify_password(plain_key, hashed_key) + + +def widget_api_key_lookup(api_key: str) -> str: + """ + Indexed lookup value for a widget API key: its SHA-256. + + A fast hash is safe here because the key is 256 random bits, nothing to + brute-force; it lets a request find its app in one query instead of + bcrypt-checking every app. + """ + return hashlib.sha256(api_key.encode()).hexdigest() diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..07f4a47 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,65 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +import os + +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from app.core.config import settings +from sqlalchemy.pool import QueuePool + +# Create SQLAlchemy engine with Unicode support +# t3.micro RDS has ~45-65 max connections, reserve some for admin and other services +engine = create_engine( + settings.DATABASE_URL, + pool_pre_ping=True, + poolclass=QueuePool, + pool_size=5, # Conservative pool size for t3.micro + max_overflow=10, # Allow up to 15 total connections from this pool + pool_timeout=30, # Timeout waiting for connection + pool_recycle=1800, # Recycle connections after 30 minutes + # Unicode, and UTC: naive timestamp columns default to now(), which is the + # session's local time. The code reads them as UTC, so pin the session. + connect_args={"options": "-c client_encoding=utf8 -c timezone=UTC"} if "postgresql" in settings.DATABASE_URL else {}, + json_serializer=lambda obj: __import__('json').dumps(obj, ensure_ascii=False), + json_deserializer=lambda s: __import__('json').loads(s) +) + + + +def _fresh_pool_after_fork() -> None: + """gunicorn --preload (scripts/start.sh) imports the app, which opens pooled + connections, before it forks the workers. A worker must never reuse its + parent's sockets: two workers talking over one connection corrupt each + other's transactions. So each child starts with an empty pool, leaving the + parent's connections open for the parent.""" + engine.dispose(close=False) + + +os.register_at_fork(after_in_child=_fresh_pool_after_fork) + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..7e2795a --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,388 @@ +""" +Copyright 2024-2026 ReplyPier + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +""" + +# Set environment variable to prevent tokenizer deadlock warnings +import os +os.environ.setdefault('TOKENIZERS_PARALLELISM', 'false') + +# Add users import +from fastapi.staticfiles import StaticFiles +import socketio +from app.api import chat, organizations, users, ai_setup, knowledge, agent, notification, widget, widget_apps, user_groups, roles, analytics, jira, shopify, workflow, workflow_node, mcp_tool, file_upload, token, lead_capture, people, tickets +from app.api import help_center as help_center_api +from app.api import help_center_images +from app.api import channels as channels_api +from app.api import webhooks as channel_webhooks +# Import widget_chat to register socket.io event handlers for /widget namespace +from app.api import widget_chat # noqa: F401 - imported for side effects (socket.io handlers registration) +from fastapi import FastAPI, BackgroundTasks +from app.core.config import settings, verify_secret_configuration +from app.core.encryption import verify_encryption_key +from app.services.file_storage import PUBLIC_UPLOAD_FOLDERS +from app.services.firebase import initialize_firebase +from app.database import engine, Base +import asyncio +from app.core.logger import get_logger +from contextlib import asynccontextmanager +import os +from app.channels.app_hooks import load_extensions +from app.core.socketio import socket_app, configure_socketio, sio +from app.core.cors import ( + FirstPartyCredentialsCORSMiddleware, + first_party_origins, + get_cors_origin_regex, + get_cors_origins, + with_configured_origins, +) +from app.core.application import app, initialize_cors_listener +from app.core.crawler_policy import install_crawler_policy + +# Import models to ensure they're registered with SQLAlchemy +from app.models import Organization, User, Customer +try: + from app.enterprise.models import OTP +except ImportError: + print("Enterprise models not available") +from app.api import session_to_agent +from app.workers.chat_auto_closer import run_auto_closer_loop + +logger = get_logger(__name__) + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Startup + # First: a bad ENCRYPTION_KEY must stop the boot, not surface as an unreadable + # conversation once the app is already serving traffic. + verify_encryption_key() + verify_secret_configuration() + initialize_firebase() + await startup_event(app) + yield + # Shutdown + pass + +# Move the CORS setup before app instantiation +cors_origins = with_configured_origins(get_cors_origins()) +logger.debug(f"CORS origins: {cors_origins}") + +# Update the FastAPI app to use the lifespan function +app.router.lifespan_context = lifespan + +# Add CORS middleware to FastAPI app. The regex covers every help-center +# subdomain (*.help.replypier.site) so a newly-created slug is allowed without a +# cache refresh; explicit origins still cover org domains + custom domains. +# Only first-party origins (the dashboard) may send credentials. +app.add_middleware( + FirstPartyCredentialsCORSMiddleware, + credentialed_origins=first_party_origins(), + allow_origins=cors_origins, + allow_origin_regex=get_cors_origin_regex(), + allow_methods=["*"], + allow_headers=["*"], +) + +async def startup_event(api: FastAPI): + """Configure Socket.IO on startup. Called from lifespan() with the FastAPI + instance; the on_event registration it used to carry never ran once a + custom lifespan was set. The module-level `app` is rebound to the Socket.IO + ASGI wrapper below, so the instance has to be passed in.""" + configure_socketio(cors_origins) + + # Channel extensions (e.g. an external-app integration) hook into message + # handling; loaded explicitly so other entry points never pull them in. + load_extensions() + + # Start CORS listener for multi-worker synchronization + initialize_cors_listener() + + # Start chat auto-closer background task, AI chat will auto close after 1 day. + # Held on app.state so the task cannot be garbage-collected mid-run. + api.state.auto_closer_task = asyncio.create_task(run_auto_closer_loop()) + +# Include routers +app.include_router( + chat.router, + prefix=f"{settings.API_V1_STR}/chats", + tags=["chats"] +) + +app.include_router( + channels_api.router, + prefix=f"{settings.API_V1_STR}/channels", + tags=["channels"] +) + +app.include_router( + tickets.router, + prefix=f"{settings.API_V1_STR}/tickets", + tags=["tickets"] +) + +from app.api import crm as crm_api +app.include_router( + crm_api.router, + prefix=f"{settings.API_V1_STR}/crm", + tags=["crm"] +) + +from app.api import ticket_db_connectors, ticket_webhooks +app.include_router( + ticket_db_connectors.router, + prefix=f"{settings.API_V1_STR}/ticket-db-connectors", + tags=["tickets"] +) + +app.include_router( + ticket_webhooks.router, + prefix=f"{settings.API_V1_STR}/tickets/webhooks", + tags=["tickets"] +) + +app.include_router( + channel_webhooks.router, + prefix=f"{settings.API_V1_STR}/webhooks", + tags=["webhooks"] +) + +# Try to import enterprise module if available +try: + from app.enterprise import router as enterprise_router + app.include_router(enterprise_router, prefix=f"{settings.API_V1_STR}/enterprise", tags=["enterprise"]) +except ImportError as e: + if os.path.exists(os.path.join(os.path.dirname(__file__), "enterprise", "__init__.py")): + # The module is there but won't load (e.g. it needs newer core code): + # signup and billing would vanish silently, so say so loudly. + logger.error(f"Enterprise module present but failed to import: {e}") + else: + logger.info("Enterprise module not available - running in community edition mode") + +app.include_router( + organizations.router, + prefix=f"{settings.API_V1_STR}/organizations", + tags=["organizations"] +) + +app.include_router( + users.router, + prefix=f"{settings.API_V1_STR}/users", + tags=["users"] +) + +app.include_router( + help_center_api.router, + prefix=f"{settings.API_V1_STR}/help-center", + tags=["help-center"] +) + +# Public: article images baked into FAQ Markdown. Same prefix as the admin +# router above but unauthenticated — article pages are public. Also registered +# on public_app for host mode. +app.include_router( + help_center_images.router, + prefix=f"{settings.API_V1_STR}/help-center", + tags=["help-center"] +) + +app.include_router( + knowledge.router, + prefix=f"{settings.API_V1_STR}/knowledge", + tags=["knowledge"] +) + +app.include_router( + ai_setup.router, + prefix=f"{settings.API_V1_STR}/ai", + tags=["ai"] +) + +app.include_router( + agent.router, + prefix=f"{settings.API_V1_STR}/agent", + tags=["agent"] +) + +app.include_router( + lead_capture.router, + prefix=f"{settings.API_V1_STR}/agent", + tags=["lead-capture"] +) + +app.include_router( + people.router, + prefix=f"{settings.API_V1_STR}/people", + tags=["people"] +) + +app.include_router( + mcp_tool.router, + prefix=f"{settings.API_V1_STR}/mcp-tools", + tags=["mcp-tools"] +) + +# Proxy router moved to enterprise module + +app.include_router( + notification.router, + prefix=f"{settings.API_V1_STR}/notifications", + tags=["notification"] +) + +app.include_router( + widget.router, + prefix=f"{settings.API_V1_STR}/widgets", + tags=["widget"] +) + +app.include_router( + user_groups.router, + prefix=f"{settings.API_V1_STR}/groups", + tags=["groups"] +) + +app.include_router( + roles.router, + prefix=f"{settings.API_V1_STR}/roles", + tags=["roles"] +) + +app.include_router( + session_to_agent.router, + prefix=f"{settings.API_V1_STR}/sessions", + tags=["session_to_agent"] +) + +app.include_router( + analytics.router, + prefix=f"{settings.API_V1_STR}/analytics", + tags=["analytics"] +) + +app.include_router( + jira.router, + prefix=f"{settings.API_V1_STR}/jira", + tags=["jira"] +) + +app.include_router( + token.router, + prefix=f"{settings.API_V1_STR}", + tags=["token"] +) + +app.include_router( + widget_apps.router, + prefix=f"{settings.API_V1_STR}/widget-apps", + tags=["widget-apps"] +) + +app.include_router( + shopify.router, + prefix=f"{settings.API_V1_STR}/shopify", + tags=["shopify"] +) + +app.include_router( + workflow.router, + prefix=f"{settings.API_V1_STR}/workflow", + tags=["workflow"] +) + +app.include_router( + workflow_node.router, + prefix=f"{settings.API_V1_STR}/workflow", + tags=["workflow_node"] +) + +app.include_router( + file_upload.router, + prefix=f"{settings.API_V1_STR}/files", + tags=["files"] +) + +@app.get("/") +async def root(): + return { + "name": settings.PROJECT_NAME, + "version": settings.VERSION, + "description": "Welcome to ReplyPier API" + } + +@app.api_route("/health", methods=["GET"], operation_id="get_health_check") +async def get_health_check(): + return { + "status": "healthy", + "version": settings.VERSION + } + +@app.api_route("/health", methods=["HEAD"], operation_id="head_health_check") +async def head_health_check(): + return { + "status": "healthy", + "version": settings.VERSION + } + + +@app.get("/health/help-center-domain", include_in_schema=False, operation_id="help_center_domain_tls_check") +async def help_center_domain_tls_check(domain: str = ""): + """On-demand-TLS "ask" gate for the edge proxy (e.g. Caddy). + + Returns 200 only when ``domain`` is a help-center host we actually serve — + a ``{slug}.HELP_CENTER_BASE_DOMAIN`` subdomain or a DB-verified custom + domain — so the edge provisions a certificate for those and refuses every + other name pointed at us. Public by design (the edge calls it + unauthenticated) but it leaks nothing: the answer is a bare status code and + the lookup reuses the same in-memory cache/slug check as host dispatch, so + there is no per-request database hit. + """ + from fastapi import Response + + from app.core.help_center_host import is_help_center_host, normalize_host + + host = normalize_host(domain) + if host and is_help_center_host(host): + return Response(status_code=200) + return Response(status_code=404) + + +# Create upload directories if they don't exist +if not os.path.exists("uploads"): + os.makedirs("uploads") +if not os.path.exists("uploads/agents"): + os.makedirs("uploads/agents") + +# Only the public upload folders are static-mounted. Chat attachments are +# served through signed links (GET {API_V1_STR}/files/local/...), and the rest +# of uploads/ is never served. check_dir=False: a folder may not exist until +# its first upload. +for folder in PUBLIC_UPLOAD_FOLDERS: + app.mount( + f"{settings.API_V1_STR}/uploads/{folder}", + StaticFiles(directory=os.path.join("uploads", folder), check_dir=False), + name=f"uploads_{folder}", + ) +app.mount("/assets", StaticFiles(directory=settings.WIDGET_ASSETS_DIR), name="assets") + +# The API origin is not a website: no robots.txt meant crawlers were free to +# discover /api/v1/... endpoints and report them as 404s. +install_crawler_policy(app) + +# Create final ASGI app +# Public help center: requests for {slug}.<help domain> / verified custom +# domains are dispatched to the SSR help-center app before reaching the API. +from app.api.help_center_public import public_app +from app.core.help_center_host import HelpCenterHostMiddleware + +app = socketio.ASGIApp(sio, HelpCenterHostMiddleware(app, public_app)) diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..2054d74 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,10 @@ +[pytest] +testpaths = tests/ +python_files = test_*.py +addopts = + --cov=app + --cov-report=term-missing + --cov-report=html + --cov-branch + -v +pythonpath = . \ No newline at end of file diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..afcbb97 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,130 @@ +# Web Framework +# fastapi and starlette are pinned for reproducible builds; bump them together and +# run the API suite. fastapi only requires starlette>=0.46.0 (no upper bound). +# starlette must stay >=1.3.1: that release enforces the request.form() limits +# for urlencoded bodies (GHSA-82w8-qh3p-5jfq). +# (The earlier "fastapi 0.138.0 breaks error handling" note was the takeover +# tests' route-swap hack becoming live, not a framework regression; the hack is gone.) +fastapi==0.137.2 +starlette==1.6.0 +uvicorn +python-socketio +websockets + +# Database +sqlalchemy +alembic +psycopg[binary]>=3.1.0 +asyncpg + +# Authentication & Security +python-jose[cryptography] +passlib[bcrypt] +bcrypt>=3.2.0,<4.0.0 +python-multipart + +# Environment & Config +python-dotenv +pydantic>=2.0.0 +pydantic-settings>=2.0.0 + +# Utilities +email-validator +python-dateutil +requests +beautifulsoup4 + +# Testing +pytest +pytest-asyncio +pytest-cov==4.1.0 +pytest-mock +coverage==7.4.1 + + +# Sentiment Analysis +textblob + +#ai +openai +agno==1.7.6 +duckduckgo-search +pypdf +pgvector + +google-generativeai +google-cloud-aiplatform +ollama +anthropic +mistralai<2 # agno 1.7.6 uses the mistralai 1.x API (mistralai.models); 2.x breaks the import + +#ocr +rapidocr-onnxruntime==1.4.4 +onnxruntime==1.23.0 +opencv-python==4.12.0.88 + +#pdf +pypdf + +pytz + +firebase_admin +beautifulsoup4 +aiofiles + +# help-center article rendering: FAQ answers are authored/stored as Markdown and +# rendered to sanitized HTML on the public page (markdown -> nh3 allowlist clean). +markdown +nh3 +# help-center article import: crawled help pages are converted HTML -> Markdown +# (subclassed converter re-hosts images and absolutizes links inline). +markdownify + +gunicorn + +redis + + +aiohttp + +bs4 + +boto3 + +groq + +# Embeddings run on fastembed (ONNX); no torch/transformers stack is installed. +fastembed==0.7.3 + +# Shopify API +ShopifyAPI + +# Razorpay payment gateway +razorpay + +# Pinned below 2.0: mcp 2.0.0 (2026-07-28) dropped streamablehttp_client from +# mcp.client.streamable_http, which the pinned agno==1.7.6 imports at module +# scope. agno catches the failure and re-raises it as "`mcp` not installed", +# so an unpinned install breaks every import of app.main with a message that +# points at the wrong problem. Lift this together with the agno 2.x upgrade. +mcp<2 + +# UV tool runner for Python-based MCP servers +uv + +# Web Crawling +httpx +# pyOpenSSL and cryptography move together: each pyOpenSSL release bounds the +# cryptography versions it accepts (26.4 needs >=49,<51). +pyOpenSSL==26.4.0 +cryptography==50.0.1 +# 0.7.x hard-required sentence-transformers (and with it torch); 0.9 dropped that. +crawl4ai>=0.9 + +# Help center (public SSR templates + custom-domain DNS verification) +jinja2 +dnspython +sqlglot>=30.0 +pymysql>=1.1 +sshtunnel>=0.4.0 +paramiko>=3.4,<4 diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000..ecea360 --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,6 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..1e31dac --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,33 @@ +# API URLs +VITE_API_URL=http://localhost:8000/api/v1 +VITE_WS_URL=ws://localhost:8000 +VITE_WIDGET_URL=http://localhost:8000 + +# Firebase Configuration +VITE_FIREBASE_API_KEY=your_firebase_api_key_here +VITE_FIREBASE_AUTH_DOMAIN=your_firebase_auth_domain_here +VITE_FIREBASE_PROJECT_ID=your_firebase_project_id_here +VITE_FIREBASE_MESSAGING_SENDER_ID=680125690355 +VITE_FIREBASE_APP_ID=your_firebase_app_id_here +VITE_FIREBASE_STORAGE_BUCKET=your_firebase_storage_bucket_here +VITE_FIREBASE_MEASUREMENT_ID=your_firebase_measurement_id_here +VITE_FIREBASE_VAPID_KEY=your_firebase_vapid_key_here + +# Google Fonts API — OPTIONAL. Only powers the font picker in agent +# customization; leave blank and a built-in font list is offered instead. +# Must be your own key: Google API keys are HTTP-referrer restricted, so a key +# issued for another domain returns 403 on yours. Get one at +# https://console.cloud.google.com/apis/credentials (enable "Web Fonts Developer +# API"), then restrict it to your app domain. +# Leave BLANK rather than a placeholder — a non-empty value is treated as a real +# key and the request goes out and fails. +VITE_GOOGLE_FONTS_API_KEY= + +# Shopify API Key (for embedded apps) — optional, blank unless you ship one. +VITE_SHOPIFY_API_KEY= + + + +# Node Environment +NODE_ENV=development +HOST=0.0.0.0 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..d317ecf --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,38 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo + +test-results/ +playwright-report/ + +# Module specific ignores +src/modules/**/node_modules/ +src/modules/**/*.log +src/modules/**/.DS_Store \ No newline at end of file diff --git a/frontend/.npmrc b/frontend/.npmrc new file mode 100644 index 0000000..b5dd1ab --- /dev/null +++ b/frontend/.npmrc @@ -0,0 +1,9 @@ +registry=https://registry.npmjs.org/ +strict-ssl=false +legacy-peer-deps=true +audit=false +fund=false +progress=false +save-exact=true +always-auth=false +access=public \ No newline at end of file diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json new file mode 100644 index 0000000..17a23d0 --- /dev/null +++ b/frontend/.prettierrc.json @@ -0,0 +1,7 @@ + +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..3361ca9 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,64 @@ +# frontend + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +npm install +``` + +### Compile and Hot-Reload for Development + +```sh +npm run dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +npm run build +``` + +### Run Unit Tests with [Vitest](https://vitest.dev/) + +```sh +npm run test:unit +``` + +### Run End-to-End Tests with [Playwright](https://playwright.dev) + +```sh +# Install browsers for the first run +npx playwright install + +# When testing on CI, must build the project first +npm run build + +# Runs the end-to-end tests +npm run test:e2e +# Runs the tests only on Chromium +npm run test:e2e -- --project=chromium +# Runs the tests of a specific file +npm run test:e2e -- tests/example.spec.ts +# Runs the tests in debug mode +npm run test:e2e -- --debug +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +npm run lint +``` diff --git a/frontend/env.d.ts b/frontend/env.d.ts new file mode 100644 index 0000000..cfd18c0 --- /dev/null +++ b/frontend/env.d.ts @@ -0,0 +1,30 @@ +/// <reference types="vite/client" /> +/// <reference types="vite-plugin-pwa/client" /> + +// Global type declarations +declare global { + interface Window { + APP_CONFIG?: { + API_URL?: string + WS_URL?: string + WIDGET_URL?: string + FIREBASE_API_KEY?: string + FIREBASE_AUTH_DOMAIN?: string + FIREBASE_PROJECT_ID?: string + FIREBASE_MESSAGING_SENDER_ID?: string + FIREBASE_APP_ID?: string + FIREBASE_STORAGE_BUCKET?: string + FIREBASE_MEASUREMENT_ID?: string + FIREBASE_VAPID_KEY?: string + RAZORPAY_KEY_ID?: string + GOOGLE_FONTS_API_KEY?: string + NODE_ENV?: string + HOST?: string + DEMO_WIDGET_ID?: string + VITE_SHOPIFY_API_KEY?: string + GTM_ID?: string + } + } +} +export {} + diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..c9d64ec --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,73 @@ +import pluginVue from 'eslint-plugin-vue' +import vueTsEslintConfig from '@vue/eslint-config-typescript' +import pluginVitest from '@vitest/eslint-plugin' +import pluginPlaywright from 'eslint-plugin-playwright' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +export default [ + { + name: 'app/files-to-lint', + files: ['**/*.{ts,mts,tsx,vue}'], + }, + + { + name: 'app/files-to-ignore', + // The enterprise Playwright suite has its own lint config (e2e/eslint.config.js rules). + ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**', 'src/modules/enterprise/e2e/**'], + }, + + ...pluginVue.configs['flat/essential'], + + // Spread, not wrapped in an object: vueTsEslintConfig() returns an ARRAY of + // flat configs. Spreading it into `{}` produced numeric keys ("0", "1", …) + // and ESLint refused the whole config with `Unexpected key "0" found`, so + // linting has been failing outright rather than reporting anything. + ...vueTsEslintConfig(), + + { + name: 'app/ts-rule-overrides', + rules: { + '@typescript-eslint/no-explicit-any': 'off', + // Replaces the old `ban-types` entry here, which typescript-eslint v8 + // split into these successor rules. It was configured to allow bare + // `Object`/`Function`; these two are the rules that would flag them. + '@typescript-eslint/no-unsafe-function-type': 'off', + '@typescript-eslint/no-wrapper-object-types': 'off', + } + }, + + { + // Published images are built once in CI and configured per deployment at + // container start, so import.meta.env.VITE_* is undefined at runtime for + // every self-hosted install. Config must come from window.APP_CONFIG via + // the resolvers in src/config/api.ts (or webclient/widget-env.ts, which + // keeps its own fallbacks). Only those resolvers may read the env directly. + name: 'app/no-build-time-env', + files: ['src/**/*.{ts,mts,tsx,vue}'], + ignores: ['src/config/api.ts', 'src/webclient/widget-env.ts', 'src/sw.ts'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + // VITE_* only. Vite's own built-ins (BASE_URL, MODE, DEV, PROD) are + // build-time by definition and stay allowed. + selector: + 'MemberExpression[object.object.type="MetaProperty"][object.property.name="env"][property.name=/^VITE_/]', + message: + 'Do not read import.meta.env.VITE_* directly — it is undefined in the published image. Use the runtime resolvers in @/config/api (getApiUrl, resolveUploadUrl, getGoogleFontsApiKey, …).', + }, + ], + }, + }, + + { + ...pluginVitest.configs.recommended, + files: ['src/**/__tests__/*'], + }, + + { + ...pluginPlaywright.configs['flat/recommended'], + files: ['e2e/**/*.{test,spec}.{js,ts,jsx,tsx}'], + }, + skipFormatting, +] diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..6fd9beb --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,12970 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "license": "Apache-2.0", + "dependencies": { + "@fortawesome/fontawesome-free": "7.0.0", + "@fortawesome/fontawesome-svg-core": "7.0.0", + "@fortawesome/free-brands-svg-icons": "7.0.0", + "@fortawesome/free-regular-svg-icons": "7.0.0", + "@fortawesome/free-solid-svg-icons": "7.0.0", + "@fortawesome/vue-fontawesome": "3.1.1", + "@headlessui/vue": "^1.7.23", + "@heroicons/vue": "^2.2.0", + "@primevue/themes": "^4.2.5", + "@shopify/app-bridge": "3.7.10", + "@shopify/app-bridge-utils": "3.5.1", + "@vue-flow/background": "1.3.2", + "@vue-flow/controls": "1.1.2", + "@vue-flow/core": "1.45.0", + "@vue-flow/minimap": "1.5.3", + "apexcharts": "4.4.0", + "axios": "1.20.0", + "chart.js": "4.4.7", + "date-fns": "^4.1.0", + "dompurify": "3.4.15", + "firebase": "^11.2.0", + "floating-vue": "^5.2.2", + "marked": "^15.0.6", + "mitt": "^3.0.1", + "pinia": "^2.3.0", + "primevue": "^4.2.5", + "socket.io-client": "4.8.3", + "timezone-select-js": "^2.0.1", + "vue": "^3.5.13", + "vue-advanced-cropper": "^2.8.9", + "vue-router": "^4.5.0", + "vue-sonner": "^1.3.0", + "vue3-apexcharts": "1.8.0", + "webfontloader": "^1.6.28" + }, + "devDependencies": { + "@playwright/test": "1.63.0", + "@tsconfig/node22": "^22.0.0", + "@types/dotenv": "^8.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "^22.10.2", + "@types/webfontloader": "^1.6.38", + "@vitejs/plugin-vue": "^5.2.1", + "@vitejs/plugin-vue-jsx": "^4.1.1", + "@vitest/eslint-plugin": "1.1.20", + "@vue/eslint-config-prettier": "^10.1.0", + "@vue/eslint-config-typescript": "^14.1.3", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.7.0", + "dotenv": "^16.4.5", + "eslint": "^9.14.0", + "eslint-plugin-playwright": "^2.1.0", + "eslint-plugin-vue": "^9.30.0", + "jsdom": "^25.0.1", + "npm-run-all2": "^7.0.2", + "prettier": "^3.3.3", + "tsx": "^4.7.1", + "typescript": "~5.6.3", + "vite": "6.4.3", + "vite-plugin-pwa": "1.3.0", + "vite-plugin-vue-devtools": "^7.6.8", + "vitest": "3.2.7", + "vue-tsc": "^2.1.10", + "workbox-core": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz", + "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonpointer": "^5.0.1", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/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==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "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.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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-decorators": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz", + "integrity": "sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-decorators": "^7.27.1" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz", + "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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-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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", + "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", + "dev": true, + "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.7" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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-regenerator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", + "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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-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==", + "dev": true, + "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.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", + "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", + "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "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-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==", + "dev": true, + "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/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "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==", + "dev": true, + "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.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.8.tgz", + "integrity": "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.8.tgz", + "integrity": "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.8.tgz", + "integrity": "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.8.tgz", + "integrity": "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.8.tgz", + "integrity": "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.8.tgz", + "integrity": "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.8.tgz", + "integrity": "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.8.tgz", + "integrity": "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.8.tgz", + "integrity": "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.8.tgz", + "integrity": "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.8.tgz", + "integrity": "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.8.tgz", + "integrity": "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.8.tgz", + "integrity": "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.8.tgz", + "integrity": "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.8.tgz", + "integrity": "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.8.tgz", + "integrity": "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.8.tgz", + "integrity": "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.8.tgz", + "integrity": "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.8.tgz", + "integrity": "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.8.tgz", + "integrity": "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.8.tgz", + "integrity": "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.8.tgz", + "integrity": "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.8.tgz", + "integrity": "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.8.tgz", + "integrity": "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.8.tgz", + "integrity": "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.8.tgz", + "integrity": "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@firebase/ai": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-1.4.1.tgz", + "integrity": "sha512-bcusQfA/tHjUjBTnMx6jdoPMpDl3r8K15Z+snHz9wq0Foox0F/V+kNLXucEOHoTL2hTc9l+onZCyBJs2QoIC3g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.17", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.17.tgz", + "integrity": "sha512-n5vfBbvzduMou/2cqsnKrIes4auaBjdhg8QNA2ZQZ59QgtO2QiwBaXQZQE4O4sgB0Ds1tvLgUUkY+pwzu6/xEg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.23", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.23.tgz", + "integrity": "sha512-3AdO10RN18G5AzREPoFgYhW6vWXr3u+OYQv6pl3CX6Fky8QRk0AHurZlY3Q1xkXO0TDxIsdhO3y65HF7PBOJDw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.17", + "@firebase/analytics-types": "0.8.3", + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.3.tgz", + "integrity": "sha512-VrIp/d8iq2g501qO46uGz3hjbDb8xzYMrbu8Tp0ovzIzrvJZ2fvmj649gTjge/b7cCCcjT0H37g1gVtlNhnkbg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.13.2.tgz", + "integrity": "sha512-jwtMmJa1BXXDCiDx1vC6SFN/+HfYG53UkfJa6qeN5ogvOunzbFDO3wISZy5n9xgYFUrEP6M7e8EG++riHNTv9w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.10.1.tgz", + "integrity": "sha512-MgNdlms9Qb0oSny87pwpjKush9qUwCJhfmTJHDfrcKo4neLGiSeVE4qJkzP7EQTIUFKp84pbTxobSAXkiuQVYQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.26.tgz", + "integrity": "sha512-PkX+XJMLDea6nmnopzFKlr+s2LMQGqdyT2DHdbx1v1dPSqOol2YzgpgymmhC67vitXVpNvS3m/AiWQWWhhRRPQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.10.1", + "@firebase/app-check-types": "0.5.3", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.3.tgz", + "integrity": "sha512-hyl5rKSj0QmwPdsAxrI5x1otDlByQ7bvNvVt8G/XPO2CSwE++rmSVf3VEhaeOR4J8ZFaF0Z0NDSmLejPweZ3ng==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.4.2.tgz", + "integrity": "sha512-LssbyKHlwLeiV8GBATyOyjmHcMpX/tFjzRUCS1jnwGAew1VsBB4fJowyS5Ud5LdFbYpJeS+IQoC+RQxpK7eH3Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.13.2", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth": { + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.10.8.tgz", + "integrity": "sha512-GpuTz5ap8zumr/ocnPY57ZanX02COsXloY6Y/2LYPAuXYiaJRf6BAGDEdRq1BMjP93kqQnKNuKZUTMZbQ8MNYA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.5.28", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.28.tgz", + "integrity": "sha512-HpMSo/cc6Y8IX7bkRIaPPqT//Jt83iWy5rmDWeThXQCAImstkdNo3giFLORJwrZw2ptiGkOij64EH1ztNJzc7Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.10.8", + "@firebase/auth-types": "0.13.0", + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.0.tgz", + "integrity": "sha512-S/PuIjni0AQRLF+l9ck0YpsMOdE8GO2KU6ubmBB7P+7TJUCQDa3R1dlgYm9UzGbbePMZsp0xzB93f2b/CgxMOg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.18.tgz", + "integrity": "sha512-n28kPCkE2dL2U28fSxZJjzPPVpKsQminJ6NrzcKXAI0E/lYC8YhfwpyllScqVEvAI3J2QgJZWYgrX+1qGI+SQQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.3.10.tgz", + "integrity": "sha512-VMVk7zxIkgwlVQIWHOKFahmleIjiVFwFOjmakXPd/LDgaB/5vzwsB5DWIYo+3KhGxWpidQlR8geCIn39YflJIQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.20.tgz", + "integrity": "sha512-H9Rpj1pQ1yc9+4HQOotFGLxqAXwOzCHsRSRjcQFNOr8lhUt6LeYjf0NSRL04sc4X0dWe8DsCvYKxMYvFG/iOJw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.11.tgz", + "integrity": "sha512-itEsHARSsYS95+udF/TtIzNeQ0Uhx4uIna0sk4E0wQJBUnLc/G1X6D7oRljoOuwwCezRLGvWBRyNrugv/esOEw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/database": "1.0.20", + "@firebase/database-types": "1.0.15", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.15.tgz", + "integrity": "sha512-XWHJ0VUJ0k2E9HDMlKxlgy/ZuTa9EvHCGLjaKSUvrQnwhgZuRU5N3yX6SZ+ftf2hTzZmfRkv+b3QRvGg40bKNw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.12.1" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.8.0.tgz", + "integrity": "sha512-QSRk+Q1/CaabKyqn3C32KSFiOdZpSqI9rpLK5BHPcooElumOBooPFa6YkDdiT+/KhJtel36LdAacha9BptMj2A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "@firebase/webchannel-wrapper": "1.0.3", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.3.53", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.53.tgz", + "integrity": "sha512-qI3yZL8ljwAYWrTousWYbemay2YZa+udLWugjdjju2KODWtLG94DfO4NALJgPLv8CVGcDHNFXoyQexdRA0Cz8Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/firestore": "4.8.0", + "@firebase/firestore-types": "3.0.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.3.tgz", + "integrity": "sha512-hD2jGdiWRxB/eZWF89xcK9gF8wvENDJkzpVFb4aGkzfEaKxVRD1kjz1t1Wj8VZEp2LCB53Yx1zD8mrhQu87R6Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.12.9", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.12.9.tgz", + "integrity": "sha512-FG95w6vjbUXN84Ehezc2SDjGmGq225UYbHrb/ptkRT7OTuCiQRErOQuyt1jI1tvcDekdNog+anIObihNFz79Lg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.18", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.3.26", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.26.tgz", + "integrity": "sha512-A798/6ff5LcG2LTWqaGazbFYnjBW8zc65YfID/en83ALmkhu2b0G8ykvQnLtakbV9ajrMYPn7Yc/XcYsZIUsjA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/functions": "0.12.9", + "@firebase/functions-types": "0.6.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.3.tgz", + "integrity": "sha512-EZoDKQLUHFKNx6VLipQwrSMh01A1SaL3Wg6Hpi//x6/fJ6Ee4hrAeswK99I5Ht8roiniKHw4iO0B1Oxj5I4plg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.18", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.18.tgz", + "integrity": "sha512-NQ86uGAcvO8nBRwVltRL9QQ4Reidc/3whdAasgeWCPIcrhOKDuNpAALa6eCVryLnK14ua2DqekCOX5uC9XbU/A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.18.tgz", + "integrity": "sha512-aLFohRpJO5kKBL/XYL4tN+GdwEB/Q6Vo9eZOM/6Kic7asSUgmSfGPpGUZO1OAaSRGwF4Lqnvi1f/f9VZnKzChw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/installations-types": "0.5.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.3.tgz", + "integrity": "sha512-2FJI7gkLqIE0iYsNQ1P751lO3hER+Umykel+TkLwHj6plzWVxqvfclPUZhcKFVQObqloEBTmpi2Ozn7EkCABAA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz", + "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.12.22", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.22.tgz", + "integrity": "sha512-GJcrPLc+Hu7nk+XQ70Okt3M1u1eRr2ZvpMbzbc54oTPJZySHcX9ccZGVFcsZbSZ6o1uqumm8Oc7OFkD3Rn1/og==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/messaging-interop-types": "0.2.3", + "@firebase/util": "1.12.1", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.22.tgz", + "integrity": "sha512-5ZHtRnj6YO6f/QPa/KU6gryjmX4Kg33Kn4gRpNU6M1K47Gm8kcQwPkX7erRUYEH1mIWptfvjvXMHWoZaWjkU7A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/messaging": "0.12.22", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.3.tgz", + "integrity": "sha512-xfzFaJpzcmtDjycpDeCUj0Ge10ATFi/VHVIvEEjDNc3hodVBQADZ7BWQU7CuFpjSHE+eLuBI13z5F/9xOoGX8Q==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.7", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.7.tgz", + "integrity": "sha512-JTlTQNZKAd4+Q5sodpw6CN+6NmwbY72av3Lb6wUKTsL7rb3cuBIhQSrslWbVz0SwK3x0ZNcqX24qtRbwKiv+6w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.20.tgz", + "integrity": "sha512-XkFK5NmOKCBuqOKWeRgBUFZZGz9SzdTZp4OqeUg+5nyjapTiZ4XoiiUL8z7mB2q+63rPmBl7msv682J3rcDXIQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/performance": "0.7.7", + "@firebase/performance-types": "0.2.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.3.tgz", + "integrity": "sha512-IgkyTz6QZVPAq8GSkLYJvwSLr3LS9+V6vNPQr0x4YozZJiLF5jYixj0amDtATf1X0EtYHqoPO48a9ija8GocxQ==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.6.5.tgz", + "integrity": "sha512-fU0c8HY0vrVHwC+zQ/fpXSqHyDMuuuglV94VF6Yonhz8Fg2J+KOowPGANM0SZkLvVOYpTeWp3ZmM+F6NjwWLnw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/installations": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.18", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.18.tgz", + "integrity": "sha512-YiETpldhDy7zUrnS8e+3l7cNs0sL7+tVAxvVYU0lu7O+qLHbmdtAxmgY+wJqWdW2c9nDvBFec7QiF58pEUu0qQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/logger": "0.4.4", + "@firebase/remote-config": "0.6.5", + "@firebase/remote-config-types": "0.4.0", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.4.0.tgz", + "integrity": "sha512-7p3mRE/ldCNYt8fmWMQ/MSGRmXYlJ15Rvs9Rk17t8p0WwZDbeK7eRmoI1tvCPaDzn9Oqh+yD6Lw+sGLsLg4kKg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.13.14", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.13.14.tgz", + "integrity": "sha512-xTq5ixxORzx+bfqCpsh+o3fxOsGoDjC1nO0Mq2+KsOcny3l7beyBhP/y1u5T6mgsFQwI1j6oAkbT5cWdDBx87g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.24.tgz", + "integrity": "sha512-XHn2tLniiP7BFKJaPZ0P8YQXKiVJX+bMyE2j2YWjYfaddqiJnROJYqSomwW6L3Y+gZAga35ONXUJQju6MB6SOQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.6.18", + "@firebase/storage": "0.13.14", + "@firebase/storage-types": "0.8.3", + "@firebase/util": "1.12.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.3.tgz", + "integrity": "sha512-+Muk7g9uwngTpd8xn9OdF/D48uiQ7I1Fae7ULsWPuKoCH3HU7bfFPhxtJYzyhjdniowhuDpQcfPmuNRAqZEfvg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.12.1.tgz", + "integrity": "sha512-zGlBn/9Dnya5ta9bX/fgEoNC3Cp8s6h+uYPYaDieZsFOAdHP/ExzQ/eaDgxD3GOROdPkLKpvKY0iIzr9adle0w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.3.tgz", + "integrity": "sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==", + "license": "Apache-2.0" + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.1.1.tgz", + "integrity": "sha512-TpIO93+DIujg3g7SykEAGZMDtbJRrmnYRCNYSjJlvIbGhBjRSNTLVbNeDQBrzy9qDgUbiWdc7KA0uZHZ2tJmiw==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.1.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.0.0.tgz", + "integrity": "sha512-PGMrIYXLGA5K8RWy8zwBkd4vFi4z7ubxtet6Yn13Plf6krRTwPbdlCwlcfmoX0R7B4Z643QvrtHmdQ5fNtfFCg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz", + "integrity": "sha512-X48nISrSOa89zu2VMljC4XaRf8NmgTwQBVHfS2Nu5G00ZwM31oOVrAtGxZF3b6wDYf9lJsf/Eq4cCSFKIkOWPQ==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.0.0.tgz", + "integrity": "sha512-obBEF+zd98r/KtKVW6A+8UGWeaOoyMpl6Q9P3FzHsOnsg742aXsl8v+H/zp09qSSu/a/Hxe9LNKzbBaQq1CEbA==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-brands-svg-icons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.0.0.tgz", + "integrity": "sha512-C8oY28gq/Qx/cHReJa2AunKJUHvUZDVoPlSTHtAvjriaNfi+5nugW4cx7yA/xN3f/nYkElw11gFBoJ2xUDDFgg==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-regular-svg-icons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.0.0.tgz", + "integrity": "sha512-qAh0mTaCY22sQzMK2lKBrtn/aR4keUu5XmtdYR7d702laMe0h+Ab4Kj2pExR9HZkKhjKoq8pbwt8Td+mjW/ipQ==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.0.0.tgz", + "integrity": "sha512-njSLAllkOddYDCXgTFboXn54Oe5FcvpkWq+FoetOHR64PbN0608kM02Lze0xtISGpXgP+i26VyXRQA0Irh3Obw==", + "license": "(CC-BY-4.0 AND MIT)", + "dependencies": { + "@fortawesome/fontawesome-common-types": "7.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/vue-fontawesome": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fortawesome/vue-fontawesome/-/vue-fontawesome-3.1.1.tgz", + "integrity": "sha512-U5azn4mcUVpjHe4JO0Wbe7Ih8e3VbN83EH7OTBtA5/QGw9qcPGffqcmwsLyZYgEkpVkYbq/6dX1Iyl5KUGMp6Q==", + "license": "MIT", + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7", + "vue": ">= 3.0.0 < 4" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@headlessui/vue": { + "version": "1.7.23", + "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz", + "integrity": "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==", + "license": "MIT", + "dependencies": { + "@tanstack/vue-virtual": "^3.0.0-beta.60" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@heroicons/vue": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/vue/-/vue-2.2.0.tgz", + "integrity": "sha512-G3dbSxoeEKqbi/DFalhRxJU4mTXJn7GwZ7ae8NuEQzd1bqdd0jAbdaBZlHPcvPD2xI1iGzNVB4k20Un2AguYPw==", + "license": "MIT", + "peerDependencies": { + "vue": ">= 3" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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==", + "dev": true, + "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/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "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==", + "dev": true, + "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.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, + "node_modules/@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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@one-ini/wasm": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz", + "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@primeuix/styled": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@primeuix/styled/-/styled-0.7.2.tgz", + "integrity": "sha512-tIJ6byZezTYZ9YUICNSidQHOIQOQL3zeUgjwiX0JnBTK3+WCvy4DyCBcrJ94RtiX0WGFZSYNvaGaFkTo4jU8FQ==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.1" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primeuix/styles": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@primeuix/styles/-/styles-1.2.3.tgz", + "integrity": "sha512-+KwmQsLTYgVAqFADmO252btz40lstPML6r4QMNjxz4gLNCKVW3kPR0/aCouQx6/21+boXG1P68tu8Zk3FAKr2w==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.2" + } + }, + "node_modules/@primeuix/themes": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@primeuix/themes/-/themes-1.2.3.tgz", + "integrity": "sha512-GLAU2h6lhgln2w10EQalUQlgwbgQ0xZoIOLMNGfIvqU4O09L282P7rwKCKQksvAGAFt1GoO/Q1NgBSxnttr7iA==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.2" + } + }, + "node_modules/@primeuix/utils": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.6.1.tgz", + "integrity": "sha512-tQL/ZOPgCdD+NTimlUmhyD0ey8J1XmpZE4hDHM+/fnuBicVVmlKOd5HpS748LcOVRUKbWjmEPdHX4hi5XZoC1Q==", + "license": "MIT", + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/core": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@primevue/core/-/core-4.3.7.tgz", + "integrity": "sha512-rYmEZTKs/C2Re+xFluY9R02+n4TcyJshooPPOG6zc23v/bmhrfcqCiS5gNiuXF/lYoOh4y8VTcHF6eZCZW724Q==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.2", + "@primeuix/utils": "^0.6.1" + }, + "engines": { + "node": ">=12.11.0" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/@primevue/icons": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@primevue/icons/-/icons-4.3.7.tgz", + "integrity": "sha512-CKiOeiJzNSbELwbQdrgWHgmfcAmjNQXqRZtBGb8sUPrB8hjWf9lYMBAbcAuqJbyLFFcC0lFiB80CfBR07FNSHw==", + "license": "MIT", + "dependencies": { + "@primeuix/utils": "^0.6.1", + "@primevue/core": "4.3.7" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@primevue/themes": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@primevue/themes/-/themes-4.3.7.tgz", + "integrity": "sha512-Ej/3CBGpSIJFo0BJHrPa7HjyQZ8YPxQUiOuXj1tKFJkSqFxgOk52JL6aLBGdQkSnx7TjkHuHuKuk4t6SKkD5hw==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.2", + "@primeuix/themes": "^1.2.3" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.30", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.30.tgz", + "integrity": "sha512-whXaSoNUFiyDAjkUF8OBpOm77Szdbk5lGNqFe6CbVbJFrhCCPinCbRA3NjawwlNHla1No7xvXXh+CpSxnPfUEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-babel": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", + "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^7.0.3", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@shopify/app-bridge": { + "version": "3.7.10", + "resolved": "https://registry.npmjs.org/@shopify/app-bridge/-/app-bridge-3.7.10.tgz", + "integrity": "sha512-zy4c05DEOkYXm1yWIe0FrI0lq6hzffIT4JWzb9XKqY5OPRNHRHg+ihDt0ZnR8eMVmYV03yStpGAITIfqOgn4TQ==", + "license": "MIT", + "dependencies": { + "@shopify/app-bridge-core": "1.1.1", + "base64url": "^3.0.1", + "web-vitals": "^3.0.1" + } + }, + "node_modules/@shopify/app-bridge-core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@shopify/app-bridge-core/-/app-bridge-core-1.1.1.tgz", + "integrity": "sha512-kEnJUpkC9vDdmGMN3Mezq/FlDBqP+DFg/A1PFzqTW8FILu0wzFmz1aFk4Hxy6Y6ilceMmn8QXsyLA9DfaeH4jQ==", + "license": "MIT" + }, + "node_modules/@shopify/app-bridge-utils": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@shopify/app-bridge-utils/-/app-bridge-utils-3.5.1.tgz", + "integrity": "sha512-VZRvRfg1nF/0/C7zIwYlQ2RXY2S7ALw04Lp6vXPkzBxggDYakx9Lbvc5/k7ZTvhwZv2q8+FVHh319IMW81b27Q==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "@shopify/app-bridge": "^3.5.1" + } + }, + "node_modules/@shopify/app-bridge/node_modules/web-vitals": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-3.5.2.tgz", + "integrity": "sha512-c0rhqNcHXRkY/ogGDJQxZ9Im9D19hDihbzSQJrsioex+KnFgmMzBiy57Z1EjkhX/+OjyBpclDCzz2ITtjokFmg==", + "license": "Apache-2.0" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@svgdotjs/svg.draggable.js": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz", + "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==", + "license": "MIT", + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4" + } + }, + "node_modules/@svgdotjs/svg.filter.js": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz", + "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==", + "license": "MIT", + "dependencies": { + "@svgdotjs/svg.js": "^3.2.4" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/@svgdotjs/svg.js": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz", + "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Fuzzyma" + } + }, + "node_modules/@svgdotjs/svg.resize.js": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz", + "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==", + "license": "MIT", + "engines": { + "node": ">= 14.18" + }, + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4", + "@svgdotjs/svg.select.js": "^4.0.1" + } + }, + "node_modules/@svgdotjs/svg.select.js": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz", + "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", + "license": "MIT", + "engines": { + "node": ">= 14.18" + }, + "peerDependencies": { + "@svgdotjs/svg.js": "^3.2.4" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", + "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.12", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz", + "integrity": "sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, + "node_modules/@trickfilm400/rollup-plugin-off-main-thread": { + "version": "3.0.0-pre1", + "resolved": "https://registry.npmjs.org/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz", + "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.10", + "json5": "^2.2.3", + "magic-string": "^0.30.21", + "string.prototype.matchall": "^4.0.12" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@tsconfig/node22": { + "version": "22.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node22/-/node22-22.0.2.tgz", + "integrity": "sha512-Kmwj4u8sDRDrMYRoN9FDEcXD8UpBSaPQQ24Gz+Gamqfm7xxn+GBR7ge/Z7pK8OXNGyUzbSwJj+TH6B+DS/epyA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/dotenv": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-8.2.3.tgz", + "integrity": "sha512-g2FXjlDX/cYuc5CiQvyU/6kkbP1JtmGzh0obW50zD7OKeILVL0NSpPWLXVfqoAGQjom2/SLLx9zHq0KXvD6mbw==", + "deprecated": "This is a stub types definition. dotenv provides its own type definitions, so you do not need this installed.", + "dev": true, + "license": "MIT", + "dependencies": { + "dotenv": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.17.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.17.0.tgz", + "integrity": "sha512-bbAKTCqX5aNVryi7qXVMi+OkB3w/OyblodicMbvE38blyAz7GxXf6XYhklokijuPwwVg9sDLKRxt0ZHXQwZVfQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, + "node_modules/@types/webfontloader": { + "version": "1.6.38", + "resolved": "https://registry.npmjs.org/@types/webfontloader/-/webfontloader-1.6.38.tgz", + "integrity": "sha512-kUaF72Fv202suFx6yBrwXqeVRMx7hGtJTesyESZgn9sEPCUeDXm2p0SiyS1MTqW74nQP4p7JyrOCwZ7pNFns4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz", + "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/type-utils": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.38.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz", + "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz", + "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.38.0", + "@typescript-eslint/types": "^8.38.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz", + "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz", + "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz", + "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz", + "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz", + "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.38.0", + "@typescript-eslint/tsconfig-utils": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/visitor-keys": "8.38.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz", + "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.38.0", + "@typescript-eslint/types": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz", + "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.38.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.2.0.tgz", + "integrity": "sha512-DSTrmrdLp+0LDNF77fqrKfx7X0ErRbOcUAgJL/HbSesqQwoUvUQ4uYQqaex+rovqgGcoPqVk+AwUh3v9CuiYIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1", + "@rolldown/pluginutils": "^1.0.0-beta.9", + "@vue/babel-plugin-jsx": "^1.4.0" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.0.0" + } + }, + "node_modules/@vitest/eslint-plugin": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.20.tgz", + "integrity": "sha512-2eLsgUm+GVOpDfNyH2do//MiNO/WZkXrPi+EjDmXEdUt6Jwnziq4H221L8vJE0aJys+l1FRfSkm4QbaIyDCfBg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@typescript-eslint/utils": ">= 8.0", + "eslint": ">= 8.57.0", + "typescript": ">= 5.0.0", + "vitest": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.15.tgz", + "integrity": "sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.15" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.15.tgz", + "integrity": "sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.15.tgz", + "integrity": "sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue-flow/background": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@vue-flow/background/-/background-1.3.2.tgz", + "integrity": "sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==", + "license": "MIT", + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/controls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@vue-flow/controls/-/controls-1.1.2.tgz", + "integrity": "sha512-6dtl/JnwDBNau5h3pDBdOCK6tdxiVAOL3cyruRL61gItwq5E97Hmjmj2BIIqX2p7gU1ENg3z80Z4zlu58fGlsg==", + "license": "MIT", + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/core": { + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/@vue-flow/core/-/core-1.45.0.tgz", + "integrity": "sha512-+Qd4fTnCfrhfYQzlHyf5Jt7rNE4PlDnEJEJZH9v6hDZoTOeOy1RhS85cSxKYxdsJ31Ttj2v3yabhoVfBf+bmJA==", + "license": "MIT", + "dependencies": { + "@vueuse/core": "^10.5.0", + "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, + "node_modules/@vue-flow/minimap": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@vue-flow/minimap/-/minimap-1.5.3.tgz", + "integrity": "sha512-w8VQc8orPdzfstIPI4/u6H7qlc/uVM1W6b5Upd5NQi0+S9seYl3CiUrzO9liW/f8Fuvr5oHVQg0X6nn2K083rA==", + "license": "MIT", + "dependencies": { + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0" + }, + "peerDependencies": { + "@vue-flow/core": "^1.23.0", + "vue": "^3.3.0" + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.4.0.tgz", + "integrity": "sha512-mCokbouEQ/ocRce/FpKCRItGo+013tHg7tixg3DUNS+6bmIchPt66012kBMm476vyEIJPafrvOf4E5OYj3shSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.4.0.tgz", + "integrity": "sha512-9zAHmwgMWlaN6qRKdrg1uKsBKHvnUU+Py+MOCTuYZBoZsopa90Di10QRjB+YPnVss0BZbG/H5XFwJY1fTxJWhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "@vue/babel-helper-vue-transform-on": "1.4.0", + "@vue/babel-plugin-resolve-type": "1.4.0", + "@vue/shared": "^3.5.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.4.0.tgz", + "integrity": "sha512-4xqDRRbQQEWHQyjlYSgZsWj44KfiF6D+ktCuXyZ8EnVDYV3pztmXJDf1HveAjUAXxAnR8daCQT51RneWWxtTyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/parser": "^7.26.9", + "@vue/compiler-sfc": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.18.tgz", + "integrity": "sha512-3slwjQrrV1TO8MoXgy3aynDQ7lslj5UqDxuHnrzHtpON5CBinhWjJETciPngpin/T3OuW3tXUf86tEurusnztw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@vue/shared": "3.5.18", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.18.tgz", + "integrity": "sha512-RMbU6NTU70++B1JyVJbNbeFkK+A+Q7y9XKE2EM4NLGm2WFR8x9MbAtWxPPLdm0wUkuZv9trpwfSlL6tjdIa1+A==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.18", + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.18.tgz", + "integrity": "sha512-5aBjvGqsWs+MoxswZPoTB9nSDb3dhd1x30xrrltKujlCxo48j8HGDNj3QPhF4VIS0VQDUrA1xUfp2hEa+FNyXA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@vue/compiler-core": "3.5.18", + "@vue/compiler-dom": "3.5.18", + "@vue/compiler-ssr": "3.5.18", + "@vue/shared": "3.5.18", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.18.tgz", + "integrity": "sha512-xM16Ak7rSWHkM3m22NlmcdIM+K4BMyFARAfV9hYFl+SFuRzrZ3uGMNW05kA5pmeMa0X9X963Kgou7ufdbpOP9g==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.18", + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/devtools-core": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.7.7.tgz", + "integrity": "sha512-9z9TLbfC+AjAi1PQyWX+OErjIaJmdFlbDHcD+cAMYKY6Bh5VlsAtCeGyRMrXwIlMEQPukvnWt3gZBLwTAIMKzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.7", + "@vue/devtools-shared": "^7.7.7", + "mitt": "^3.0.1", + "nanoid": "^5.1.0", + "pathe": "^2.0.3", + "vite-hot-client": "^2.0.4" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz", + "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.7", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz", + "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-prettier/-/eslint-config-prettier-10.2.0.tgz", + "integrity": "sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2" + }, + "peerDependencies": { + "eslint": ">= 8.21.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "14.6.0", + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-14.6.0.tgz", + "integrity": "sha512-UpiRY/7go4Yps4mYCjkvlIbVWmn9YvPGQDxTAlcKLphyaD77LjIu3plH4Y9zNT0GB4f3K5tMmhhtRhPOgrQ/bQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.35.1", + "fast-glob": "^3.3.3", + "typescript-eslint": "^8.35.1", + "vue-eslint-parser": "^10.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": "^9.10.0", + "eslint-plugin-vue": "^9.28.0 || ^10.0.0", + "typescript": ">=4.8.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.2.12.tgz", + "integrity": "sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.15", + "@vue/compiler-dom": "^3.5.0", + "@vue/compiler-vue2": "^2.7.16", + "@vue/shared": "^3.5.0", + "alien-signals": "^1.0.3", + "minimatch": "^9.0.3", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.18.tgz", + "integrity": "sha512-x0vPO5Imw+3sChLM5Y+B6G1zPjwdOri9e8V21NnTnlEvkxatHEH5B5KEAJcjuzQ7BsjGrKtfzuQ5eQwXh8HXBg==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.18.tgz", + "integrity": "sha512-DUpHa1HpeOQEt6+3nheUfqVXRog2kivkXHUhoqJiKR33SO4x+a5uNOMkV487WPerQkL0vUuRvq/7JhRgLW3S+w==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.18", + "@vue/shared": "3.5.18" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.18.tgz", + "integrity": "sha512-YwDj71iV05j4RnzZnZtGaXwPoUWeRsqinblgVJwR8XTXYZ9D5PbahHQgsbmzUvCWNF6x7siQ89HgnX5eWkr3mw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.18", + "@vue/runtime-core": "3.5.18", + "@vue/shared": "3.5.18", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.18.tgz", + "integrity": "sha512-PvIHLUoWgSbDG7zLHqSqaCoZvHi6NNmfVFOqO+OnwvqMz/tqQr3FuGWS8ufluNddk7ZLBJYMrjcw1c6XzR12mA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.18", + "@vue/shared": "3.5.18" + }, + "peerDependencies": { + "vue": "3.5.18" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.18.tgz", + "integrity": "sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==", + "license": "MIT" + }, + "node_modules/@vue/test-utils": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.6.tgz", + "integrity": "sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-beautify": "^1.14.9", + "vue-component-type-helpers": "^2.0.0" + } + }, + "node_modules/@vue/tsconfig": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz", + "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@yr/monotone-cubic-spline": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", + "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", + "license": "MIT" + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/alien-signals": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz", + "integrity": "sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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/apexcharts": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-4.4.0.tgz", + "integrity": "sha512-JGsHeQEKDlQh1rob8aBai9/HKvXIpbZ83TnobKZAcdOELf+oQZaxZyAnbbldr6PPBdCgG2zzzLaP1dtEsJxzWw==", + "license": "MIT", + "dependencies": { + "@svgdotjs/svg.draggable.js": "^3.0.4", + "@svgdotjs/svg.filter.js": "^3.0.8", + "@svgdotjs/svg.js": "^3.2.4", + "@svgdotjs/svg.resize.js": "^2.0.2", + "@svgdotjs/svg.select.js": "^4.0.1", + "@yr/monotone-cubic-spline": "^1.0.3" + } + }, + "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==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "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.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "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-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==", + "dev": true, + "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/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.21", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz", + "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/birpc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.5.0.tgz", + "integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.9.tgz", + "integrity": "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==", + "dev": true, + "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": { + "baseline-browser-mapping": "^2.11.20", + "caniuse-lite": "^1.0.30001810", + "electron-to-chromium": "^1.5.420", + "node-releases": "^2.0.54", + "update-browserslist-db": "^1.3.2" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "dev": true, + "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/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "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/chart.js": { + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", + "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, + "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/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/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/cliui/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/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/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": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cssstyle/node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "license": "MIT" + }, + "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/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/dompurify": { + "version": "3.4.15", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.15.tgz", + "integrity": "sha512-EUBjM+B+lkDE41iE82DDSCfkoPGfXx8IxFxPMjNzm/Uk4xDet77rTN9wqlxlVg71kK7XGuUMv6wUxJUwwv+Xyw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/easy-bem": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/easy-bem/-/easy-bem-1.1.1.tgz", + "integrity": "sha512-GJRqdiy2h+EXy6a8E6R+ubmqUM08BK0FWNq41k24fup6045biQ8NXxoXimiwegMQvFFV3t1emADdGNL1TlS61A==", + "license": "MIT" + }, + "node_modules/editorconfig": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz", + "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@one-ini/wasm": "0.1.1", + "commander": "^10.0.0", + "minimatch": "9.0.1", + "semver": "^7.5.3" + }, + "bin": { + "editorconfig": "bin/editorconfig" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/editorconfig/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.423", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.423.tgz", + "integrity": "sha512-rRZfTSY8ptHYMQxa+uIycJMFKmY1T0GIApNMXJYGehguTZa56TEEl19pKPCoBqk5Gpf7QizZn/jt7xur+DYxag==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/engine.io-client": { + "version": "6.6.6", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.6.tgz", + "integrity": "sha512-iY6QdftLQ9pyiPoX082bpf/u1UewnOaJrtJIF9T0++QB34lZrj0uP+Q/bj8AlUsAxqhnkTV2BS8SBZSxOmoV5Q==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.21.0", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.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/error-stack-parser-es": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz", + "integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "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/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.8.tgz", + "integrity": "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.8", + "@esbuild/android-arm": "0.25.8", + "@esbuild/android-arm64": "0.25.8", + "@esbuild/android-x64": "0.25.8", + "@esbuild/darwin-arm64": "0.25.8", + "@esbuild/darwin-x64": "0.25.8", + "@esbuild/freebsd-arm64": "0.25.8", + "@esbuild/freebsd-x64": "0.25.8", + "@esbuild/linux-arm": "0.25.8", + "@esbuild/linux-arm64": "0.25.8", + "@esbuild/linux-ia32": "0.25.8", + "@esbuild/linux-loong64": "0.25.8", + "@esbuild/linux-mips64el": "0.25.8", + "@esbuild/linux-ppc64": "0.25.8", + "@esbuild/linux-riscv64": "0.25.8", + "@esbuild/linux-s390x": "0.25.8", + "@esbuild/linux-x64": "0.25.8", + "@esbuild/netbsd-arm64": "0.25.8", + "@esbuild/netbsd-x64": "0.25.8", + "@esbuild/openbsd-arm64": "0.25.8", + "@esbuild/openbsd-x64": "0.25.8", + "@esbuild/openharmony-arm64": "0.25.8", + "@esbuild/sunos-x64": "0.25.8", + "@esbuild/win32-arm64": "0.25.8", + "@esbuild/win32-ia32": "0.25.8", + "@esbuild/win32-x64": "0.25.8" + } + }, + "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-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-playwright": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-2.2.2.tgz", + "integrity": "sha512-j0jKpndIPOXRRP9uMkwb9l/nSmModOU3452nrFdgFJoEv/435J1onk8+aITzjDW8DfypxgmVaDMdmVIa6F7I0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "globals": "^13.23.0" + }, + "engines": { + "node": ">=16.6.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/eslint-plugin-playwright/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.3.tgz", + "integrity": "sha512-NAdMYww51ehKfDyDhv59/eIItUVzU0Io9H2E8nHNGKEeeqlnci+1gCvrHib6EmZdf6GxF+LCV5K7UC65Ezvw7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", + "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-vue/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue/node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-vue/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-vue/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-plugin-vue/node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz", + "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/bgub/eta?sponsor=1" + } + }, + "node_modules/execa": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.0.tgz", + "integrity": "sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "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==", + "dev": true, + "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-glob/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==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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/firebase": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-11.10.0.tgz", + "integrity": "sha512-nKBXoDzF0DrXTBQJlZa+sbC5By99ysYU1D6PkMRYknm0nCW7rJly47q492Ht7Ndz5MeYSBuboKuhS1e6mFC03w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "1.4.1", + "@firebase/analytics": "0.10.17", + "@firebase/analytics-compat": "0.2.23", + "@firebase/app": "0.13.2", + "@firebase/app-check": "0.10.1", + "@firebase/app-check-compat": "0.3.26", + "@firebase/app-compat": "0.4.2", + "@firebase/app-types": "0.9.3", + "@firebase/auth": "1.10.8", + "@firebase/auth-compat": "0.5.28", + "@firebase/data-connect": "0.3.10", + "@firebase/database": "1.0.20", + "@firebase/database-compat": "2.0.11", + "@firebase/firestore": "4.8.0", + "@firebase/firestore-compat": "0.3.53", + "@firebase/functions": "0.12.9", + "@firebase/functions-compat": "0.3.26", + "@firebase/installations": "0.6.18", + "@firebase/installations-compat": "0.2.18", + "@firebase/messaging": "0.12.22", + "@firebase/messaging-compat": "0.2.22", + "@firebase/performance": "0.7.7", + "@firebase/performance-compat": "0.2.20", + "@firebase/remote-config": "0.6.5", + "@firebase/remote-config-compat": "0.2.18", + "@firebase/storage": "0.13.14", + "@firebase/storage-compat": "0.3.24", + "@firebase/util": "1.12.1" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/floating-vue": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/floating-vue/-/floating-vue-5.2.2.tgz", + "integrity": "sha512-afW+h2CFafo+7Y9Lvw/xsqjaQlKLdJV7h1fCHfcYQ1C4SVMlu7OAekqWgu5d4SgvkBVU0pVpLlVsrSTBURFRkg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "~1.1.1", + "vue-resize": "^2.0.0-alpha.1" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.0", + "vue": "^3.2.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "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/fs-extra": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "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/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "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-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, + "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": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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", + "dev": true, + "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": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "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/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-beautify": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz", + "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.13", + "editorconfig": "^1.0.4", + "glob": "^10.4.2", + "js-cookie": "^3.0.5", + "nopt": "^7.2.1" + }, + "bin": { + "css-beautify": "js/bin/css-beautify.js", + "html-beautify": "js/bin/html-beautify.js", + "js-beautify": "js/bin/js-beautify.js" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "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/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "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/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "dev": true, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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/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/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.54", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", + "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-run-all2": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-7.0.2.tgz", + "integrity": "sha512-7tXR+r9hzRNOPNTvXegM+QzCuMjzUIIq66VDunL6j60O4RrExx32XUhlrS7UK4VcdGw5/Wxzb3kfNcFix9JKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "cross-spawn": "^7.0.6", + "memorystream": "^0.3.1", + "minimatch": "^9.0.0", + "pidtree": "^0.6.0", + "read-package-json-fast": "^4.0.0", + "shell-quote": "^1.7.3", + "which": "^5.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "npm-run-all2": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0", + "npm": ">= 9" + } + }, + "node_modules/npm-run-all2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm-run-all2/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/npm-run-all2/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nwsapi": { + "version": "2.2.21", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", + "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "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": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "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.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-ms": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz", + "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/primevue": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/primevue/-/primevue-4.3.7.tgz", + "integrity": "sha512-yyknh2kFjSGwRyxqmUUWy/pY4FK1vxoonGH3eQdtd8k/sGl5JpSQ2f0tXpR79lV6dcGf3FZPL1B5JNR11zJfmg==", + "license": "MIT", + "dependencies": { + "@primeuix/styled": "^0.7.2", + "@primeuix/styles": "^1.2.3", + "@primeuix/utils": "^0.6.1", + "@primevue/core": "4.3.7", + "@primevue/icons": "4.3.7" + }, + "engines": { + "node": ">=12.11.0" + } + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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==", + "dev": true, + "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/read-package-json-fast": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", + "integrity": "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "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": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "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-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.7.tgz", + "integrity": "sha512-YAy8Od6KV+uuwUuU50np8fGB/Aues6Y0nAhA9y/hId74PlKUcme4pXcBD46NWKr1Q4osN/iseZ17YqO1XfmI8g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "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==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/socket.io-client": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz", + "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/source-map/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/source-map/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.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==", + "dev": true, + "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==", + "dev": true, + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/superjson": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz", + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "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==", + "dev": true, + "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==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/timezone-select-js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/timezone-select-js/-/timezone-select-js-2.0.1.tgz", + "integrity": "sha512-alGTIkA0193yj+cE2oDa47VMrfYvJGgx6waZm1s0UnVF5hGYZGZkcfd2DtFqjf/v5Qs+Fl6hR7jeUrPMbfWLSg==", + "license": "ISC" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.6.tgz", + "integrity": "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "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/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.38.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.38.0.tgz", + "integrity": "sha512-FsZlrYK6bPDGoLeZRuvx2v6qrM03I0U0SnfCLPs/XCCPCFD80xU9Pg09H/K+XFa68uJuZo7l/Xhs+eDRg2l3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.38.0", + "@typescript-eslint/parser": "8.38.0", + "@typescript-eslint/typescript-estree": "8.38.0", + "@typescript-eslint/utils": "8.38.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", + "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "dev": true, + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-hot-client": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-2.1.0.tgz", + "integrity": "sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.9.tgz", + "integrity": "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.3", + "debug": "^4.3.7", + "error-stack-parser-es": "^0.1.5", + "fs-extra": "^11.2.0", + "open": "^10.1.0", + "perfect-debounce": "^1.0.0", + "picocolors": "^1.1.1", + "sirv": "^3.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-pwa": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.3.0.tgz", + "integrity": "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.4.1", + "workbox-window": "^7.4.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "workbox-build": "^7.4.1", + "workbox-window": "^7.4.1" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } + } + }, + "node_modules/vite-plugin-vue-devtools": { + "version": "7.7.7", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.7.7.tgz", + "integrity": "sha512-d0fIh3wRcgSlr4Vz7bAk4va1MkdqhQgj9ANE/rBhsAjOnRfTLs2ocjFMvSUOsv6SRRXU9G+VM7yMgqDb6yI4iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-core": "^7.7.7", + "@vue/devtools-kit": "^7.7.7", + "@vue/devtools-shared": "^7.7.7", + "execa": "^9.5.2", + "sirv": "^3.0.1", + "vite-plugin-inspect": "0.8.9", + "vite-plugin-vue-inspector": "^5.3.1" + }, + "engines": { + "node": ">=v14.21.3" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.2.tgz", + "integrity": "sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.18", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.18.tgz", + "integrity": "sha512-7W4Y4ZbMiQ3SEo+m9lnoNpV9xG7QVMLa+/0RFwwiAVkeYoyGXqWE85jabU4pllJNUzqfLShJ5YLptewhCWUgNA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.18", + "@vue/compiler-sfc": "3.5.18", + "@vue/runtime-dom": "3.5.18", + "@vue/server-renderer": "3.5.18", + "@vue/shared": "3.5.18" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-advanced-cropper": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/vue-advanced-cropper/-/vue-advanced-cropper-2.8.9.tgz", + "integrity": "sha512-1jc5gO674kVGpJKekoaol6ZlwaF5VYDLSBwBOUpViW0IOrrRsyLw6XNszjEqgbavvqinlKNS6Kqlom3B5M72Tw==", + "license": "MIT", + "dependencies": { + "classnames": "^2.2.6", + "debounce": "^1.2.0", + "easy-bem": "^1.0.2" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-component-type-helpers": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.2.12.tgz", + "integrity": "sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-10.2.0.tgz", + "integrity": "sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.6.0", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/vue-eslint-parser/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/vue-eslint-parser/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vue-resize": { + "version": "2.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz", + "integrity": "sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-sonner": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/vue-sonner/-/vue-sonner-1.3.2.tgz", + "integrity": "sha512-UbZ48E9VIya3ToiRHAZUbodKute/z/M1iT8/3fU8zEbwBRE11AKuHikssv18LMk2gTTr6eMQT4qf6JoLHWuj/A==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "2.2.12", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.2.12.tgz", + "integrity": "sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.15", + "@vue/language-core": "2.2.12" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/vue3-apexcharts": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/vue3-apexcharts/-/vue3-apexcharts-1.8.0.tgz", + "integrity": "sha512-5tSD4mXTBbIJ9ir+58qHE6oNtIe0RNgqIRYMKpcsIaxkKtwUww4JhvPkpUFlmiW4OJbbdklgjleXq1lfcM4gdA==", + "license": "MIT", + "peerDependencies": { + "apexcharts": ">=4.0.0", + "vue": ">=3.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/webfontloader": { + "version": "1.6.28", + "resolved": "https://registry.npmjs.org/webfontloader/-/webfontloader-1.6.28.tgz", + "integrity": "sha512-Egb0oFEga6f+nSgasH3E0M405Pzn6y3/9tOVanv/DLfa1YBIgcv90L18YyWnvXkRbIM17v5Kv6IT2N6g1x5tvQ==", + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workbox-background-sync": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz", + "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz", + "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-build": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.1.tgz", + "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-replace": "^6.0.3", + "@rollup/plugin-terser": "^1.0.0", + "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "eta": "^4.5.1", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "pretty-bytes": "^5.3.0", + "rollup": "^4.53.3", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.1", + "workbox-broadcast-update": "7.4.1", + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-google-analytics": "7.4.1", + "workbox-navigation-preload": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-range-requests": "7.4.1", + "workbox-recipes": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1", + "workbox-streams": "7.4.1", + "workbox-sw": "7.4.1", + "workbox-window": "7.4.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/workbox-build/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/workbox-build/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/workbox-build/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/workbox-build/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/workbox-build/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==", + "dev": true, + "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/workbox-build/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "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", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/workbox-build/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz", + "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-core": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz", + "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.1.tgz", + "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz", + "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-background-sync": "7.4.1", + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz", + "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-precaching": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz", + "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz", + "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-recipes": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.1.tgz", + "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-routing": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz", + "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-strategies": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz", + "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-streams": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.1.tgz", + "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1" + } + }, + "node_modules/workbox-sw": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.1.tgz", + "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.1.tgz", + "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.1" + } + }, + "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==", + "dev": true, + "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/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==", + "dev": true, + "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/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "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/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.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==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/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/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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..fc8782f --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,104 @@ +{ + "name": "frontend", + "version": "0.0.0", + "private": true, + "license": "Apache-2.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "build:dev": "run-p type-check \"build-only:dev {@}\" --", + "prebuild:dev": "tsx scripts/generate-dev-env.ts", + "build-only:dev": "vite build --mode dev", + "build:prod": "run-p type-check \"build-only:prod {@}\" --", + "prebuild:prod": "tsx scripts/generate-prod-env.ts", + "build-only:prod": "vite build --mode prod", + "preview": "vite preview", + "test:unit": "vitest", + "build-only": "vite build", + "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "format": "prettier --write src/", + "build:webclient": "npm run build:webclient:prod", + "build:webclient:dev": "tsx scripts/build-webclient-dev.ts", + "build:webclient:prod": "tsx scripts/build-webclient-prod.ts", + "build:widget": "tsx scripts/build-widget.ts", + "build:widget:dev": "tsx scripts/build-widget-dev.ts", + "build:widget:prod": "tsx scripts/build-widget-prod.ts", + "build:widgetdocker": "tsx scripts/build-widget-docker.ts" + }, + "dependencies": { + "@fortawesome/fontawesome-free": "7.0.0", + "@fortawesome/fontawesome-svg-core": "7.0.0", + "@fortawesome/free-brands-svg-icons": "7.0.0", + "@fortawesome/free-regular-svg-icons": "7.0.0", + "@fortawesome/free-solid-svg-icons": "7.0.0", + "@fortawesome/vue-fontawesome": "3.1.1", + "@headlessui/vue": "^1.7.23", + "@heroicons/vue": "^2.2.0", + "@primevue/themes": "^4.2.5", + "@shopify/app-bridge": "3.7.10", + "@shopify/app-bridge-utils": "3.5.1", + "@vue-flow/background": "1.3.2", + "@vue-flow/controls": "1.1.2", + "@vue-flow/core": "1.45.0", + "@vue-flow/minimap": "1.5.3", + "apexcharts": "4.4.0", + "axios": "1.20.0", + "chart.js": "4.4.7", + "date-fns": "^4.1.0", + "dompurify": "3.4.15", + "firebase": "^11.2.0", + "floating-vue": "^5.2.2", + "marked": "^15.0.6", + "mitt": "^3.0.1", + "pinia": "^2.3.0", + "primevue": "^4.2.5", + "socket.io-client": "4.8.3", + "timezone-select-js": "^2.0.1", + "vue": "^3.5.13", + "vue-advanced-cropper": "^2.8.9", + "vue-router": "^4.5.0", + "vue-sonner": "^1.3.0", + "vue3-apexcharts": "1.8.0", + "webfontloader": "^1.6.28" + }, + "devDependencies": { + "@playwright/test": "1.63.0", + "@tsconfig/node22": "^22.0.0", + "@types/dotenv": "^8.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "^22.10.2", + "@types/webfontloader": "^1.6.38", + "@vitejs/plugin-vue": "^5.2.1", + "@vitejs/plugin-vue-jsx": "^4.1.1", + "@vitest/eslint-plugin": "1.1.20", + "@vue/eslint-config-prettier": "^10.1.0", + "@vue/eslint-config-typescript": "^14.1.3", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.7.0", + "dotenv": "^16.4.5", + "eslint": "^9.14.0", + "eslint-plugin-playwright": "^2.1.0", + "eslint-plugin-vue": "^9.30.0", + "jsdom": "^25.0.1", + "npm-run-all2": "^7.0.2", + "prettier": "^3.3.3", + "tsx": "^4.7.1", + "typescript": "~5.6.3", + "vite": "6.4.3", + "vite-plugin-pwa": "1.3.0", + "vite-plugin-vue-devtools": "^7.6.8", + "vitest": "3.2.7", + "vue-tsc": "^2.1.10", + "workbox-core": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + }, + "overrides": { + "editorconfig": { + "minimatch": "^9.0.7" + } + } +} diff --git a/frontend/scripts/build-webclient-dev.ts b/frontend/scripts/build-webclient-dev.ts new file mode 100644 index 0000000..3a609a1 --- /dev/null +++ b/frontend/scripts/build-webclient-dev.ts @@ -0,0 +1,76 @@ +import { build } from 'esbuild' +import { resolve, dirname } from 'path' +import { fileURLToPath } from 'url' +import * as dotenv from 'dotenv' +import * as fs from 'fs' +import * as path from 'path' + +// Load development environment variables +dotenv.config({ path: '.env.dev' }) + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +async function buildWebClientDev() { + try { + console.log('Building webclient with development configuration...') + + // For development, we use the existing config.js which is already set for development + // No need to copy files like in production build + console.log('Using existing config.js for development build') + + // Set NODE_ENV to development for the build + process.env.NODE_ENV = 'development' + + // Baked DEFAULT base URL only; getBaseUrl() still resolves + // window.replypierBaseUrl / window.APP_CONFIG at runtime, so no rebuild is + // needed when a deployment's env changes. + const apiUrl = process.env.VITE_API_URL || 'http://localhost:8000/api/v1' + console.log('Using API URL:', apiUrl) + + const sourceFile = resolve(dirname(__dirname), 'src/webclient/replypier.js') + + await build({ + entryPoints: [sourceFile], + bundle: true, + minify: false, // Don't minify for development + outfile: resolve(dirname(__dirname), 'public/webclient/replypier.min.js'), + format: 'iife', + target: ['es2015'], + loader: { + '.svg': 'text', + }, + define: { + 'process.env.NODE_ENV': '"development"', + '__REPLYPIER_API_URL__': JSON.stringify(apiUrl), + }, + sourcemap: true, // Add sourcemap for development + }) + + // Copy output to dist/webclient directory + const publicWebclientPath = resolve(dirname(__dirname), 'public/webclient/replypier.min.js') + const publicSourcemapPath = resolve(dirname(__dirname), 'public/webclient/replypier.min.js.map') + const distWebclientDir = resolve(dirname(__dirname), 'dist/webclient') + const distWebclientPath = resolve(distWebclientDir, 'replypier.min.js') + const distSourcemapPath = resolve(distWebclientDir, 'replypier.min.js.map') + + // Ensure dist/webclient directory exists + if (!fs.existsSync(distWebclientDir)) { + fs.mkdirSync(distWebclientDir, { recursive: true }) + } + + // Copy the built file and sourcemap to dist/webclient + fs.copyFileSync(publicWebclientPath, distWebclientPath) + if (fs.existsSync(publicSourcemapPath)) { + fs.copyFileSync(publicSourcemapPath, distSourcemapPath) + } + console.log('Development web client copied to dist/webclient/') + + console.log('Development web client built successfully!') + } catch (error) { + console.error('Development build failed:', error) + process.exit(1) + } +} + +buildWebClientDev() diff --git a/frontend/scripts/build-webclient-prod.ts b/frontend/scripts/build-webclient-prod.ts new file mode 100644 index 0000000..0b0ad04 --- /dev/null +++ b/frontend/scripts/build-webclient-prod.ts @@ -0,0 +1,69 @@ +import { build } from 'esbuild' +import { resolve, dirname } from 'path' +import { fileURLToPath } from 'url' +import * as dotenv from 'dotenv' +import * as fs from 'fs' +import * as path from 'path' + +// Load production environment variables +dotenv.config({ path: '.env.prod' }) + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) + +async function buildWebClientProd() { + try { + console.log('Building webclient with production configuration...') + + // Config is now generated by the prebuild step (generate-prod-env.ts) + // No need to copy config files here + + // Set NODE_ENV to production for the build + process.env.NODE_ENV = 'production' + + // Baked DEFAULT base URL. This only sets the fallback; getBaseUrl() still + // resolves window.replypierBaseUrl / window.APP_CONFIG at runtime, so an + // env change on a deployment needs no rebuild of this artifact. + const apiUrl = process.env.VITE_API_URL || 'https://api.replypier.site/api/v1' + console.log('Using API URL:', apiUrl) + + const sourceFile = resolve(dirname(__dirname), 'src/webclient/replypier.js') + + await build({ + entryPoints: [sourceFile], + bundle: true, + minify: true, + outfile: resolve(dirname(__dirname), 'public/webclient/replypier.min.js'), + format: 'iife', + target: ['es2015'], + loader: { + '.svg': 'text', + }, + define: { + 'process.env.NODE_ENV': '"production"', + '__REPLYPIER_API_URL__': JSON.stringify(apiUrl), + }, + }) + + // Copy output to dist/webclient directory + const publicWebclientPath = resolve(dirname(__dirname), 'public/webclient/replypier.min.js') + const distWebclientDir = resolve(dirname(__dirname), 'dist/webclient') + const distWebclientPath = resolve(distWebclientDir, 'replypier.min.js') + + // Ensure dist/webclient directory exists + if (!fs.existsSync(distWebclientDir)) { + fs.mkdirSync(distWebclientDir, { recursive: true }) + } + + // Copy the built file to dist/webclient + fs.copyFileSync(publicWebclientPath, distWebclientPath) + console.log('Production web client copied to dist/webclient/') + + console.log('Production web client built successfully!') + } catch (error) { + console.error('Production build failed:', error) + process.exit(1) + } +} + +buildWebClientProd() diff --git a/frontend/scripts/build-widget-dev.ts b/frontend/scripts/build-widget-dev.ts new file mode 100644 index 0000000..c48c6f6 --- /dev/null +++ b/frontend/scripts/build-widget-dev.ts @@ -0,0 +1,67 @@ +import { build } from 'vite' +import { resolve } from 'path' +import * as fs from 'fs' +import * as path from 'path' +import * as dotenv from 'dotenv' + +// Load development environment variables +dotenv.config({ path: '.env.dev' }) + +const config = resolve(process.cwd(), 'vite.widget.config.ts') +const assetsDir = resolve(process.cwd(), '../backend/assets') +const filesToKeep = ['widget.js', 'widget.css'] + +async function buildWidgetDev() { + try { + console.log('Building widget with development configuration...') + + // Set NODE_ENV to development for the build + process.env.NODE_ENV = 'development' + + // Run the Vite build + await build({ configFile: config }) + console.log('Vite build completed') + + // Copy CSS file from unused directory to the root assets directory + const unusedDir = path.join(assetsDir, 'unused') + if (fs.existsSync(unusedDir)) { + const files = fs.readdirSync(unusedDir) + const cssFile = files.find(file => file.endsWith('.css')) + + if (cssFile) { + const sourcePath = path.join(unusedDir, cssFile) + const destPath = path.join(assetsDir, 'widget.css') + + fs.copyFileSync(sourcePath, destPath) + console.log(`Copied CSS file to ${destPath}`) + } + + // Clean up unused directory + fs.rmSync(unusedDir, { recursive: true, force: true }) + console.log('Cleaned up unused directory') + } + + // Clean up the assets directory - keep only widget.js and widget.css + const entries = fs.readdirSync(assetsDir) + + for (const entry of entries) { + if (!filesToKeep.includes(entry)) { + const entryPath = path.join(assetsDir, entry) + + if (fs.statSync(entryPath).isDirectory()) { + fs.rmSync(entryPath, { recursive: true, force: true }) + } else { + fs.unlinkSync(entryPath) + } + } + } + + console.log(`Cleaned up assets directory, keeping only: ${filesToKeep.join(', ')}`) + console.log('Development widget built successfully!') + } catch (error) { + console.error('Development build failed:', error) + process.exit(1) + } +} + +buildWidgetDev() diff --git a/frontend/scripts/build-widget-docker.ts b/frontend/scripts/build-widget-docker.ts new file mode 100644 index 0000000..7edf172 --- /dev/null +++ b/frontend/scripts/build-widget-docker.ts @@ -0,0 +1,67 @@ +import { build } from 'vite' +import { resolve } from 'path' +import * as fs from 'fs' +import * as path from 'path' + +const config = resolve(process.cwd(), 'vite.widget.docker.config.ts') +// For Docker, we need to copy to a shared volume or mounted directory +// This assumes the backend assets are mounted/shared between containers +const assetsDir = resolve(process.cwd(), '/app/backend-assets') +const filesToKeep = ['widget.js', 'widget.css'] + +async function buildWidgetDocker() { + try { + // Ensure the backend assets directory exists + if (!fs.existsSync(assetsDir)) { + fs.mkdirSync(assetsDir, { recursive: true }) + console.log(`Created backend assets directory: ${assetsDir}`) + } + + // Run the Vite build + await build({ configFile: config }) + console.log('Vite build completed') + + // Copy CSS file from unused directory to the root assets directory + const unusedDir = path.join(assetsDir, 'unused') + if (fs.existsSync(unusedDir)) { + const files = fs.readdirSync(unusedDir) + const cssFile = files.find(file => file.endsWith('.css')) + + if (cssFile) { + const sourcePath = path.join(unusedDir, cssFile) + const destPath = path.join(assetsDir, 'widget.css') + + fs.copyFileSync(sourcePath, destPath) + console.log(`Copied CSS file to ${destPath}`) + } + + // Clean up unused directory + fs.rmSync(unusedDir, { recursive: true, force: true }) + console.log('Cleaned up unused directory') + } + + // Clean up the assets directory - keep only widget.js and widget.css + const entries = fs.readdirSync(assetsDir) + + for (const entry of entries) { + if (!filesToKeep.includes(entry)) { + const entryPath = path.join(assetsDir, entry) + + if (fs.statSync(entryPath).isDirectory()) { + fs.rmSync(entryPath, { recursive: true, force: true }) + } else { + fs.unlinkSync(entryPath) + } + } + } + + console.log(`Cleaned up assets directory, keeping only: ${filesToKeep.join(', ')}`) + console.log('Widget built successfully for Docker!') + console.log(`Files available at: ${assetsDir}`) + } catch (error) { + console.error('Docker widget build failed:', error) + process.exit(1) + } +} + +buildWidgetDocker() \ No newline at end of file diff --git a/frontend/scripts/build-widget-prod.ts b/frontend/scripts/build-widget-prod.ts new file mode 100644 index 0000000..f679239 --- /dev/null +++ b/frontend/scripts/build-widget-prod.ts @@ -0,0 +1,67 @@ +import { build } from 'vite' +import { resolve } from 'path' +import * as fs from 'fs' +import * as path from 'path' +import dotenv from 'dotenv' + +// Load production environment variables +dotenv.config({ path: '.env.prod' }) + +const config = resolve(process.cwd(), 'vite.widget.config.ts') +const assetsDir = resolve(process.cwd(), '../backend/assets') +const filesToKeep = ['widget.js', 'widget.css'] + +async function buildWidgetProd() { + try { + console.log('Building widget with production configuration...') + + // Set NODE_ENV to production for the build + process.env.NODE_ENV = 'production' + + // Run the Vite build + await build({ configFile: config }) + console.log('Vite build completed') + + // Copy CSS file from unused directory to the root assets directory + const unusedDir = path.join(assetsDir, 'unused') + if (fs.existsSync(unusedDir)) { + const files = fs.readdirSync(unusedDir) + const cssFile = files.find(file => file.endsWith('.css')) + + if (cssFile) { + const sourcePath = path.join(unusedDir, cssFile) + const destPath = path.join(assetsDir, 'widget.css') + + fs.copyFileSync(sourcePath, destPath) + console.log(`Copied CSS file to ${destPath}`) + } + + // Clean up unused directory + fs.rmSync(unusedDir, { recursive: true, force: true }) + console.log('Cleaned up unused directory') + } + + // Clean up the assets directory - keep only widget.js and widget.css + const entries = fs.readdirSync(assetsDir) + + for (const entry of entries) { + if (!filesToKeep.includes(entry)) { + const entryPath = path.join(assetsDir, entry) + + if (fs.statSync(entryPath).isDirectory()) { + fs.rmSync(entryPath, { recursive: true, force: true }) + } else { + fs.unlinkSync(entryPath) + } + } + } + + console.log(`Cleaned up assets directory, keeping only: ${filesToKeep.join(', ')}`) + console.log('Production widget built successfully!') + } catch (error) { + console.error('Production build failed:', error) + process.exit(1) + } +} + +buildWidgetProd() diff --git a/frontend/scripts/build-widget.ts b/frontend/scripts/build-widget.ts new file mode 100644 index 0000000..6cab28d --- /dev/null +++ b/frontend/scripts/build-widget.ts @@ -0,0 +1,63 @@ +import { build } from 'vite' +import { resolve } from 'path' +import * as fs from 'fs' +import * as path from 'path' +import dotenv from 'dotenv' + +// Load the local .env so a plain `build:widget` bakes the local API URL +// (VITE_API_URL) instead of falling through to the widget-env default. +dotenv.config({ path: '.env' }) + +const config = resolve(process.cwd(), 'vite.widget.config.ts') +const assetsDir = resolve(process.cwd(), '../backend/assets') +const filesToKeep = ['widget.js', 'widget.css'] + +async function buildWidget() { + try { + // Run the Vite build + await build({ configFile: config }) + console.log('Vite build completed') + + // Copy CSS file from unused directory to the root assets directory + const unusedDir = path.join(assetsDir, 'unused') + if (fs.existsSync(unusedDir)) { + const files = fs.readdirSync(unusedDir) + const cssFile = files.find(file => file.endsWith('.css')) + + if (cssFile) { + const sourcePath = path.join(unusedDir, cssFile) + const destPath = path.join(assetsDir, 'widget.css') + + fs.copyFileSync(sourcePath, destPath) + console.log(`Copied CSS file to ${destPath}`) + } + + // Clean up unused directory + fs.rmSync(unusedDir, { recursive: true, force: true }) + console.log('Cleaned up unused directory') + } + + // Clean up the assets directory - keep only widget.js and widget.css + const entries = fs.readdirSync(assetsDir) + + for (const entry of entries) { + if (!filesToKeep.includes(entry)) { + const entryPath = path.join(assetsDir, entry) + + if (fs.statSync(entryPath).isDirectory()) { + fs.rmSync(entryPath, { recursive: true, force: true }) + } else { + fs.unlinkSync(entryPath) + } + } + } + + console.log(`Cleaned up assets directory, keeping only: ${filesToKeep.join(', ')}`) + console.log('Widget built successfully!') + } catch (error) { + console.error('Build failed:', error) + process.exit(1) + } +} + +buildWidget() diff --git a/frontend/scripts/generate-dev-env.ts b/frontend/scripts/generate-dev-env.ts new file mode 100644 index 0000000..b2bd6de --- /dev/null +++ b/frontend/scripts/generate-dev-env.ts @@ -0,0 +1,56 @@ +import * as fs from 'fs' +import * as path from 'path' +import * as dotenv from 'dotenv' + +// Load development environment variables +dotenv.config({ path: '.env.dev' }) + +// (swenv.js is gone — the app service worker src/sw.ts inlines its Firebase +// config at build time via import.meta.env) + +// Create config.js content for development using environment variables +const configContent = `// Runtime configuration - this file gets replaced at container startup +// Assigned to self, not window, so the service worker can importScripts() +// it too; in a page self === window, so window.APP_CONFIG still works. +self.APP_CONFIG = { + // API URLs + API_URL: "${process.env.VITE_API_URL || 'http://localhost:8000/api/v1'}", + WS_URL: "${process.env.VITE_WS_URL || 'ws://localhost:8000'}", + WIDGET_URL: "${process.env.VITE_WIDGET_URL || 'http://localhost:8000'}", + + // Firebase Configuration + FIREBASE_API_KEY: "${process.env.VITE_FIREBASE_API_KEY || 'your_firebase_api_key_here'}", + FIREBASE_AUTH_DOMAIN: "${process.env.VITE_FIREBASE_AUTH_DOMAIN || 'your_firebase_auth_domain_here'}", + FIREBASE_PROJECT_ID: "${process.env.VITE_FIREBASE_PROJECT_ID || 'your_firebase_project_id_here'}", + FIREBASE_MESSAGING_SENDER_ID: "${process.env.VITE_FIREBASE_MESSAGING_SENDER_ID || '680125690355'}", + FIREBASE_APP_ID: "${process.env.VITE_FIREBASE_APP_ID || 'your_firebase_app_id_here'}", + FIREBASE_STORAGE_BUCKET: "${process.env.VITE_FIREBASE_STORAGE_BUCKET || 'your_firebase_storage_bucket_here'}", + FIREBASE_MEASUREMENT_ID: "${process.env.VITE_FIREBASE_MEASUREMENT_ID || 'your_firebase_measurement_id_here'}", + FIREBASE_VAPID_KEY: "${process.env.VITE_FIREBASE_VAPID_KEY || 'your_firebase_vapid_key_here'}", + + // Google Fonts API + GOOGLE_FONTS_API_KEY: "${process.env.VITE_GOOGLE_FONTS_API_KEY || 'your_google_fonts_api_key_here'}", + + // Demo Widget ID + DEMO_WIDGET_ID: "${process.env.VITE_DEMO_WIDGET_ID || 'your_demo_widget_id_here'}", + + // Node Environment + NODE_ENV: "development", + HOST: "${process.env.VITE_HOST || '0.0.0.0'}" +}; +` + +// Write config to both public and dist directories +const publicConfigPath = './public/config.js' +const distConfigPath = './dist/config.js' + +// Write to public directory +fs.writeFileSync(publicConfigPath, configContent) +console.log('Development config file generated to public/config.js') + +// Ensure dist directory exists and write to dist directory +if (!fs.existsSync('./dist')) { + fs.mkdirSync('./dist', { recursive: true }) +} +fs.writeFileSync(distConfigPath, configContent) +console.log('Development config file generated to dist/config.js') diff --git a/frontend/scripts/generate-prod-env.ts b/frontend/scripts/generate-prod-env.ts new file mode 100644 index 0000000..807b0e1 --- /dev/null +++ b/frontend/scripts/generate-prod-env.ts @@ -0,0 +1,57 @@ +import * as fs from 'fs' +import * as path from 'path' +import * as dotenv from 'dotenv' + +// Load production environment variables +dotenv.config({ path: '.env.prod' }) + +// (swenv.js is gone — the app service worker src/sw.ts inlines its Firebase +// config at build time via import.meta.env) + +// Create config.js content for production using environment variables +const configContent = `// Runtime configuration - this file gets replaced at container startup +// Assigned to self, not window, so the service worker can importScripts() +// it too; in a page self === window, so window.APP_CONFIG still works. +self.APP_CONFIG = { + // API URLs + API_URL: "${process.env.VITE_API_URL || 'https://api.replypier.site/api/v1'}", + WS_URL: "${process.env.VITE_WS_URL || 'wss://api.replypier.site'}", + WIDGET_URL: "${process.env.VITE_WIDGET_URL || 'https://api.replypier.site'}", + + // Firebase Configuration + FIREBASE_API_KEY: "${process.env.VITE_FIREBASE_API_KEY || ''}", + FIREBASE_AUTH_DOMAIN: "${process.env.VITE_FIREBASE_AUTH_DOMAIN || ''}", + FIREBASE_PROJECT_ID: "${process.env.VITE_FIREBASE_PROJECT_ID || ''}", + FIREBASE_MESSAGING_SENDER_ID: "${process.env.VITE_FIREBASE_MESSAGING_SENDER_ID || ''}", + FIREBASE_APP_ID: "${process.env.VITE_FIREBASE_APP_ID || ''}", + FIREBASE_STORAGE_BUCKET: "${process.env.VITE_FIREBASE_STORAGE_BUCKET || ''}", + FIREBASE_MEASUREMENT_ID: "${process.env.VITE_FIREBASE_MEASUREMENT_ID || ''}", + FIREBASE_VAPID_KEY: "${process.env.VITE_FIREBASE_VAPID_KEY || ''}", + + // Google Fonts API + GOOGLE_FONTS_API_KEY: "${process.env.VITE_GOOGLE_FONTS_API_KEY || ''}", + + // Demo Widget ID + DEMO_WIDGET_ID: "${process.env.VITE_DEMO_WIDGET_ID || ''}", + + // Node Environment + NODE_ENV: "production", + HOST: "${process.env.VITE_HOST || '0.0.0.0'}", + VITE_SHOPIFY_API_KEY: "${process.env.VITE_SHOPIFY_API_KEY || ''}", +}; +` + +// Write config to both public and dist directories +const publicConfigPath = './public/config.js' +const distConfigPath = './dist/config.js' + +// Write to public directory +fs.writeFileSync(publicConfigPath, configContent) +console.log('Production config file generated to public/config.js') + +// Ensure dist directory exists and write to dist directory +if (!fs.existsSync('./dist')) { + fs.mkdirSync('./dist', { recursive: true }) +} +fs.writeFileSync(distConfigPath, configContent) +console.log('Production config file generated to dist/config.js') diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..f4db90f --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,13 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*", "src/modules/enterprise/e2e/**"], + "compilerOptions": { + "incremental": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..100cf6a --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.vitest.json" + } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..3741499 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node22/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*" + ], + "compilerOptions": { + "noEmit": true, + "incremental": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/frontend/tsconfig.vitest.json b/frontend/tsconfig.vitest.json new file mode 100644 index 0000000..7d1d8ce --- /dev/null +++ b/frontend/tsconfig.vitest.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.app.json", + "include": ["src/**/__tests__/*", "env.d.ts"], + "exclude": [], + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", + + "lib": [], + "types": ["node", "jsdom"] + } +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..948edb3 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,153 @@ +import { fileURLToPath, URL } from 'node:url' +import * as fs from 'fs' +import * as path from 'path' +import * as dotenv from 'dotenv' + +import { defineConfig, loadEnv } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' +import vueDevTools from 'vite-plugin-vue-devtools' +import { VitePWA } from 'vite-plugin-pwa' +import { THEME_COLORS } from './src/config/themeColors' + +// https://vite.dev/config/ +export default defineConfig(({ command, mode }) => { + // Load env file based on `mode` in the current working directory. + // When mode is 'prod' or 'dev', load .env.prod or .env.dev respectively + let envDir = process.cwd() + let envFile = '.env' + + if (mode === 'prod' || mode === 'production') { + // Manually load .env.prod for production builds. Plain `vite build` + // (mode 'production', used by Dockerfile.frontend.prod) must see the + // VITE_FIREBASE_* values too: src/sw.ts inlines them at build time, and + // without them the service worker ships with push silently disabled. + const envPath = path.resolve(envDir, '.env.prod') + if (fs.existsSync(envPath)) { + const envConfig = dotenv.parse(fs.readFileSync(envPath)) + // Merge with process.env + for (const key in envConfig) { + if (!process.env[key]) { + process.env[key] = envConfig[key] + } + } + } + } else if (mode === 'dev') { + // Manually load .env.dev for development builds + const envPath = path.resolve(envDir, '.env.dev') + if (fs.existsSync(envPath)) { + const envConfig = dotenv.parse(fs.readFileSync(envPath)) + // Merge with process.env + for (const key in envConfig) { + if (!process.env[key]) { + process.env[key] = envConfig[key] + } + } + } + } + + // Also load standard Vite env files + const env = loadEnv(mode, process.cwd(), '') + + if (command === 'build' && !(process.env.VITE_FIREBASE_API_KEY || env.VITE_FIREBASE_API_KEY)) { + console.warn( + '\n[pwa] VITE_FIREBASE_API_KEY is not set for this build — the service worker ' + + 'will ship WITHOUT background push support (foreground notifications still work ' + + 'via runtime config.js). Provide .env/.env.prod with VITE_FIREBASE_* to enable it.\n' + ) + } + + return { + plugins: [ + vue({ + template: { + compilerOptions: { + // Treat all tags starting with 's-' as custom elements (Polaris web components) + isCustomElement: (tag) => tag.startsWith('s-') + } + } + }), + vueJsx(), + vueDevTools(), + // Plugin to inject environment variables into HTML + { + name: 'html-transform', + transformIndexHtml(html) { + return html.replace( + /<%= VITE_SHOPIFY_API_KEY %>/g, + process.env.VITE_SHOPIFY_API_KEY || env.VITE_SHOPIFY_API_KEY || '' + ) + } + }, + VitePWA({ + strategies: 'injectManifest', + srcDir: 'src', + filename: 'sw.ts', + // Registration is manual (src/pwa/register.ts) so the Shopify App Bridge + // script stays the first script tag and embedded contexts never register. + injectRegister: false, + manifest: { + name: 'ReplyPier', + short_name: 'ReplyPier', + description: 'Agent console for ReplyPier — handle customer chats anywhere.', + start_url: '/', + scope: '/', + display: 'standalone', + background_color: THEME_COLORS.dark, + theme_color: THEME_COLORS.dark, + icons: [ + { src: '/pwa-192x192.png', sizes: '192x192', type: 'image/png' }, + { src: '/pwa-512x512.png', sizes: '512x512', type: 'image/png' }, + { src: '/pwa-maskable-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, + ], + }, + injectManifest: { + maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, + // config.js is runtime-substituted per environment (docker-entrypoint) + // and must never be precached; webclient isn't the agent app. + globIgnores: ['config.js', 'config.dev.js', 'webclient/**'], + }, + devOptions: { + enabled: false, + }, + }) + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, + server: { + headers: { + 'Service-Worker-Allowed': '/', + }, + // Allow ngrok and other tunnel services for Shopify embedded app development + allowedHosts: [ + 'localhost', + '.ngrok.app', + '.ngrok.io', + 'frontendchat.ngrok.app', + '.replypier.site', + ], + }, + publicDir: 'public', + build: { + outDir: 'dist', + assetsDir: 'assets', + chunkSizeWarningLimit: 600, + rollupOptions: { + input: { + main: './index.html', + }, + output: { + manualChunks: { + 'vendor-vue': ['vue', 'vue-router', 'pinia'], + 'vendor-ui': ['vue3-apexcharts'], + 'vendor-utils': ['axios', 'marked'], + 'vendor-firebase': ['firebase/app', 'firebase/messaging', 'firebase/auth'], + }, + }, + }, + }, + } +}) diff --git a/frontend/vite.widget.config.ts b/frontend/vite.widget.config.ts new file mode 100644 index 0000000..cdf12a1 --- /dev/null +++ b/frontend/vite.widget.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [ + vue({ + template: { + compilerOptions: { + // Treat all tags starting with 's-' as custom elements (Polaris web components) + isCustomElement: (tag) => tag.startsWith('s-') + } + } + }) + ], + build: { + lib: { + entry: resolve(__dirname, 'src/webclient/widget.ts'), + name: 'ReplyPierWidget', + fileName: 'widget', + formats: ['es'], + }, + outDir: '../backend/assets', + emptyOutDir: false, + rollupOptions: { + output: { + format: 'es', + entryFileNames: 'widget.js', + assetFileNames: (assetInfo) => { + if (assetInfo.name === 'style.css') { + return 'widget.css' + } + // Place other assets in a subdirectory that we can ignore or clean up later + return 'unused/[name]-[hash][extname]' + }, + chunkFileNames: 'unused/[name]-[hash].js', + manualChunks: undefined, // Disable code splitting + }, + }, + }, + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), + }, +}) diff --git a/frontend/vite.widget.docker.config.ts b/frontend/vite.widget.docker.config.ts new file mode 100644 index 0000000..ceeb36e --- /dev/null +++ b/frontend/vite.widget.docker.config.ts @@ -0,0 +1,44 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' + +export default defineConfig({ + plugins: [ + vue({ + template: { + compilerOptions: { + // Treat all tags starting with 's-' as custom elements (Polaris web components) + isCustomElement: (tag) => tag.startsWith('s-') + } + } + }) + ], + build: { + lib: { + entry: resolve(__dirname, 'src/webclient/widget.ts'), + name: 'ReplyPierWidget', + fileName: 'widget', + formats: ['es'], + }, + outDir: '/app/backend-assets', + emptyOutDir: false, + rollupOptions: { + output: { + format: 'es', + entryFileNames: 'widget.js', + assetFileNames: (assetInfo) => { + if (assetInfo.name === 'style.css') { + return 'widget.css' + } + // Place other assets in a subdirectory that we can ignore or clean up later + return 'unused/[name]-[hash][extname]' + }, + chunkFileNames: 'unused/[name]-[hash].js', + manualChunks: undefined, // Disable code splitting + }, + }, + }, + define: { + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'production'), + }, +}) \ No newline at end of file diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..c3ee2ff --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig, configDefaults } from 'vitest/config' +import vue from '@vitejs/plugin-vue' +import vueJsx from '@vitejs/plugin-vue-jsx' + +export default defineConfig({ + plugins: [vue(), vueJsx()], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + // vite-plugin-pwa virtual module doesn't exist under vitest + 'virtual:pwa-register': fileURLToPath( + new URL('./src/__tests__/stubs/pwa-register.ts', import.meta.url) + ), + }, + }, + test: { + environment: 'jsdom', + // Playwright specs (the enterprise suite lives under its module), not vitest. + exclude: [...configDefaults.exclude, 'e2e/**', 'src/modules/enterprise/e2e/**'], + root: fileURLToPath(new URL('./', import.meta.url)), + }, +})