From b4b0ad0a53ae8d1b85be013690eca85746e7bb6e Mon Sep 17 00:00:00 2001 From: VPS 4 Builder Date: Mon, 18 May 2026 20:57:05 +0200 Subject: [PATCH] feat: restaura o projeto original do clube67.com (Next.js + Express API + MySQL + Redis) livre de gambiarras e dockerizado --- .gitignore | 37 +- .htaccess | 1 + .user.ini | 1 + 404.html | 74 + 502.html | 74 + .../plugins/UnifiedStorageProvider/index.js | 214 - backend/check_column.ts | 30 - backend/check_column_final.ts | 23 - backend/check_ruibto.ts | 20 - backend/fix_ruibto.ts | 22 - backend/knexfile.ts | 24 - backend/package-lock.json | 8816 ++++++----------- backend/package.json | 105 +- backend/plugins/whatsapp-v1/.env.example | 3 - backend/plugins/whatsapp-v1/.gitignore | 6 - backend/plugins/whatsapp-v1/README.md | 115 - backend/plugins/whatsapp-v1/package-lock.json | 2922 ------ backend/plugins/whatsapp-v1/package.json | 28 - backend/plugins/whatsapp-v1/public/app.js | 628 -- backend/plugins/whatsapp-v1/public/index.html | 169 - backend/plugins/whatsapp-v1/public/styles.css | 499 - backend/plugins/whatsapp-v1/src/config.ts | 18 - .../src/handlers/superAdminMenu.ts | 267 - backend/plugins/whatsapp-v1/src/index.ts | 42 - .../whatsapp-v1/src/routes/instances.ts | 170 - .../whatsapp-v1/src/routes/messages.ts | 291 - .../whatsapp-v1/src/services/whatsapp.ts | 198 - .../plugins/whatsapp-v1/src/types/whatsapp.ts | 90 - .../plugins/whatsapp-v1/src/utils/helpers.ts | 25 - backend/plugins/whatsapp-v1/tsconfig.json | 20 - backend/schema.sql | 282 + backend/scripts/inspect_benefits.ts | 18 - backend/scripts/test_update.ts | 26 - backend/seed.sql | 36 + backend/seed_admin.js | 67 - backend/src/config/database.ts | 31 +- backend/src/config/index.ts | 42 +- backend/src/config/knexfile.ts | 50 +- backend/src/config/logger.ts | 3 - backend/src/config/redis.ts | 20 +- backend/src/controllers/admin.controller.ts | 122 + backend/src/controllers/auth.controller.ts | 298 +- backend/src/controllers/benefit.controller.ts | 338 +- backend/src/controllers/lead.controller.ts | 90 - backend/src/controllers/partner.controller.ts | 294 +- .../src/controllers/referral.controller.ts | 146 - backend/src/controllers/user.controller.ts | 262 +- backend/src/controllers/webhook.controller.ts | 112 + backend/src/core/StorageProvider.ts | 82 - backend/src/core/hooks.ts | 69 - backend/src/core/plugin-config.ts | 44 - backend/src/core/plugin-loader.ts | 719 -- backend/src/core/plugin-registry.ts | 120 - backend/src/core/types.js | 6 - backend/src/core/types.ts | 120 - .../migrations/20260215000000_init_schema.ts | 120 - .../20260216151047_add_deleted_at_to_users.ts | 15 - .../20260216163300_update_user_status_enum.ts | 17 - ...04800_add_hide_partner_name_to_benefits.ts | 15 - .../20260217145800_add_partners_columns.ts | 17 - ...0260217210000_add_partner_logo_bg_color.ts | 13 - ...0260217220000_add_benefit_public_fields.ts | 17 - .../20260218120000_create_referrals.ts | 25 - backend/src/index.ts | 163 +- backend/src/infra/cache/dragonfly.ts | 34 - backend/src/middleware/auth.ts | 76 +- backend/src/middleware/rbac.ts | 47 +- backend/src/middleware/security.ts | 86 +- backend/src/middleware/storage-health.ts | 18 - backend/src/middleware/upload.ts | 33 +- .../migrations/20260210_001_initial_schema.ts | 277 + backend/src/modules/whatsapp/engine/index.js | 80 - .../src/modules/whatsapp/engine/index.js.map | 1 - backend/src/modules/whatsapp/engine/index.ts | 60 - .../src/modules/whatsapp/engine/infinite.js | 18 - .../modules/whatsapp/engine/infinite.js.map | 1 - .../src/modules/whatsapp/engine/infinite.ts | 1 - .../src/modules/whatsapp/engine/official.js | 18 - .../modules/whatsapp/engine/official.js.map | 1 - .../src/modules/whatsapp/engine/official.ts | 1 - backend/src/modules/whatsapp/rich-message.ts | 411 - backend/src/routes/admin.routes.ts | 675 +- backend/src/routes/auth.routes.ts | 9 +- backend/src/routes/benefit.routes.ts | 21 +- backend/src/routes/lead.routes.ts | 11 - backend/src/routes/partner.routes.ts | 20 +- backend/src/routes/plugin.routes.ts | 103 +- backend/src/routes/referral.routes.ts | 16 - backend/src/routes/upload.routes.ts | 22 - backend/src/routes/user.routes.ts | 27 +- backend/src/routes/webhook.routes.ts | 17 +- backend/src/seeds/001_initial_data.ts | 183 + backend/src/services/UserLifecycleService.ts | 78 - backend/src/services/plugin.service.ts | 178 +- backend/src/types/express.d.ts | 16 - backend/src/utils/audit.ts | 30 +- backend/src/utils/logger.ts | 49 +- backend/storage/plugin-configs.json | 1 - backend/test_pg_conn.js | 28 - backend/tsconfig.json | 20 +- deploy-prod-builder.sh | 85 +- docker-compose.yml | 82 +- ecosystem.config.js | 29 + frontend/app/admin/page.tsx | 196 + frontend/app/benefits/page.tsx | 138 + frontend/app/dashboard/page.tsx | 127 + frontend/app/globals.css | 72 + frontend/app/layout.tsx | 21 + frontend/app/login/page.tsx | 86 + frontend/app/page.tsx | 164 + frontend/app/partner/page.tsx | 77 + frontend/app/profile/page.tsx | 85 + frontend/app/register/page.tsx | 92 + .../components/layout/DashboardLayout.tsx | 37 + frontend/components/layout/Sidebar.tsx | 89 + frontend/index.html | 330 - frontend/lib/api.ts | 135 + frontend/lib/auth.tsx | 101 + frontend/next-env.d.ts | 5 + frontend/next.config.js | 24 + frontend/package-lock.json | 1904 ++++ frontend/package.json | 32 + frontend/postcss.config.js | 6 + frontend/script.js | 393 - frontend/style.css | 324 - frontend/tailwind.config.js | 68 + frontend/tsconfig.json | 40 + index.html | 55 + nginx/clube67.conf | 114 + nginx/default.conf | 103 +- plugins/SmartVision/backend/Optimizer.js | 60 + plugins/SmartVision/backend/Optimizer.js.map | 1 + plugins/SmartVision/backend/Optimizer.ts | 63 + plugins/SmartVision/index.js | 33 + plugins/SmartVision/index.js.map | 1 + plugins/SmartVision/index.ts | 35 + plugins/SmartVision/manifest.json | 19 + plugins/UnifiedStorageProvider/index.js | 388 + plugins/UnifiedStorageProvider/index.js.map | 1 + plugins/UnifiedStorageProvider/index.ts | 426 + plugins/UnifiedStorageProvider/manifest.json | 22 + plugins/config.js | 43 + .../backend/migrations/001_auth_tables.sql | 47 + plugins/core-auth/backend/routes.js | 148 + plugins/core-auth/backend/routes.js.map | 1 + plugins/core-auth/backend/routes.ts | 180 + plugins/core-auth/index.js | 23 + plugins/core-auth/index.js.map | 1 + plugins/core-auth/index.ts | 23 + plugins/core-auth/manifest.json | 46 + plugins/exemplo-plugin/index.js | 35 + plugins/exemplo-plugin/manifest.json | 13 + plugins/ext-api/backend/apikey-auth.js | 103 + plugins/ext-api/backend/apikey-auth.js.map | 1 + plugins/ext-api/backend/apikey-auth.ts | 133 + .../ext-api/backend/backend/src/config/env.js | 26 + .../backend/backend/src/config/logger.js | 14 + .../backend/src/core/StorageProvider.js | 50 + .../backend/backend/src/core/hook-bus.js | 49 + .../backend/backend/src/core/plugin-config.js | 51 + .../ext-api/backend/backend/src/core/types.js | 2 + .../backend/src/infra/cache/dragonfly.js | 54 + .../backend/src/infra/database/prisma.js | 13 + .../src/modules/chatbot/chatbot.repository.js | 67 + .../src/modules/chatbot/chatbot.service.js | 164 + .../connection/WhatsAppConnectionManager.js | 1113 +++ .../whatsapp/handlers/ContactHandler.js | 494 + .../whatsapp/handlers/MessageHandler.js | 758 ++ .../backend/src/shared/utils/whatsapp.js | 68 + .../backend/plugins/ext-api/backend/routes.js | 1144 +++ .../backend/plugins/secretaria/brain.js | 721 ++ .../backend/plugins/secretaria/tools.js | 160 + plugins/ext-api/backend/routes.js | 1283 +++ plugins/ext-api/backend/routes.js.map | 1 + plugins/ext-api/backend/routes.ts | 1295 +++ plugins/ext-api/backend/webhook-dispatcher.js | 98 + .../ext-api/backend/webhook-dispatcher.js.map | 1 + plugins/ext-api/backend/webhook-dispatcher.ts | 110 + plugins/ext-api/backend/ws-bridge.js | 118 + plugins/ext-api/backend/ws-bridge.js.map | 1 + plugins/ext-api/backend/ws-bridge.ts | 138 + plugins/ext-api/index.js | 36 + plugins/ext-api/index.js.map | 1 + plugins/ext-api/index.ts | 54 + plugins/ext-api/manifest.json | 24 + .../migrations/001_financial_tables.sql | 51 + plugins/financeiro/backend/routes.js | 117 + plugins/financeiro/backend/routes.js.map | 1 + plugins/financeiro/backend/routes.ts | 123 + plugins/financeiro/index.js | 24 + plugins/financeiro/index.js.map | 1 + plugins/financeiro/index.ts | 25 + plugins/financeiro/manifest.json | 66 + plugins/index.js | 140 + .../backend/migrations/001_leads_table.sql | 16 + .../migrations/002_normalize_leads.sql | 11 + .../backend/migrations/003_lead_history.sql | 11 + .../backend/migrations/004_lead_owner.sql | 10 + .../migrations/005_lead_classification.sql | 19 + plugins/leads/backend/routes.js | 243 + plugins/leads/backend/routes.js.map | 1 + plugins/leads/backend/routes.ts | 256 + plugins/leads/index.js | 26 + plugins/leads/index.js.map | 1 + plugins/leads/index.ts | 22 + plugins/leads/manifest.json | 52 + .../migrations/001_create_usage_table.sql | 6 + plugins/nanobana/backend/routes.js | 164 + plugins/nanobana/backend/routes.js.map | 1 + plugins/nanobana/backend/routes.ts | 182 + plugins/nanobana/index.js | 20 + plugins/nanobana/index.js.map | 1 + plugins/nanobana/index.ts | 19 + plugins/nanobana/manifest.json | 21 + plugins/newwhats/auth.js | 42 + plugins/newwhats/context-builder.js | 842 ++ plugins/newwhats/follow-up-detector.js | 176 + plugins/newwhats/index.js | 67 + plugins/newwhats/manifest.json | 44 + plugins/newwhats/media-transcribe.js | 616 ++ plugins/newwhats/personas.js | 102 + plugins/newwhats/proxy.js | 169 + plugins/newwhats/routes.js | 345 + plugins/newwhats/smart-router.js | 339 + plugins/newwhats/sync-knowledge.js | 246 + plugins/newwhats/webhook-receiver.js | 662 ++ plugins/secretaria/PLUGIN_EMPRESA.md | 240 + plugins/secretaria/brain.js | 730 ++ plugins/secretaria/brain.js.map | 1 + plugins/secretaria/brain.ts | 869 ++ plugins/secretaria/index.js | 25 + plugins/secretaria/index.js.map | 1 + plugins/secretaria/index.ts | 28 + plugins/secretaria/manifest.json | 75 + plugins/secretaria/migrate.js | 276 + plugins/secretaria/migrate.js.map | 1 + plugins/secretaria/migrate.ts | 290 + plugins/secretaria/routes.js | 302 + plugins/secretaria/routes.js.map | 1 + plugins/secretaria/routes.ts | 301 + plugins/secretaria/tools.js | 161 + plugins/secretaria/tools.js.map | 1 + plugins/secretaria/tools.ts | 218 + plugins/tsconfig.json | 26 + 244 files changed, 28512 insertions(+), 16680 deletions(-) create mode 100644 .htaccess create mode 100644 .user.ini create mode 100644 404.html create mode 100644 502.html delete mode 100644 backend/backend/plugins/UnifiedStorageProvider/index.js delete mode 100644 backend/check_column.ts delete mode 100644 backend/check_column_final.ts delete mode 100644 backend/check_ruibto.ts delete mode 100644 backend/fix_ruibto.ts delete mode 100644 backend/knexfile.ts delete mode 100644 backend/plugins/whatsapp-v1/.env.example delete mode 100644 backend/plugins/whatsapp-v1/.gitignore delete mode 100644 backend/plugins/whatsapp-v1/README.md delete mode 100644 backend/plugins/whatsapp-v1/package-lock.json delete mode 100644 backend/plugins/whatsapp-v1/package.json delete mode 100644 backend/plugins/whatsapp-v1/public/app.js delete mode 100644 backend/plugins/whatsapp-v1/public/index.html delete mode 100644 backend/plugins/whatsapp-v1/public/styles.css delete mode 100644 backend/plugins/whatsapp-v1/src/config.ts delete mode 100644 backend/plugins/whatsapp-v1/src/handlers/superAdminMenu.ts delete mode 100644 backend/plugins/whatsapp-v1/src/index.ts delete mode 100644 backend/plugins/whatsapp-v1/src/routes/instances.ts delete mode 100644 backend/plugins/whatsapp-v1/src/routes/messages.ts delete mode 100644 backend/plugins/whatsapp-v1/src/services/whatsapp.ts delete mode 100644 backend/plugins/whatsapp-v1/src/types/whatsapp.ts delete mode 100644 backend/plugins/whatsapp-v1/src/utils/helpers.ts delete mode 100644 backend/plugins/whatsapp-v1/tsconfig.json create mode 100644 backend/schema.sql delete mode 100644 backend/scripts/inspect_benefits.ts delete mode 100644 backend/scripts/test_update.ts create mode 100644 backend/seed.sql delete mode 100644 backend/seed_admin.js delete mode 100644 backend/src/config/logger.ts create mode 100644 backend/src/controllers/admin.controller.ts delete mode 100644 backend/src/controllers/lead.controller.ts delete mode 100644 backend/src/controllers/referral.controller.ts create mode 100644 backend/src/controllers/webhook.controller.ts delete mode 100644 backend/src/core/StorageProvider.ts delete mode 100644 backend/src/core/hooks.ts delete mode 100644 backend/src/core/plugin-config.ts delete mode 100644 backend/src/core/plugin-loader.ts delete mode 100644 backend/src/core/plugin-registry.ts delete mode 100644 backend/src/core/types.js delete mode 100644 backend/src/core/types.ts delete mode 100644 backend/src/database/migrations/20260215000000_init_schema.ts delete mode 100644 backend/src/database/migrations/20260216151047_add_deleted_at_to_users.ts delete mode 100644 backend/src/database/migrations/20260216163300_update_user_status_enum.ts delete mode 100644 backend/src/database/migrations/20260217004800_add_hide_partner_name_to_benefits.ts delete mode 100644 backend/src/database/migrations/20260217145800_add_partners_columns.ts delete mode 100644 backend/src/database/migrations/20260217210000_add_partner_logo_bg_color.ts delete mode 100644 backend/src/database/migrations/20260217220000_add_benefit_public_fields.ts delete mode 100644 backend/src/database/migrations/20260218120000_create_referrals.ts delete mode 100644 backend/src/infra/cache/dragonfly.ts delete mode 100644 backend/src/middleware/storage-health.ts create mode 100644 backend/src/migrations/20260210_001_initial_schema.ts delete mode 100644 backend/src/modules/whatsapp/engine/index.js delete mode 100644 backend/src/modules/whatsapp/engine/index.js.map delete mode 100644 backend/src/modules/whatsapp/engine/index.ts delete mode 100644 backend/src/modules/whatsapp/engine/infinite.js delete mode 100644 backend/src/modules/whatsapp/engine/infinite.js.map delete mode 100644 backend/src/modules/whatsapp/engine/infinite.ts delete mode 100644 backend/src/modules/whatsapp/engine/official.js delete mode 100644 backend/src/modules/whatsapp/engine/official.js.map delete mode 100644 backend/src/modules/whatsapp/engine/official.ts delete mode 100644 backend/src/modules/whatsapp/rich-message.ts delete mode 100644 backend/src/routes/lead.routes.ts delete mode 100644 backend/src/routes/referral.routes.ts delete mode 100644 backend/src/routes/upload.routes.ts create mode 100644 backend/src/seeds/001_initial_data.ts delete mode 100644 backend/src/services/UserLifecycleService.ts delete mode 100644 backend/src/types/express.d.ts delete mode 100644 backend/storage/plugin-configs.json delete mode 100644 backend/test_pg_conn.js create mode 100644 ecosystem.config.js create mode 100644 frontend/app/admin/page.tsx create mode 100644 frontend/app/benefits/page.tsx create mode 100644 frontend/app/dashboard/page.tsx create mode 100644 frontend/app/globals.css create mode 100644 frontend/app/layout.tsx create mode 100644 frontend/app/login/page.tsx create mode 100644 frontend/app/page.tsx create mode 100644 frontend/app/partner/page.tsx create mode 100644 frontend/app/profile/page.tsx create mode 100644 frontend/app/register/page.tsx create mode 100644 frontend/components/layout/DashboardLayout.tsx create mode 100644 frontend/components/layout/Sidebar.tsx delete mode 100644 frontend/index.html create mode 100644 frontend/lib/api.ts create mode 100644 frontend/lib/auth.tsx create mode 100644 frontend/next-env.d.ts create mode 100644 frontend/next.config.js create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/postcss.config.js delete mode 100644 frontend/script.js delete mode 100644 frontend/style.css create mode 100644 frontend/tailwind.config.js create mode 100644 frontend/tsconfig.json create mode 100644 index.html create mode 100644 nginx/clube67.conf create mode 100644 plugins/SmartVision/backend/Optimizer.js create mode 100644 plugins/SmartVision/backend/Optimizer.js.map create mode 100644 plugins/SmartVision/backend/Optimizer.ts create mode 100644 plugins/SmartVision/index.js create mode 100644 plugins/SmartVision/index.js.map create mode 100644 plugins/SmartVision/index.ts create mode 100644 plugins/SmartVision/manifest.json create mode 100644 plugins/UnifiedStorageProvider/index.js create mode 100644 plugins/UnifiedStorageProvider/index.js.map create mode 100644 plugins/UnifiedStorageProvider/index.ts create mode 100644 plugins/UnifiedStorageProvider/manifest.json create mode 100644 plugins/config.js create mode 100644 plugins/core-auth/backend/migrations/001_auth_tables.sql create mode 100644 plugins/core-auth/backend/routes.js create mode 100644 plugins/core-auth/backend/routes.js.map create mode 100644 plugins/core-auth/backend/routes.ts create mode 100644 plugins/core-auth/index.js create mode 100644 plugins/core-auth/index.js.map create mode 100644 plugins/core-auth/index.ts create mode 100644 plugins/core-auth/manifest.json create mode 100644 plugins/exemplo-plugin/index.js create mode 100644 plugins/exemplo-plugin/manifest.json create mode 100644 plugins/ext-api/backend/apikey-auth.js create mode 100644 plugins/ext-api/backend/apikey-auth.js.map create mode 100644 plugins/ext-api/backend/apikey-auth.ts create mode 100644 plugins/ext-api/backend/backend/src/config/env.js create mode 100644 plugins/ext-api/backend/backend/src/config/logger.js create mode 100644 plugins/ext-api/backend/backend/src/core/StorageProvider.js create mode 100644 plugins/ext-api/backend/backend/src/core/hook-bus.js create mode 100644 plugins/ext-api/backend/backend/src/core/plugin-config.js create mode 100644 plugins/ext-api/backend/backend/src/core/types.js create mode 100644 plugins/ext-api/backend/backend/src/infra/cache/dragonfly.js create mode 100644 plugins/ext-api/backend/backend/src/infra/database/prisma.js create mode 100644 plugins/ext-api/backend/backend/src/modules/chatbot/chatbot.repository.js create mode 100644 plugins/ext-api/backend/backend/src/modules/chatbot/chatbot.service.js create mode 100644 plugins/ext-api/backend/backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.js create mode 100644 plugins/ext-api/backend/backend/src/modules/whatsapp/handlers/ContactHandler.js create mode 100644 plugins/ext-api/backend/backend/src/modules/whatsapp/handlers/MessageHandler.js create mode 100644 plugins/ext-api/backend/backend/src/shared/utils/whatsapp.js create mode 100644 plugins/ext-api/backend/plugins/ext-api/backend/routes.js create mode 100644 plugins/ext-api/backend/plugins/secretaria/brain.js create mode 100644 plugins/ext-api/backend/plugins/secretaria/tools.js create mode 100644 plugins/ext-api/backend/routes.js create mode 100644 plugins/ext-api/backend/routes.js.map create mode 100644 plugins/ext-api/backend/routes.ts create mode 100644 plugins/ext-api/backend/webhook-dispatcher.js create mode 100644 plugins/ext-api/backend/webhook-dispatcher.js.map create mode 100644 plugins/ext-api/backend/webhook-dispatcher.ts create mode 100644 plugins/ext-api/backend/ws-bridge.js create mode 100644 plugins/ext-api/backend/ws-bridge.js.map create mode 100644 plugins/ext-api/backend/ws-bridge.ts create mode 100644 plugins/ext-api/index.js create mode 100644 plugins/ext-api/index.js.map create mode 100644 plugins/ext-api/index.ts create mode 100644 plugins/ext-api/manifest.json create mode 100644 plugins/financeiro/backend/migrations/001_financial_tables.sql create mode 100644 plugins/financeiro/backend/routes.js create mode 100644 plugins/financeiro/backend/routes.js.map create mode 100644 plugins/financeiro/backend/routes.ts create mode 100644 plugins/financeiro/index.js create mode 100644 plugins/financeiro/index.js.map create mode 100644 plugins/financeiro/index.ts create mode 100644 plugins/financeiro/manifest.json create mode 100644 plugins/index.js create mode 100644 plugins/leads/backend/migrations/001_leads_table.sql create mode 100644 plugins/leads/backend/migrations/002_normalize_leads.sql create mode 100644 plugins/leads/backend/migrations/003_lead_history.sql create mode 100644 plugins/leads/backend/migrations/004_lead_owner.sql create mode 100644 plugins/leads/backend/migrations/005_lead_classification.sql create mode 100644 plugins/leads/backend/routes.js create mode 100644 plugins/leads/backend/routes.js.map create mode 100644 plugins/leads/backend/routes.ts create mode 100644 plugins/leads/index.js create mode 100644 plugins/leads/index.js.map create mode 100644 plugins/leads/index.ts create mode 100644 plugins/leads/manifest.json create mode 100644 plugins/nanobana/backend/migrations/001_create_usage_table.sql create mode 100644 plugins/nanobana/backend/routes.js create mode 100644 plugins/nanobana/backend/routes.js.map create mode 100644 plugins/nanobana/backend/routes.ts create mode 100644 plugins/nanobana/index.js create mode 100644 plugins/nanobana/index.js.map create mode 100644 plugins/nanobana/index.ts create mode 100644 plugins/nanobana/manifest.json create mode 100644 plugins/newwhats/auth.js create mode 100644 plugins/newwhats/context-builder.js create mode 100644 plugins/newwhats/follow-up-detector.js create mode 100644 plugins/newwhats/index.js create mode 100644 plugins/newwhats/manifest.json create mode 100644 plugins/newwhats/media-transcribe.js create mode 100644 plugins/newwhats/personas.js create mode 100644 plugins/newwhats/proxy.js create mode 100644 plugins/newwhats/routes.js create mode 100644 plugins/newwhats/smart-router.js create mode 100644 plugins/newwhats/sync-knowledge.js create mode 100644 plugins/newwhats/webhook-receiver.js create mode 100644 plugins/secretaria/PLUGIN_EMPRESA.md create mode 100644 plugins/secretaria/brain.js create mode 100644 plugins/secretaria/brain.js.map create mode 100644 plugins/secretaria/brain.ts create mode 100644 plugins/secretaria/index.js create mode 100644 plugins/secretaria/index.js.map create mode 100644 plugins/secretaria/index.ts create mode 100644 plugins/secretaria/manifest.json create mode 100644 plugins/secretaria/migrate.js create mode 100644 plugins/secretaria/migrate.js.map create mode 100644 plugins/secretaria/migrate.ts create mode 100644 plugins/secretaria/routes.js create mode 100644 plugins/secretaria/routes.js.map create mode 100644 plugins/secretaria/routes.ts create mode 100644 plugins/secretaria/tools.js create mode 100644 plugins/secretaria/tools.js.map create mode 100644 plugins/secretaria/tools.ts create mode 100644 plugins/tsconfig.json diff --git a/.gitignore b/.gitignore index 51bcad2..1a7dc61 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,35 @@ -# Node modules +# Node.js node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* -# Builds e Compilados -dist/ -build/ -out/ +# Next.js .next/ +out/ -# Variáveis de ambiente sensíveis +# TypeScript +dist/ +*.tsbuildinfo + +# Env / local configs .env -.env.production .env.local -.env.development +.env.development.local +.env.test.local +.env.production.local +.env.sync.local -# Logs e Dados de Estado locais -*.log +# Logs logs/ +*.log -# Uploads locais de mídia +# Uploads / storage uploads/ +storage/db/ +storage/sessions/ -# Sistema +# OS Files .DS_Store Thumbs.db - -# Legado / Projeto NewWhats (Migrado e Isolado) -newwhats.local/ diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..0519ecb --- /dev/null +++ b/.htaccess @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/.user.ini b/.user.ini new file mode 100644 index 0000000..ff95de1 --- /dev/null +++ b/.user.ini @@ -0,0 +1 @@ +open_basedir=/www/wwwroot/clube67.com/:/tmp/ \ No newline at end of file diff --git a/404.html b/404.html new file mode 100644 index 0000000..a2f560c --- /dev/null +++ b/404.html @@ -0,0 +1,74 @@ + + + + + + 404 Not Found + + + +
+ +
404
+
Sorry, the page you visited does not exist.
It may be that the access link is wrong or the file does not exist.
+ +
+ + diff --git a/502.html b/502.html new file mode 100644 index 0000000..5f25659 --- /dev/null +++ b/502.html @@ -0,0 +1,74 @@ + + + + + + 502 Bad Gateway + + + +
+ +
502
+
Gateway error.
The website server has timed out in returning data, please wait a few minutes and try again.
+ +
+ + diff --git a/backend/backend/plugins/UnifiedStorageProvider/index.js b/backend/backend/plugins/UnifiedStorageProvider/index.js deleted file mode 100644 index 3f900c4..0000000 --- a/backend/backend/plugins/UnifiedStorageProvider/index.js +++ /dev/null @@ -1,214 +0,0 @@ -"use strict"; -// ============================================================ -// Clube67 — Unified Storage Provider (Wasabi) -// ============================================================ -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const aws_sdk_1 = __importDefault(require("aws-sdk")); -const StorageProvider_1 = require("../../backend/src/core/StorageProvider"); -const plugin_config_1 = require("../../backend/src/core/plugin-config"); -const VALID_CATEGORIES = ['cdi', 'xrays', 'documents', 'exports', 'posts', 'cards', 'partners', 'whatsapp', 'media']; -class UnifiedStorageProviderPlugin { - constructor() { - this.s3 = null; - this.bucket = ''; - } - /** Activation logic */ - async activate(ctx) { - const storedConfig = plugin_config_1.pluginConfig.get('UnifiedStorageProvider') || {}; - const accessKey = storedConfig.wasabiAccessKey || process.env.WASABI_ACCESS_KEY; - const secretKey = storedConfig.wasabiSecretKey || process.env.WASABI_SECRET_KEY; - const region = storedConfig.wasabiRegion || process.env.WASABI_REGION || 'us-east-1'; - this.bucket = storedConfig.wasabiBucket || process.env.WASABI_BUCKET || ''; - if (!accessKey || !secretKey || !this.bucket) { - ctx.logger.error('Wasabi credentials or bucket missing in .env or plugin config'); - return; - } - aws_sdk_1.default.config.update({ - accessKeyId: accessKey, - secretAccessKey: secretKey, - region: region, - }); - this.s3 = new aws_sdk_1.default.S3({ - endpoint: 'https://s3.wasabisys.com', - s3ForcePathStyle: true, // Recommended for Wasabi/Minio - }); - // Register this plugin as the central storage provider - StorageProvider_1.storageProvider.register(this); - ctx.logger.info('UnifiedStorageProvider activated and registered in core'); - // Add route to fetch buckets (for admin config) - ctx.app.post('/api/storage/fetch-buckets', async (req, res) => { - console.log(`[UnifiedStorageProvider] POST /api/storage/fetch-buckets - Body: ${JSON.stringify(req.body)}`); - const { accessKey, secretKey } = req.body; - if (!accessKey || !secretKey) { - return res.status(400).json({ error: 'Access Key e Secret Key são obrigatórios.' }); - } - try { - const s3 = new aws_sdk_1.default.S3({ - accessKeyId: accessKey, - secretAccessKey: secretKey, - endpoint: 'https://s3.wasabisys.com', - s3ForcePathStyle: true, - }); - const data = await s3.listBuckets().promise(); - const buckets = data.Buckets?.map(b => b.Name).filter(Boolean) || []; - console.log(`[UnifiedStorageProvider] Found ${buckets.length} buckets`); - res.json({ buckets }); - } - catch (err) { - console.error('[UnifiedStorageProvider] Fetch buckets failed:', err); - res.status(500).json({ error: `Erro ao buscar buckets: ${err.message}` }); - } - }); - // Add route to create bucket - ctx.app.post('/api/storage/create-bucket', async (req, res) => { - console.log(`[UnifiedStorageProvider] POST /api/storage/create-bucket - Body: ${JSON.stringify(req.body)}`); - const { accessKey, secretKey, bucketName, region } = req.body; - if (!accessKey || !secretKey || !bucketName) { - return res.status(400).json({ error: 'Parâmetros incompletos para criação de bucket.' }); - } - try { - const s3 = new aws_sdk_1.default.S3({ - accessKeyId: accessKey, - secretAccessKey: secretKey, - endpoint: 'https://s3.wasabisys.com', - s3ForcePathStyle: true, - region: region || 'us-east-1' - }); - await s3.createBucket({ Bucket: bucketName }).promise(); - console.log(`[UnifiedStorageProvider] Bucket ${bucketName} created successfully`); - res.json({ success: true, message: `Bucket ${bucketName} criado com sucesso.` }); - } - catch (err) { - console.error('[UnifiedStorageProvider] Create bucket failed:', err); - res.status(500).json({ error: `Erro ao criar bucket: ${err.message}` }); - } - }); - // Add route to list files (for search tab) - ctx.app.post('/api/storage/list-files', async (req, res) => { - console.log(`[UnifiedStorageProvider] POST /api/storage/list-files - Body: ${JSON.stringify(req.body)}`); - const { accessKey, secretKey, bucket, prefix } = req.body; - if (!accessKey || !secretKey || !bucket) { - return res.status(400).json({ error: 'Parâmetros incompletos.' }); - } - try { - const s3 = new aws_sdk_1.default.S3({ - accessKeyId: accessKey, - secretAccessKey: secretKey, - endpoint: 'https://s3.wasabisys.com', - s3ForcePathStyle: true, - }); - const data = await s3.listObjectsV2({ - Bucket: bucket, - Prefix: prefix || '', - MaxKeys: 20 - }).promise(); - const files = data.Contents?.map(item => ({ - key: item.Key, - size: item.Size, - lastModified: item.LastModified - })) || []; - console.log(`[UnifiedStorageProvider] Found ${files.length} files in bucket ${bucket}`); - res.json({ files }); - } - catch (err) { - console.error('[UnifiedStorageProvider] List files failed:', err); - res.status(500).json({ error: `Erro ao listar arquivos: ${err.message}` }); - } - }); - // Start periodic health check (every 30 seconds) - setInterval(() => { - StorageProvider_1.storageProvider.updateHealth(); - }, 30000); - } - /** Deactivation logic */ - async deactivate(ctx) { - ctx.logger.info('UnifiedStorageProvider deactivated'); - } - /** Health Check Implementation */ - async checkHealth() { - if (!this.s3 || !this.bucket) - return false; - try { - // headBucket is a cheap way to verify connectivity and credentials - await this.s3.headBucket({ Bucket: this.bucket }).promise(); - return true; - } - catch (err) { - console.error('[UnifiedStorageProvider] Health check failed:', err); - return false; - } - } - /** Centralized Upload Logic */ - async upload(payload) { - if (!this.s3) { - throw new Error('[UnifiedStorageProvider] S3 client not initialized. Plugin error?'); - } - const storedConfig = plugin_config_1.pluginConfig.get('UnifiedStorageProvider') || {}; - const { clinicId, patientId, partnerId, benefitId, postId, whatsappId, userId, category, file } = payload; - if (!VALID_CATEGORIES.includes(category)) { - throw new Error(`Categoria inválida: ${category}`); - } - const timestamp = Date.now(); - const safeFilename = file.originalname.replace(/[^a-zA-Z0-9.-]/g, '_'); - let path = ''; - // Routing logic based on category - switch (category) { - case 'posts': - path = `posts/${timestamp}-${safeFilename}`; - break; - case 'cards': - path = `benefits/${benefitId || 'unclassified'}/${timestamp}-${safeFilename}`; - break; - case 'partners': - path = `partners/${partnerId || 'unclassified'}/${timestamp}-${safeFilename}`; - break; - case 'whatsapp': - path = `whatsapp/${timestamp}-${safeFilename}`; - break; - case 'media': - path = `media/${timestamp}-${safeFilename}`; - break; - // Legacy/Clinical Clinical logic - case 'cdi': - case 'xrays': - case 'documents': - case 'exports': - if (!clinicId || !patientId) - throw new Error('clinicId/patientId obrigatórios para dados clínicos'); - path = `clinics/${clinicId}/patients/${patientId}/${category}/${timestamp}-${safeFilename}`; - break; - default: - path = `${category}/${timestamp}-${safeFilename}`; - } - // Apply USER separation if userId is present (for general categories) - if (userId && !['cdi', 'xrays', 'documents', 'exports'].includes(category)) { - path = `users/${userId}/${path}`; - } - // Determine destination bucket (Mapping Overrides) - // category_bucket_whatsapp, category_bucket_posts, etc. - const mappedBucket = storedConfig[`category_bucket_${category}`] || this.bucket; - const params = { - Bucket: mappedBucket, - Key: path, - Body: file.buffer, - ContentType: file.mimetype, - }; - try { - await this.s3.upload(params).promise(); - return { - provider: 'wasabi', - path, - }; - } - catch (err) { - console.error('[UnifiedStorageProvider] Upload failed:', err); - throw new Error(`Upload storage error: ${err.message}`); - } - } -} -const instance = new UnifiedStorageProviderPlugin(); -exports.default = instance; -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/backend/check_column.ts b/backend/check_column.ts deleted file mode 100644 index 835467b..0000000 --- a/backend/check_column.ts +++ /dev/null @@ -1,30 +0,0 @@ - -import { db } from './src/config/database'; - -async function check() { - try { - const id = 1; // Assuming ID 1 exists from previous run - console.log('Testing update on ID:', id); - - // 1. Update to TRUE - console.log('Updating hide_partner_name to TRUE...'); - await db('benefits').where({ id }).update({ hide_partner_name: true }); - - let row = await db('benefits').where({ id }).first(); - console.log('After TRUE update:', row.hide_partner_name, typeof row.hide_partner_name); - - // 2. Update to FALSE - console.log('Updating hide_partner_name to FALSE...'); - await db('benefits').where({ id }).update({ hide_partner_name: false }); - - row = await db('benefits').where({ id }).first(); - console.log('After FALSE update:', row.hide_partner_name, typeof row.hide_partner_name); - - process.exit(0); - } catch (e) { - console.error('Error:', e); - process.exit(1); - } -} - -check(); diff --git a/backend/check_column_final.ts b/backend/check_column_final.ts deleted file mode 100644 index 42607f7..0000000 --- a/backend/check_column_final.ts +++ /dev/null @@ -1,23 +0,0 @@ - -import { db } from './src/config/database'; - -async function check() { - try { - const id = 1; - console.log('Testing update on ID:', id); - - // 1. Update to TRUE - console.log('Updating hide_partner_name to TRUE...'); - await db('benefits').where({ id }).update({ hide_partner_name: true }); - - let row = await db('benefits').where({ id }).first(); - console.log('After TRUE update:', row.hide_partner_name, typeof row.hide_partner_name); - - process.exit(0); - } catch (e) { - console.error('Error:', e); - process.exit(1); - } -} - -check(); diff --git a/backend/check_ruibto.ts b/backend/check_ruibto.ts deleted file mode 100644 index f44fef8..0000000 --- a/backend/check_ruibto.ts +++ /dev/null @@ -1,20 +0,0 @@ - -import { db } from './src/config/database'; - -async function checkUser() { - try { - const user = await db('users').where({ email: 'ruibto@gmail.com' }).first(); - console.log('User Record:', user); - - if (user) { - const partner = await db('partners').where({ owner_user_id: user.id }).first(); - console.log('Partner Record:', partner); - } - process.exit(0); - } catch (error) { - console.error(error); - process.exit(1); - } -} - -checkUser(); diff --git a/backend/fix_ruibto.ts b/backend/fix_ruibto.ts deleted file mode 100644 index b623a23..0000000 --- a/backend/fix_ruibto.ts +++ /dev/null @@ -1,22 +0,0 @@ - -import { db } from './src/config/database'; - -async function fixUser() { - try { - // Hardcoded fix for ruibto@gmail.com based on previous check - const userEmail = 'ruibto@gmail.com'; - const partnerId = 4; // Consultt Clinic - - await db('users') - .where({ email: userEmail }) - .update({ partner_id: partnerId }); - - console.log(`Updated user ${userEmail} with partner_id ${partnerId}`); - process.exit(0); - } catch (error) { - console.error(error); - process.exit(1); - } -} - -fixUser(); diff --git a/backend/knexfile.ts b/backend/knexfile.ts deleted file mode 100644 index 1c913ee..0000000 --- a/backend/knexfile.ts +++ /dev/null @@ -1,24 +0,0 @@ -import dotenv from 'dotenv'; -import path from 'path'; - -// Enforce loading the .env from the knexfile directory (project root) -dotenv.config({ path: path.join(__dirname, '.env') }); - -const config = { - client: 'pg', - connection: { - host: process.env.DB_HOST, - port: parseInt(process.env.DB_PORT || '5432', 10), - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - }, - pool: { min: 2, max: 10 }, - migrations: { - directory: './src/database/migrations', - tableName: 'knex_migrations' - } -}; - -export default config; -module.exports = config; diff --git a/backend/package-lock.json b/backend/package-lock.json index dd4f878..21b795a 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,5595 +1,3229 @@ { - "name": "clube67-backend", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "clube67-backend", - "version": "1.0.0", - "dependencies": { - "@aws-sdk/client-s3": "^3.996.0", - "@aws-sdk/lib-storage": "^3.996.0", - "@aws-sdk/s3-request-presigner": "^3.996.0", - "@opentelemetry/api": "^1.9.0", - "@types/qrcode": "^1.5.6", - "aws-sdk": "^2.1693.0", - "baileys": "^7.0.0-rc.9", - "bcryptjs": "^2.4.3", - "compression": "^1.7.4", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-rate-limit": "^7.2.0", - "helmet": "^7.1.0", - "hpp": "^0.2.3", - "ioredis": "^5.4.1", - "jsonwebtoken": "^9.0.3", - "knex": "^3.1.0", - "multer": "^1.4.5-lts.1", - "mysql2": "^3.18.0", - "openai": "^6.22.0", - "pg": "^8.11.5", - "protobufjs": "^8.0.0", - "qrcode": "^1.5.4", - "socket.io": "^4.7.5", - "uuid": "^9.0.1", - "winston": "^3.13.0", - "winston-daily-rotate-file": "^5.0.0" - }, - "devDependencies": { - "@types/bcryptjs": "^2.4.6", - "@types/compression": "^1.7.5", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/jsonwebtoken": "^9.0.6", - "@types/multer": "^1.4.11", - "@types/node": "^20.12.7", - "@types/pg": "^8.11.5", - "@types/uuid": "^9.0.8", - "ts-node-dev": "^2.0.0", - "typescript": "^5.4.5" - } - }, - "node_modules/@aws-crypto/crc32": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/crc32c": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", - "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha1-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", - "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", - "dependencies": { - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-s3": { - "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1048.0.tgz", - "integrity": "sha512-SrJn5FteqqtcDBgQIvqLKk3Qn/2vSsi5XR03I53EDDR4CbCdLysVSNgUnjVncEECMua9Pz+nxO0/lEx3TP+6mA==", - "dependencies": { - "@aws-crypto/sha1-browser": "5.2.0", - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/credential-provider-node": "^3.972.42", - "@aws-sdk/middleware-bucket-endpoint": "^3.972.13", - "@aws-sdk/middleware-expect-continue": "^3.972.12", - "@aws-sdk/middleware-flexible-checksums": "^3.974.19", - "@aws-sdk/middleware-location-constraint": "^3.972.10", - "@aws-sdk/middleware-sdk-s3": "^3.972.40", - "@aws-sdk/middleware-ssec": "^3.972.10", - "@aws-sdk/signature-v4-multi-region": "^3.996.27", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.974.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", - "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.24", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/core": "^3.24.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/crc64-nvme": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.8.tgz", - "integrity": "sha512-fVfUCL/Xh2zINYMPZvj+iBn6XWouQf0DAnjaWCI9MkmqXzL2Iy5FoQB8O7syFe6gN6AH1ecDDU58T51Ou0kFkA==", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", - "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.39", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", - "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", - "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/credential-provider-env": "^3.972.37", - "@aws-sdk/credential-provider-http": "^3.972.39", - "@aws-sdk/credential-provider-login": "^3.972.41", - "@aws-sdk/credential-provider-process": "^3.972.37", - "@aws-sdk/credential-provider-sso": "^3.972.41", - "@aws-sdk/credential-provider-web-identity": "^3.972.41", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", - "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", - "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.37", - "@aws-sdk/credential-provider-http": "^3.972.39", - "@aws-sdk/credential-provider-ini": "^3.972.41", - "@aws-sdk/credential-provider-process": "^3.972.37", - "@aws-sdk/credential-provider-sso": "^3.972.41", - "@aws-sdk/credential-provider-web-identity": "^3.972.41", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", - "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", - "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/token-providers": "3.1048.0", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", - "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/lib-storage": { - "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.1048.0.tgz", - "integrity": "sha512-3u+Ycwo84crWoyPU4DrZgv6GO92cUE4CX5/h32vmSMeG8e93XgB40Jz2PqPim/r73IYRuQUrZY8E1P1MHvXZqw==", - "dependencies": { - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "buffer": "5.6.0", - "events": "3.3.0", - "stream-browserify": "3.0.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "@aws-sdk/client-s3": "^3.1048.0" - } - }, - "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.972.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.13.tgz", - "integrity": "sha512-JDaukix+kt5KwF7FzNSkfZHpqiPJajVkKJLJexF6z5B44+CN70BXGiQaCEAiCtKtRZNvC16eF3SY9L0bDJPlbA==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.972.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.12.tgz", - "integrity": "sha512-dA5pKTom/Ls9mgeyeaRBNQrRIVOLVjv4AmKOB0/e4yaiXEUy0gSz2d3liP8JHtYoCAEWySU1jWnyzwLOREN+4g==", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.19.tgz", - "integrity": "sha512-GLciZVIvWM3C+ffuqnUqlAZwRjQdLt+KXiqr9+aRwZyKVyF2J5lrJAzzSqwweNl9hUWBN00BhilWXdMI5DjNcw==", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@aws-crypto/crc32c": "5.2.0", - "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/crc64-nvme": "^3.972.8", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.10.tgz", - "integrity": "sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.40", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.40.tgz", - "integrity": "sha512-vyFY4EsAGySqqd87Z7n4qcCYXJO3QArB8VIJzuupY5XuLHIp579HTZldIUGGABvAOzLptfPb9+lJBJcB+3/cvA==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/signature-v4-multi-region": "^3.996.27", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.972.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.10.tgz", - "integrity": "sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", - "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/signature-v4-multi-region": "^3.996.27", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.1048.0.tgz", - "integrity": "sha512-7/yZh562OhKvwBFS/nVL+7qMecUGO91XfaPfqisWiswtlgoQ4gt4zHn9MuUwoPcIYgdkOO0RucOzcD13u7uJEg==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/signature-v4-multi-region": "^3.996.27", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", - "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/token-providers": { - "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", - "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", - "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", - "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", - "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", - "dependencies": { - "@nodable/entities": "2.1.0", - "@smithy/types": "^4.14.1", - "fast-xml-parser": "5.7.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", - "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@borewit/text-codec": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", - "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@cacheable/memory": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.9.tgz", - "integrity": "sha512-HdMx6DoGywB30vacDbBsITbIX4pgFqj1zsrV58jZBUw3klzkNoXhj7qOqAgledhxG7YZI5rBSJg7Zp8/VG0DuA==", - "dependencies": { - "@cacheable/utils": "^2.4.1", - "@keyv/bigmap": "^1.3.1", - "hookified": "^1.15.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@cacheable/node-cache": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", - "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==", - "dependencies": { - "cacheable": "^2.3.1", - "hookified": "^1.14.0", - "keyv": "^5.5.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@cacheable/utils": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.1.tgz", - "integrity": "sha512-eiFgzCbIneyMlLOmNG4g9xzF7Hv3Mga4LjxjcSC/ues6VYq2+gUbQI8JqNuw/ZM8tJIeIaBGpswAsqV2V7ApgA==", - "dependencies": { - "hashery": "^1.5.1", - "keyv": "^5.6.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", - "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@dabh/diagnostics": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", - "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", - "dependencies": { - "@so-ric/colorspace": "^1.1.6", - "enabled": "2.0.x", - "kuler": "^2.0.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@hapi/boom": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", - "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", - "dependencies": { - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "peer": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "peer": true, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@ioredis/commands": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", - "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==" - }, - "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, - "engines": { - "node": ">=6.0.0" - } - }, - "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==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", - "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.6.0" - } - }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==" - }, - "node_modules/@nodable/entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/nodable" - } - ] - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" - }, - "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==" - }, - "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==" - }, - "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==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "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==" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz", - "integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==" - }, - "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==" - }, - "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==" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", - "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==" - }, - "node_modules/@smithy/core": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", - "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/credential-provider-imds": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", - "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", - "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", - "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", - "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/types": { - "version": "4.14.2", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", - "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@so-ric/colorspace": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", - "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", - "dependencies": { - "color": "^5.0.2", - "text-hex": "1.0.x" - } - }, - "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==" - }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/inflate/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@tokenizer/inflate/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==" - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", - "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true - }, - "node_modules/@types/bcryptjs": { - "version": "2.4.6", - "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", - "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", - "dev": true - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", - "dev": true, - "dependencies": { - "@types/express": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cors": { - "version": "2.8.19", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", - "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true - }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "dev": true, - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true - }, - "node_modules/@types/multer": { - "version": "1.4.13", - "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", - "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", - "dev": true, - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/node": { - "version": "20.19.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", - "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", - "dev": true, - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/qrcode": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", - "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", - "dev": true - }, - "node_modules/@types/strip-json-comments": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", - "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", - "dev": true - }, - "node_modules/@types/triple-beam": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" - }, - "node_modules/@types/uuid": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", - "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", - "dev": true - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansi-styles/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==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ansi-styles/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==" - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/append-field": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", - "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" - }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" - }, - "node_modules/async-mutex": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", - "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "engines": { - "node": ">=8.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==", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/aws-sdk": { - "version": "2.1693.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1693.0.tgz", - "integrity": "sha512-cJmb8xEnVLT+R6fBS5sn/EFJiX7tUnDaPtOPZ1vFbOJtd0fnZn/Ky2XGgsvvoeliWeH7mL3TWSX5zXXGSQV6gQ==", - "deprecated": "The AWS SDK for JavaScript (v2) has reached end-of-support, and no longer receives updates. Please migrate your code to use AWS SDK for JavaScript (v3). More info https://a.co/cUPnyil", - "hasInstallScript": true, - "dependencies": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "util": "^0.12.4", - "uuid": "8.0.0", - "xml2js": "0.6.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aws-sdk/node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "node_modules/aws-sdk/node_modules/events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/aws-sdk/node_modules/ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "node_modules/aws-sdk/node_modules/uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/baileys": { - "version": "7.0.0-rc11", - "resolved": "https://registry.npmjs.org/baileys/-/baileys-7.0.0-rc11.tgz", - "integrity": "sha512-yi7An4f5DmShjb75L8eSttrA6HLy7PYoP3XIQBmvQ5yH8vVZWBXZDogDxfGQQL6N98ELRwznG7puhjyLEa5CQg==", - "hasInstallScript": true, - "dependencies": { - "@cacheable/node-cache": "^1.4.0", - "@hapi/boom": "^9.1.3", - "async-mutex": "^0.5.0", - "libsignal": "^6.0.0", - "lru-cache": "^11.1.0", - "music-metadata": "^11.12.3", - "p-queue": "^9.0.0", - "pino": "^9.6", - "protobufjs": "^7.5.6", - "whatsapp-rust-bridge": "0.5.4", - "ws": "^8.13.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "audio-decode": "^2.1.3", - "jimp": "^1.6.1", - "link-preview-js": "^3.0.0", - "sharp": "*" - }, - "peerDependenciesMeta": { - "audio-decode": { - "optional": true - }, - "jimp": { - "optional": true - }, - "link-preview-js": { - "optional": true - } - } - }, - "node_modules/baileys/node_modules/protobufjs": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz", - "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.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 - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "engines": { - "node": "^4.5.0 || >= 5.9" - } - }, - "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" - }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "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==" - }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacheable": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.5.tgz", - "integrity": "sha512-EQfaKe09tl615iNvq/TBRWTFf1AKJNXYQSsMx0Z3EI0nA+pVsVPS8wJhnRlkbdacKPh1d0qVIhwTc2zsQNFEEg==", - "dependencies": { - "@cacheable/memory": "^2.0.8", - "@cacheable/utils": "^2.4.1", - "hookified": "^1.15.0", - "keyv": "^5.6.0", - "qified": "^0.10.1" - } - }, - "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==", - "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==", - "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==", - "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/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", - "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", - "dependencies": { - "color-convert": "^3.1.3", - "color-string": "^2.1.3" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/color-convert": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", - "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=14.6" - } - }, - "node_modules/color-name": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", - "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/color-string": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", - "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", - "dependencies": { - "color-name": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/colorette": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", - "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" - }, - "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "engines": { - "node": ">=14" - } - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "node_modules/cors": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", - "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", - "dependencies": { - "object-assign": "^4", - "vary": "^1" - }, - "engines": { - "node": ">= 0.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "node_modules/curve25519-js": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", - "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "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==", - "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/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/diff": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", - "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", - "dev": true, - "engines": { - "node": ">=0.3.1" - } - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "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==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/dynamic-dedupe": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", - "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", - "dev": true, - "dependencies": { - "xtend": "^4.0.0" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "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==" - }, - "node_modules/enabled": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/engine.io": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.7.tgz", - "integrity": "sha512-DgOngfDKM2EviOH3Mr9m7ks1q8roetLy/IMmYthAYzbpInMbYc/GS+fWFA3rl1gvwKVsQrVV61fo5emD1y3OJQ==", - "dependencies": { - "@types/cors": "^2.8.12", - "@types/node": ">=10.0.0", - "@types/ws": "^8.5.12", - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.7.2", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.18.3" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "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==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io/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==" - }, - "node_modules/engine.io/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "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/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==", - "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==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-xml-builder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", - "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "dependencies": { - "path-expression-matcher": "^1.5.0", - "xml-naming": "^0.1.0" - } - }, - "node_modules/fast-xml-parser": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", - "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "dependencies": { - "@nodable/entities": "^2.1.0", - "fast-xml-builder": "^1.1.7", - "path-expression-matcher": "^1.5.0", - "strnum": "^2.2.3" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/fecha": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" - }, - "node_modules/file-stream-rotator": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz", - "integrity": "sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ==", - "dependencies": { - "moment": "^2.29.1" - } - }, - "node_modules/file-type": { - "version": "21.3.4", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", - "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "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, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/fn.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "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, - "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==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "dependencies": { - "is-property": "^1.0.2" - } - }, - "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==", - "engines": { - "node": ">= 0.4" - } - }, - "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==", - "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==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/getopts": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", - "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/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, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "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==", - "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==", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hashery": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", - "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", - "dependencies": { - "hookified": "^1.15.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/helmet": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz", - "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/hookified": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", - "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==" - }, - "node_modules/hpp": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hpp/-/hpp-0.2.3.tgz", - "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", - "dependencies": { - "lodash": "^4.17.12", - "type-is": "^1.6.12" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/interpret": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", - "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/ioredis": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", - "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", - "dependencies": { - "@ioredis/commands": "1.5.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, - "node_modules/ioredis/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/ioredis/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==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "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-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "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==", - "dependencies": { - "hasown": "^2.0.3" - }, - "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, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "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==", - "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, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" - }, - "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==", - "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-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" - }, - "node_modules/jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", - "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "dependencies": { - "jws": "^4.0.1", - "lodash.includes": "^4.3.0", - "lodash.isboolean": "^3.0.3", - "lodash.isinteger": "^4.0.4", - "lodash.isnumber": "^3.0.3", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.once": "^4.0.0", - "ms": "^2.1.1", - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jsonwebtoken/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==" - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/knex": { - "version": "3.2.10", - "resolved": "https://registry.npmjs.org/knex/-/knex-3.2.10.tgz", - "integrity": "sha512-oypTHfrc9i72iyxaUQBKHOxhcr0xM65MPf6FpN02nimsftXwzXprIkLjfXdubvhbu4PMWLp023q8o8CYvHSuZw==", - "dependencies": { - "colorette": "2.0.19", - "commander": "^10.0.0", - "debug": "4.3.4", - "escalade": "^3.1.1", - "esm": "^3.2.25", - "get-package-type": "^0.1.0", - "getopts": "2.3.0", - "interpret": "^2.2.0", - "lodash": "^4.18.1", - "pg-connection-string": "2.6.2", - "rechoir": "^0.8.0", - "resolve-from": "^5.0.0", - "tarn": "^3.0.2", - "tildify": "2.0.0" - }, - "bin": { - "knex": "bin/cli.js" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "pg-query-stream": "^4.14.0" - }, - "peerDependenciesMeta": { - "better-sqlite3": { - "optional": true - }, - "mysql": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-native": { - "optional": true - }, - "pg-query-stream": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "tedious": { - "optional": true - } - } - }, - "node_modules/knex/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/knex/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "node_modules/kuler": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" - }, - "node_modules/libsignal": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/libsignal/-/libsignal-6.0.0.tgz", - "integrity": "sha512-d/5V3YFtDljbFMufz4ncyUYGYhJl+vzAe+c2EFFBQ6bz1h8Q3IOMEGXYMzlibU60I+e8GagMMpji18iez3P1hA==", - "dependencies": { - "curve25519-js": "^0.0.4", - "protobufjs": "^7.5.5" - } - }, - "node_modules/libsignal/node_modules/protobufjs": { - "version": "7.5.9", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.9.tgz", - "integrity": "sha512-Od4muIm3HW1AouyHF5lONOf1FWo3hY1NbFDoy191X9GzhpgW1clCoaFjfVs2rKJNFYpTNJbje4cbAIDBZJ63ZA==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.5", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.1", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.2", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.1", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==" - }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" - }, - "node_modules/lodash.includes": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", - "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" - }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" - }, - "node_modules/lodash.isboolean": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", - "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" - }, - "node_modules/lodash.isinteger": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", - "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" - }, - "node_modules/lodash.isnumber": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", - "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" - }, - "node_modules/lodash.isstring": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" - }, - "node_modules/lodash.once": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", - "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" - }, - "node_modules/logform": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", - "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", - "dependencies": { - "@colors/colors": "1.6.0", - "@types/triple-beam": "^1.3.2", - "fecha": "^4.2.0", - "ms": "^2.1.1", - "safe-stable-stringify": "^2.3.1", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/logform/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==" - }, - "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==" - }, - "node_modules/lru-cache": { - "version": "11.3.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", - "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/lru.min": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", - "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, - "node_modules/make-error": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "dev": true - }, - "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==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "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==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "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, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/multer": { - "version": "1.4.5-lts.2", - "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz", - "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", - "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", - "dependencies": { - "append-field": "^1.0.0", - "busboy": "^1.0.0", - "concat-stream": "^1.5.2", - "mkdirp": "^0.5.4", - "object-assign": "^4.1.1", - "type-is": "^1.6.4", - "xtend": "^4.0.0" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/music-metadata": { - "version": "11.12.3", - "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.12.3.tgz", - "integrity": "sha512-n6hSTZkuD59qWgHh6IP5dtDlDZQXoxk/bcA85Jywg8Z1iFrlNgl2+GTFgjZyn52W5UgQpV42V4XqrQZZAMbZTQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - }, - { - "type": "buymeacoffee", - "url": "https://buymeacoffee.com/borewit" - } - ], - "dependencies": { - "@borewit/text-codec": "^0.2.2", - "@tokenizer/token": "^0.3.0", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "file-type": "^21.3.1", - "media-typer": "^1.1.0", - "strtok3": "^10.3.4", - "token-types": "^6.1.2", - "uint8array-extras": "^1.5.0", - "win-guid": "^0.2.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/music-metadata/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/music-metadata/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==" - }, - "node_modules/mysql2": { - "version": "3.22.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.3.tgz", - "integrity": "sha512-uWWxvZSRvRhtBdh2CdcuK83YcOfPdmEeEYB069bAmPnV93QApDGVPuvCQOLjlh7tYHEWdgQPrn6kosDxHBVLkA==", - "dependencies": { - "aws-ssl-profiles": "^1.1.2", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.2", - "long": "^5.3.2", - "lru.min": "^1.1.4", - "named-placeholders": "^1.1.6", - "sql-escaper": "^1.3.3" - }, - "engines": { - "node": ">= 8.0" - }, - "peerDependencies": { - "@types/node": ">= 8" - } - }, - "node_modules/mysql2/node_modules/iconv-lite": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", - "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", - "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "dependencies": { - "lru.min": "^1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, - "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==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/one-time": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", - "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", - "dependencies": { - "fn.name": "1.x.x" - } - }, - "node_modules/openai": { - "version": "6.38.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.38.0.tgz", - "integrity": "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g==", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-queue": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.0.tgz", - "integrity": "sha512-7NED7xhQ74Ngp4JP/2e0VZHp7vSWfJfqeiR92jPgxsz6m0Se4P03YoTKa9dDXyZ3r6P616gUXttrB6nnHYKang==", - "dependencies": { - "eventemitter3": "^5.0.4", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", - "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-expression-matcher": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", - "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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==" - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==" - }, - "node_modules/pg": { - "version": "8.20.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", - "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", - "dependencies": { - "pg-connection-string": "^2.12.0", - "pg-pool": "^3.13.0", - "pg-protocol": "^1.13.0", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.3.0" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", - "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", - "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", - "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", - "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pg/node_modules/pg-connection-string": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", - "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==" - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "dependencies": { - "split2": "^4.1.0" - } - }, - "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, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pino": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", - "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==" - }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "engines": { - "node": ">=10.13.0" - } - }, - "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==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", - "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] - }, - "node_modules/protobufjs": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.3.0.tgz", - "integrity": "sha512-JpJpFaR7yKNb6WqKvJJ1MLbiuIQWQnbUUb06nDtf2/i8YWYYLEfP6xf9BwSJoJQg1wAy61EQB8dssQg64oX4aA==", - "hasInstallScript": true, - "dependencies": { - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" - }, - "node_modules/qified": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", - "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", - "dependencies": { - "hookified": "^2.1.1" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/qified/node_modules/hookified": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", - "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==" - }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/rechoir": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", - "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", - "dependencies": { - "resolve": "^1.20.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "node_modules/resolve": { - "version": "1.22.12", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", - "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "engines": { - "node": ">=8" - } - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "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" - } - ] - }, - "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==", - "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/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" - }, - "node_modules/semver": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", - "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "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==", - "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/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "peer": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "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==", - "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==", - "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==", - "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/socket.io": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", - "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.4.1", - "engine.io": "~6.6.0", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/socket.io-adapter": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", - "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", - "dependencies": { - "debug": "~4.4.1", - "ws": "~8.18.3" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-adapter/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==" - }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "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/socket.io-parser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", - "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.4.1" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io-parser/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==" - }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socket.io/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==" - }, - "node_modules/sonic-boom": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", - "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "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, - "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, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/sql-escaper": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", - "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", - "engines": { - "bun": ">=1.0.0", - "deno": ">=2.0.0", - "node": ">=12.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" - } - }, - "node_modules/stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", - "engines": { - "node": "*" - } - }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "dependencies": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - } - }, - "node_modules/stream-browserify/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string_decoder/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "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==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strnum": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", - "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ] - }, - "node_modules/strtok3": { - "version": "10.3.5", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", - "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "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==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/tarn": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", - "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/tildify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", - "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", - "engines": { - "node": ">=8" - } - }, - "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, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/token-types": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", - "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/tree-kill": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", - "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true, - "bin": { - "tree-kill": "cli.js" - } - }, - "node_modules/triple-beam": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", - "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/ts-node": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@tsconfig/node10": "^1.0.7", - "@tsconfig/node12": "^1.0.7", - "@tsconfig/node14": "^1.0.0", - "@tsconfig/node16": "^1.0.2", - "acorn": "^8.4.1", - "acorn-walk": "^8.1.1", - "arg": "^4.1.0", - "create-require": "^1.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } - } - }, - "node_modules/ts-node-dev": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", - "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", - "dev": true, - "dependencies": { - "chokidar": "^3.5.1", - "dynamic-dedupe": "^0.3.0", - "minimist": "^1.2.6", - "mkdirp": "^1.0.4", - "resolve": "^1.0.0", - "rimraf": "^2.6.1", - "source-map-support": "^0.5.12", - "tree-kill": "^1.2.2", - "ts-node": "^10.4.0", - "tsconfig": "^7.0.0" - }, - "bin": { - "ts-node-dev": "lib/bin.js", - "tsnd": "lib/bin.js" - }, - "engines": { - "node": ">=0.8.0" - }, - "peerDependencies": { - "node-notifier": "*", - "typescript": "*" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "node_modules/ts-node-dev/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tsconfig": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", - "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", - "dev": true, - "dependencies": { - "@types/strip-bom": "^3.0.0", - "@types/strip-json-comments": "0.0.30", - "strip-bom": "^3.0.0", - "strip-json-comments": "^2.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "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==" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/whatsapp-rust-bridge": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/whatsapp-rust-bridge/-/whatsapp-rust-bridge-0.5.4.tgz", - "integrity": "sha512-yYO1qSs0Fe7tGtnxOFHomocUD6IZtoAgmA4oDFyGIRZ67D3QZk3w7swA6XXFXNQngiyrg2k7tul6IrM3eUFh7A==" - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, - "node_modules/which-typed-array": { - "version": "1.1.20", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", - "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "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/win-guid": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz", - "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==" - }, - "node_modules/winston": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", - "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", - "dependencies": { - "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.8", - "async": "^3.2.3", - "is-stream": "^2.0.0", - "logform": "^2.7.0", - "one-time": "^1.0.0", - "readable-stream": "^3.4.0", - "safe-stable-stringify": "^2.3.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.9.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-daily-rotate-file": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/winston-daily-rotate-file/-/winston-daily-rotate-file-5.0.0.tgz", - "integrity": "sha512-JDjiXXkM5qvwY06733vf09I2wnMXpZEhxEVOSPenZMii+g7pcDcTBt2MRugnoi8BwVSuCT2jfRXBUy+n1Zz/Yw==", - "dependencies": { - "file-stream-rotator": "^0.6.1", - "object-hash": "^3.0.0", - "triple-beam": "^1.4.1", - "winston-transport": "^4.7.0" - }, - "engines": { - "node": ">=8" - }, - "peerDependencies": { - "winston": "^3" - } - }, - "node_modules/winston-transport": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", - "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", - "dependencies": { - "logform": "^2.7.0", - "readable-stream": "^3.6.2", - "triple-beam": "^1.3.0" - }, - "engines": { - "node": ">= 12.0.0" - } - }, - "node_modules/winston-transport/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/winston/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xml-naming": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", - "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yn": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "dev": true, - "engines": { - "node": ">=6" - } + "name": "clube67-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clube67-backend", + "version": "1.0.0", + "dependencies": { + "bcryptjs": "^2.4.3", + "compression": "^1.7.4", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.0", + "express-rate-limit": "^7.4.0", + "express-validator": "^7.2.0", + "helmet": "^7.1.0", + "hpp": "^0.2.3", + "ioredis": "^5.4.1", + "jsonwebtoken": "^9.0.2", + "knex": "^3.1.0", + "multer": "^1.4.5-lts.1", + "mysql2": "^3.11.0", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "qrcode": "^1.5.4", + "socket.io": "^4.7.5", + "uuid": "^10.0.0", + "winston": "^3.14.0", + "xss-clean": "^0.1.4" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/hpp": "^0.2.6", + "@types/jsonwebtoken": "^9.0.6", + "@types/multer": "^1.4.12", + "@types/node": "^22.0.0", + "@types/passport": "^1.0.16", + "@types/passport-google-oauth20": "^2.0.16", + "@types/qrcode": "^1.5.5", + "@types/uuid": "^10.0.0", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "typescript": "^5.5.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmmirror.com/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.5.0", + "resolved": "https://registry.npmmirror.com/@ioredis/commands/-/commands-1.5.0.tgz", + "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/bcryptjs": { + "version": "2.4.6", + "resolved": "https://registry.npmmirror.com/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", + "integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==", + "dev": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmmirror.com/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmmirror.com/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmmirror.com/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmmirror.com/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmmirror.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/hpp": { + "version": "0.2.7", + "resolved": "https://registry.npmmirror.com/@types/hpp/-/hpp-0.2.7.tgz", + "integrity": "sha512-YSQBkTwZepklRez0wgsljeewMytGNKgBAZR1YbmE0X49+elqkZ+fr/gvB407wL9Dl7a/Kv3W04yJueRmEHytBw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmmirror.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true + }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmmirror.com/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.10", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.10.tgz", + "integrity": "sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/oauth": { + "version": "0.9.6", + "resolved": "https://registry.npmmirror.com/@types/oauth/-/oauth-0.9.6.tgz", + "integrity": "sha512-H9TRCVKBNOhZZmyHLqFt9drPM9l+ShWiqqJijU1B8P3DX3ub84NjxDuy+Hjrz+fEca5Kwip3qPMKNyiLgNJtIA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/passport": { + "version": "1.0.17", + "resolved": "https://registry.npmmirror.com/@types/passport/-/passport-1.0.17.tgz", + "integrity": "sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/passport-google-oauth20": { + "version": "2.0.17", + "resolved": "https://registry.npmmirror.com/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.17.tgz", + "integrity": "sha512-MHNOd2l7gOTCn3iS+wInPQMiukliAUvMpODO3VlXxOiwNEMSyzV7UNvAdqxSN872o8OXx1SqPDVT6tLW74AtqQ==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/passport": "*", + "@types/passport-oauth2": "*" + } + }, + "node_modules/@types/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/@types/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-6//z+4orIOy/g3zx17HyQ71GSRK4bs7Sb+zFasRoc2xzlv7ZCJ+vkDBYFci8U6HY+or6Zy7ajf4mz4rK7nsWJQ==", + "dev": true, + "dependencies": { + "@types/express": "*", + "@types/oauth": "*", + "@types/passport": "*" + } + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmmirror.com/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmmirror.com/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmmirror.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmmirror.com/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmmirror.com/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmmirror.com/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "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.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "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/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==" + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "engines": { + "node": ">=14" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmmirror.com/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmmirror.com/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmmirror.com/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmmirror.com/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmmirror.com/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmmirror.com/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmmirror.com/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmmirror.com/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmmirror.com/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmmirror.com/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express-validator": { + "version": "7.3.1", + "resolved": "https://registry.npmmirror.com/express-validator/-/express-validator-7.3.1.tgz", + "integrity": "sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==", + "dependencies": { + "lodash": "^4.17.21", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmmirror.com/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmmirror.com/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "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.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/helmet": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/helmet/-/helmet-7.2.0.tgz", + "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/hpp": { + "version": "0.2.3", + "resolved": "https://registry.npmmirror.com/hpp/-/hpp-0.2.3.tgz", + "integrity": "sha512-4zDZypjQcxK/8pfFNR7jaON7zEUpXZxz4viyFmqjb3kWNWAHsLEUmWXcdn25c5l76ISvnD6hbOGO97cXUI3Ryw==", + "dependencies": { + "lodash": "^4.17.12", + "type-is": "^1.6.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/ioredis": { + "version": "5.9.2", + "resolved": "https://registry.npmmirror.com/ioredis/-/ioredis-5.9.2.tgz", + "integrity": "sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==", + "dependencies": { + "@ioredis/commands": "1.5.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/knex": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/knex/-/knex-3.1.0.tgz", + "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/knex/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/multer": { + "version": "1.4.5-lts.2", + "resolved": "https://registry.npmmirror.com/multer/-/multer-1.4.5-lts.2.tgz", + "integrity": "sha512-VzGiVigcG9zUAoCNU+xShztrlr1auZOlurXynNvO9GiWD1/mTBbUljOKY+qMeazBqXgRnjzeEgJI/wyjJUHg9A==", + "deprecated": "Multer 1.x is impacted by a number of vulnerabilities, which have been patched in 2.x. You should upgrade to the latest 2.x version.", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/mysql2": { + "version": "3.16.3", + "resolved": "https://registry.npmmirror.com/mysql2/-/mysql2-3.16.3.tgz", + "integrity": "sha512-+3XhQEt4FEFuvGV0JjIDj4eP2OT/oIj/54dYvqhblnSzlfcxVOuj+cd15Xz6hsG4HU1a+A5+BA9gm0618C4z7A==", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.3", + "named-placeholders": "^1.1.6", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.3" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmmirror.com/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmmirror.com/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oauth": { + "version": "0.10.2", + "resolved": "https://registry.npmmirror.com/oauth/-/oauth-0.10.2.tgz", + "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmmirror.com/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/passport": { + "version": "0.7.0", + "resolved": "https://registry.npmmirror.com/passport/-/passport-0.7.0.tgz", + "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", + "dependencies": { + "passport-strategy": "1.x.x", + "pause": "0.0.1", + "utils-merge": "^1.0.1" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-google-oauth20": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz", + "integrity": "sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ==", + "dependencies": { + "passport-oauth2": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/passport-jwt": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/passport-jwt/-/passport-jwt-4.0.1.tgz", + "integrity": "sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==", + "dependencies": { + "jsonwebtoken": "^9.0.0", + "passport-strategy": "^1.0.0" + } + }, + "node_modules/passport-oauth2": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/passport-oauth2/-/passport-oauth2-1.8.0.tgz", + "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==", + "dependencies": { + "base64url": "3.x.x", + "oauth": "0.10.x", + "passport-strategy": "1.x.x", + "uid2": "0.0.x", + "utils-merge": "1.x.x" + }, + "engines": { + "node": ">= 0.4.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/jaredhanson" + } + }, + "node_modules/passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" + }, + "node_modules/pause": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/pause/-/pause-0.0.1.tgz", + "integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==" + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmmirror.com/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/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" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmmirror.com/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmmirror.com/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmmirror.com/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "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.0", + "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "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.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "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/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmmirror.com/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmmirror.com/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmmirror.com/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tarn": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/tarn/-/tarn-3.0.2.tgz", + "integrity": "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmmirror.com/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmmirror.com/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmmirror.com/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uid2": { + "version": "0.0.4", + "resolved": "https://registry.npmmirror.com/uid2/-/uid2-0.0.4.tgz", + "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmmirror.com/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/validator": { + "version": "13.15.26", + "resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.26.tgz", + "integrity": "sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmmirror.com/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmmirror.com/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/winston/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmmirror.com/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "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/xss-clean": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/xss-clean/-/xss-clean-0.1.4.tgz", + "integrity": "sha512-4hArTFHYxrifK9VXOu/zFvwjTXVjKByPi6woUHb1IqxlX0Z4xtFBRjOhTKuYV/uE1VswbYsIh5vUEYp7MmoISQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dependencies": { + "xss-filters": "1.2.7" + } + }, + "node_modules/xss-filters": { + "version": "1.2.7", + "resolved": "https://registry.npmmirror.com/xss-filters/-/xss-filters-1.2.7.tgz", + "integrity": "sha512-KzcmYT/f+YzcYrYRqw6mXxd25BEZCxBQnf+uXTopQDIhrmiaLwO+f+yLsIvvNlPhYvgff8g3igqrBxYh9k8NbQ==" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } } + } } diff --git a/backend/package.json b/backend/package.json index fb27dc2..66d57b4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,53 +1,56 @@ { - "name": "clube67-backend", - "version": "1.0.0", - "main": "../dist/backend/src/index.js", - "scripts": { - "start": "node dist/backend/src/index.js", - "dev": "ts-node-dev --respawn --transpile-only src/index.ts", - "build": "node node_modules/typescript/bin/tsc -p tsconfig.json && cp -r dist/backend/src/infra dist/ 2>/dev/null && cp -r dist/backend/src/modules dist/ 2>/dev/null && cp -r dist/backend/src/config dist/ 2>/dev/null || true" - }, - "dependencies": { - "@aws-sdk/client-s3": "^3.996.0", - "@aws-sdk/lib-storage": "^3.996.0", - "@aws-sdk/s3-request-presigner": "^3.996.0", - "@opentelemetry/api": "^1.9.0", - "@types/qrcode": "^1.5.6", - "aws-sdk": "^2.1693.0", - "baileys": "^7.0.0-rc.9", - "bcryptjs": "^2.4.3", - "compression": "^1.7.4", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-rate-limit": "^7.2.0", - "helmet": "^7.1.0", - "hpp": "^0.2.3", - "ioredis": "^5.4.1", - "jsonwebtoken": "^9.0.3", - "knex": "^3.1.0", - "multer": "^1.4.5-lts.1", - "mysql2": "^3.18.0", - "openai": "^6.22.0", - "pg": "^8.11.5", - "protobufjs": "^8.0.0", - "qrcode": "^1.5.4", - "socket.io": "^4.7.5", - "uuid": "^9.0.1", - "winston": "^3.13.0", - "winston-daily-rotate-file": "^5.0.0" - }, - "devDependencies": { - "@types/bcryptjs": "^2.4.6", - "@types/compression": "^1.7.5", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/jsonwebtoken": "^9.0.6", - "@types/multer": "^1.4.11", - "@types/node": "^20.12.7", - "@types/pg": "^8.11.5", - "@types/uuid": "^9.0.8", - "ts-node-dev": "^2.0.0", - "typescript": "^5.4.5" - } + "name": "clube67-backend", + "version": "1.0.0", + "description": "Clube de Benefícios - Backend API", + "main": "dist/index.js", + "scripts": { + "dev": "ts-node-dev --respawn --transpile-only src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "migrate": "knex migrate:latest --knexfile src/config/knexfile.ts", + "migrate:rollback": "knex migrate:rollback --knexfile src/config/knexfile.ts", + "seed": "knex seed:run --knexfile src/config/knexfile.ts", + "lint": "eslint src --ext .ts" + }, + "dependencies": { + "express": "^4.21.0", + "mysql2": "^3.11.0", + "knex": "^3.1.0", + "jsonwebtoken": "^9.0.2", + "bcryptjs": "^2.4.3", + "cors": "^2.8.5", + "helmet": "^7.1.0", + "express-rate-limit": "^7.4.0", + "multer": "^1.4.5-lts.1", + "dotenv": "^16.4.5", + "winston": "^3.14.0", + "ioredis": "^5.4.1", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "express-validator": "^7.2.0", + "xss-clean": "^0.1.4", + "hpp": "^0.2.3", + "compression": "^1.7.4", + "socket.io": "^4.7.5", + "uuid": "^10.0.0", + "qrcode": "^1.5.4" + }, + "devDependencies": { + "typescript": "^5.5.0", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "@types/express": "^4.17.21", + "@types/jsonwebtoken": "^9.0.6", + "@types/bcryptjs": "^2.4.6", + "@types/cors": "^2.8.17", + "@types/multer": "^1.4.12", + "@types/passport": "^1.0.16", + "@types/passport-google-oauth20": "^2.0.16", + "@types/hpp": "^0.2.6", + "@types/compression": "^1.7.5", + "@types/uuid": "^10.0.0", + "@types/qrcode": "^1.5.5", + "@types/node": "^22.0.0" + } } diff --git a/backend/plugins/whatsapp-v1/.env.example b/backend/plugins/whatsapp-v1/.env.example deleted file mode 100644 index 749cc02..0000000 --- a/backend/plugins/whatsapp-v1/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -PORT=8787 -API_KEY=ACFH4RFOTME4RU50R4FKGNW34LDFG8DSQ -AUTH_FOLDER=auth diff --git a/backend/plugins/whatsapp-v1/.gitignore b/backend/plugins/whatsapp-v1/.gitignore deleted file mode 100644 index 4819c21..0000000 --- a/backend/plugins/whatsapp-v1/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -dist/ -auth/ -.env -*.log -.DS_Store diff --git a/backend/plugins/whatsapp-v1/README.md b/backend/plugins/whatsapp-v1/README.md deleted file mode 100644 index be9cd41..0000000 --- a/backend/plugins/whatsapp-v1/README.md +++ /dev/null @@ -1,115 +0,0 @@ -# rscara - -API em **TypeScript** para múltiplas contas WhatsApp usando [InfiniteAPI](https://github.com/rsalcara/InfiniteAPI) (fork do Baileys), com geração de QR code, gerenciamento de conexões e disparo de componentes especiais (botões, listas, carrossel, enquete). Inclui interface web para conectar números e enviar mensagens. - -## Requisitos - -- Node.js >= 20 -- npm ou yarn - -## Instalação - -```bash -npm install -``` - -## Configuração - -Copie o arquivo de exemplo e ajuste: - -```bash -cp .env.example .env -``` - -Variáveis em `.env`: - -| Variável | Descrição | Padrão | -|-------------|------------------------|--------| -| `PORT` | Porta do servidor | 8787 | -| `API_KEY` | Chave para header `x-api-key` (deixe vazio para desativar) | - | -| `AUTH_FOLDER` | Pasta onde salvar credenciais por instância | auth | - -## Desenvolvimento - -```bash -npm run dev -``` - -## Build e produção - -```bash -npm run build -npm start -``` - -## Interface web - -Com o servidor rodando, acesse **http://localhost:8787**. A página inicial e os arquivos estáticos não exigem API key; apenas as rotas `/v1/*` usam o header `x-api-key` quando `API_KEY` está definida. - -- **Conexões**: listar conexões salvas (clique em Conectar), conectar por nome, ver QR (atualizado automaticamente), listar instâncias ativas com ações (Desconectar, Novo QR, Deletar). Status e QR são atualizados a cada 2 segundos enquanto a aba estiver aberta. -- **Disparos**: escolher instância, colar **lista de mailing** (um número por linha, com DDI), definir **intervalo mínimo e máximo** (em segundos) entre cada envio, escolher tipo de mensagem e preencher os campos (formulários dinâmicos com adicionar/remover). O envio é feito em lote com espera aleatória entre os números. - -## Endpoints - -O header **`x-api-key`** é obrigatório apenas nas rotas `/v1/*` quando `API_KEY` está definida. A rota `/health` e a interface em `/` não exigem key. - -### Instâncias - -| Método | Rota | Descrição | -|--------|------|-----------| -| POST | `/v1/instances` | Cria/conecta instância e retorna QR (body: `{ "instance": "main" }`) | -| GET | `/v1/instances` | Lista instâncias ativas e nomes das conexões salvas (`saved`) | -| GET | `/v1/instances/saved` | Lista apenas nomes das conexões salvas (pastas em `auth/`) | -| GET | `/v1/instances/:name` | Status de uma instância | -| GET | `/v1/instances/:name/qr` | QR em base64 (quando status = qr) | -| POST | `/v1/instances/:name/disconnect` | Desconecta e remove da memória (credenciais ficam em disco) | -| POST | `/v1/instances/:name/logout` | Logout e apaga sessão em disco (próxima conexão gera novo QR) | -| DELETE | `/v1/instances/:name` | Remove instância da memória (fecha socket) | - -### Mensagens (componentes especiais) - -| Método | Rota | Descrição | -|--------|------|-----------| -| POST | `/v1/messages/send_menu` | Menu texto (opções numeradas) | -| POST | `/v1/messages/send_buttons_helpers` | Botões quick reply (até 3) | -| POST | `/v1/messages/send_interactive_helpers` | Botões CTA (URL, Copiar, Ligar) | -| POST | `/v1/messages/send_list_helpers` | Lista dropdown (nativeList) | -| POST | `/v1/messages/send_poll` | Enquete | -| POST | `/v1/messages/send_carousel_helpers` | Carrossel com cards (imagem + botões) | - -Em todos os endpoints de mensagens o body deve incluir **`instance`** (nome da instância) e **`to`** (número no formato `5511999999999`). - -## Exemplo rápido - -1. Subir a API e abrir a interface em http://localhost:8787 (ou criar instância via API): - -```bash -curl -X POST http://localhost:8787/v1/instances \ - -H "Content-Type: application/json" \ - -H "x-api-key: SUA_API_KEY" \ - -d '{"instance": "main"}' -``` - -2. A resposta pode trazer `qr` em base64. Exiba a imagem ou use `GET /v1/instances/main/qr` até conectar. Na interface, o QR e o status são atualizados automaticamente. - -3. Enviar botões: - -```bash -curl -X POST http://localhost:8787/v1/messages/send_buttons_helpers \ - -H "Content-Type: application/json" \ - -H "x-api-key: SUA_API_KEY" \ - -d '{ - "instance": "main", - "to": "553598828503", - "text": "Como posso ajudar?", - "footer": "Atendimento 24h", - "buttons": [ - {"id": "vendas", "text": "Fazer Pedido"}, - {"id": "suporte", "text": "Suporte"} - ] - }' -``` - -## Licença - -MIT. diff --git a/backend/plugins/whatsapp-v1/package-lock.json b/backend/plugins/whatsapp-v1/package-lock.json deleted file mode 100644 index f27933e..0000000 --- a/backend/plugins/whatsapp-v1/package-lock.json +++ /dev/null @@ -1,2922 +0,0 @@ -{ - "name": "rsalcara", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "rsalcara", - "version": "1.0.0", - "dependencies": { - "baileys": "github:rsalcara/InfiniteAPI", - "dotenv": "^16.4.5", - "express": "^4.21.0", - "qrcode": "^1.5.4" - }, - "devDependencies": { - "@types/express": "^4.17.21", - "@types/node": "^22.0.0", - "@types/qrcode": "^1.5.5", - "tsx": "^4.19.0", - "typescript": "^5.6.0" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@borewit/text-codec": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz", - "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@cacheable/memory": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.7.tgz", - "integrity": "sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==", - "dependencies": { - "@cacheable/utils": "^2.3.3", - "@keyv/bigmap": "^1.3.0", - "hookified": "^1.14.0", - "keyv": "^5.5.5" - } - }, - "node_modules/@cacheable/node-cache": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@cacheable/node-cache/-/node-cache-1.7.6.tgz", - "integrity": "sha512-6Omk2SgNnjtxB5f/E6bTIWIt5xhdpx39fGNRQgU9lojvRxU68v+qY+SXXLsp3ZGukqoPjsK21wZ6XABFr/Ge3A==", - "dependencies": { - "cacheable": "^2.3.1", - "hookified": "^1.14.0", - "keyv": "^5.5.5" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@cacheable/utils": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.3.3.tgz", - "integrity": "sha512-JsXDL70gQ+1Vc2W/KUFfkAJzgb4puKwwKehNLuB+HrNKWf91O736kGfxn4KujXCCSuh6mRRL4XEB0PkAFjWS0A==", - "dependencies": { - "hashery": "^1.3.0", - "keyv": "^5.5.5" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", - "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@hapi/boom": { - "version": "9.1.4", - "resolved": "https://registry.npmjs.org/@hapi/boom/-/boom-9.1.4.tgz", - "integrity": "sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==", - "dependencies": { - "@hapi/hoek": "9.x.x" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==" - }, - "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", - "cpu": [ - "ppc64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", - "cpu": [ - "riscv64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", - "cpu": [ - "wasm32" - ], - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.7.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@keyv/bigmap": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", - "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", - "dependencies": { - "hashery": "^1.4.0", - "hookified": "^1.15.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "keyv": "^5.6.0" - } - }, - "node_modules/@keyv/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==" - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" - }, - "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==" - }, - "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==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "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==" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "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==" - }, - "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==" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, - "node_modules/@tokenizer/inflate": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", - "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", - "dependencies": { - "debug": "^4.4.3", - "token-types": "^6.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/@tokenizer/inflate/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@tokenizer/inflate/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==" - }, - "node_modules/@tokenizer/token": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", - "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==" - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true - }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true - }, - "node_modules/@types/node": { - "version": "22.19.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.9.tgz", - "integrity": "sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/qrcode": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", - "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true - }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" - }, - "node_modules/async-mutex": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz", - "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==", - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/baileys": { - "version": "7.0.0-rc.9", - "resolved": "git+ssh://git@github.com/rsalcara/InfiniteAPI.git#7981f89059c499a46ecd7445d9937641a790b69a", - "hasInstallScript": true, - "dependencies": { - "@cacheable/node-cache": "^1.4.0", - "@hapi/boom": "^9.1.3", - "async-mutex": "^0.5.0", - "fflate": "^0.8.2", - "libsignal": "git+https://github.com/whiskeysockets/libsignal-node", - "lru-cache": "^11.1.0", - "music-metadata": "^11.7.0", - "p-queue": "^9.0.0", - "pino": "^9.6", - "prom-client": "^15.1.3", - "protobufjs": "^7.2.4", - "ws": "^8.13.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "audio-decode": "^2.1.3", - "jimp": "^1.6.0", - "link-preview-js": "^3.0.0", - "sharp": "*" - }, - "peerDependenciesMeta": { - "audio-decode": { - "optional": true - }, - "jimp": { - "optional": true - }, - "link-preview-js": { - "optional": true - } - } - }, - "node_modules/bintrees": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz", - "integrity": "sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==" - }, - "node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacheable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.2.tgz", - "integrity": "sha512-w+ZuRNmex9c1TR9RcsxbfTKCjSL0rh1WA5SABbrWprIHeNBdmyQLSYonlDy9gpD+63XT8DgZ/wNh1Smvc9WnJA==", - "dependencies": { - "@cacheable/memory": "^2.0.7", - "@cacheable/utils": "^2.3.3", - "hookified": "^1.15.0", - "keyv": "^5.5.5", - "qified": "^0.6.0" - } - }, - "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==", - "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==", - "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/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "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==" - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" - }, - "node_modules/curve25519-js": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/curve25519-js/-/curve25519-js-0.0.4.tgz", - "integrity": "sha512-axn2UMEnkhyDUPWOwVKBMVIzSQy2ejH2xRGy1wq81dqRwApXfIzfbE3hIX0ZRFBIihf/KDqK158DLwESu4AK1w==" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/dijkstrajs": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", - "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==" - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "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==", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" - }, - "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==" - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "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==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==" - }, - "node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" - }, - "node_modules/file-type": { - "version": "21.3.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.0.tgz", - "integrity": "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==", - "dependencies": { - "@tokenizer/inflate": "^0.4.1", - "strtok3": "^10.3.4", - "token-types": "^6.1.1", - "uint8array-extras": "^1.4.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sindresorhus/file-type?sponsor=1" - } - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "engines": { - "node": ">= 0.6" - } - }, - "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, - "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==", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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==", - "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==", - "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-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.5", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.5.tgz", - "integrity": "sha512-v4/4xAEpBRp6SvCkWhnGCaLkJf9IwWzrsygJPxD/+p2/xPE3C5m2fA9FD0Ry9tG+Rqqq3gBzHSl6y1/T9V/tMQ==", - "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "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==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hashery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.4.0.tgz", - "integrity": "sha512-Wn2i1In6XFxl8Az55kkgnFRiAlIAushzh26PTjL2AKtQcEfXrcLa7Hn5QOWGZEf3LU057P9TwwZjFyxfS1VuvQ==", - "dependencies": { - "hookified": "^1.14.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hookified": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", - "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==" - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "node": ">= 0.10" - } - }, - "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==", - "engines": { - "node": ">=8" - } - }, - "node_modules/keyv": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", - "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", - "peer": true, - "dependencies": { - "@keyv/serialize": "^1.1.1" - } - }, - "node_modules/libsignal": { - "name": "@whiskeysockets/libsignal-node", - "version": "2.0.1", - "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67", - "dependencies": { - "curve25519-js": "^0.0.4", - "protobufjs": "6.8.8" - } - }, - "node_modules/libsignal/node_modules/@types/node": { - "version": "10.17.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", - "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==" - }, - "node_modules/libsignal/node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node_modules/libsignal/node_modules/protobufjs": { - "version": "6.8.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", - "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.0", - "@types/node": "^10.1.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "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==" - }, - "node_modules/lru-cache": { - "version": "11.2.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.5.tgz", - "integrity": "sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==", - "engines": { - "node": "20 || >=22" - } - }, - "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==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "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==", - "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==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/music-metadata": { - "version": "11.11.2", - "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.11.2.tgz", - "integrity": "sha512-tJx+lsDg1bGUOxojKKj12BIvccBBUcVa6oWrvOchCF0WAQ9E5t/hK35ILp1z3wWrUSYtgg57LfRbvVMkxGIyzA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - }, - { - "type": "buymeacoffee", - "url": "https://buymeacoffee.com/borewit" - } - ], - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "content-type": "^1.0.5", - "debug": "^4.4.3", - "file-type": "^21.3.0", - "media-typer": "^1.1.0", - "strtok3": "^10.3.4", - "token-types": "^6.1.2", - "uint8array-extras": "^1.5.0", - "win-guid": "^0.2.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/music-metadata/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/music-metadata/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==" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "engines": { - "node": ">= 0.6" - } - }, - "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==", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-queue": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz", - "integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==", - "dependencies": { - "eventemitter3": "^5.0.1", - "p-timeout": "^7.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-timeout": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", - "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" - }, - "node_modules/pino": { - "version": "9.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", - "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", - "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==" - }, - "node_modules/pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] - }, - "node_modules/prom-client": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", - "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", - "dependencies": { - "@opentelemetry/api": "^1.4.0", - "tdigest": "^0.1.1" - }, - "engines": { - "node": "^16 || ^18 || >=20" - } - }, - "node_modules/protobufjs": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", - "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/node": ">=13.7.0", - "long": "^5.0.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/qified": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/qified/-/qified-0.6.0.tgz", - "integrity": "sha512-tsSGN1x3h569ZSU1u6diwhltLyfUWDp3YbFHedapTmpBl0B3P6U3+Qptg7xu+v+1io1EwhdPyyRHYbEw0KN2FA==", - "dependencies": { - "hookified": "^1.14.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "engines": { - "node": ">= 12.13.0" - } - }, - "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==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "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, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "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" - } - ] - }, - "node_modules/safe-stable-stringify": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", - "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, - "peer": true, - "dependencies": { - "@img/colour": "^1.0.0", - "detect-libc": "^2.1.2", - "semver": "^7.7.3" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "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.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "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==", - "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==", - "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/sonic-boom": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz", - "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "engines": { - "node": ">= 0.8" - } - }, - "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==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "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==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strtok3": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz", - "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==", - "dependencies": { - "@tokenizer/token": "^0.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "node_modules/tdigest": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/tdigest/-/tdigest-0.1.2.tgz", - "integrity": "sha512-+G0LLgjjo9BZX2MfdvPfH+MKLCrxlXSYec5DaPYP1fe6Iyhf0/fSmJ0bFiZ1F8BT6cGXl2LpltQptzjXKWEkKA==", - "dependencies": { - "bintrees": "1.0.2" - } - }, - "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", - "dependencies": { - "real-require": "^0.2.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/token-types": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", - "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", - "dependencies": { - "@borewit/text-codec": "^0.2.1", - "@tokenizer/token": "^0.3.0", - "ieee754": "^1.2.1" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Borewit" - } - }, - "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==" - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/uint8array-extras": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", - "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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==" - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which-module": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==" - }, - "node_modules/win-guid": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/win-guid/-/win-guid-0.2.1.tgz", - "integrity": "sha512-gEIQU4mkgl2OPeoNrWflcJFJ3Ae2BPd4eCsHHA/XikslkIVms/nHhvnvzIZV7VLmBvtFlDOzLt9rrZT+n6D67A==" - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "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/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==" - }, - "node_modules/yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dependencies": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "engines": { - "node": ">=6" - } - } - } -} diff --git a/backend/plugins/whatsapp-v1/package.json b/backend/plugins/whatsapp-v1/package.json deleted file mode 100644 index 66ad3ad..0000000 --- a/backend/plugins/whatsapp-v1/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "rsalcara", - "version": "1.0.0", - "description": "API Node.js para múltiplas contas WhatsApp com InfiniteAPI (Baileys) e disparo de componentes especiais", - "type": "module", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx watch src/index.ts" - }, - "engines": { - "node": ">=20.0.0" - }, - "dependencies": { - "express": "^4.21.0", - "qrcode": "^1.5.4", - "dotenv": "^16.4.5", - "baileys": "github:rsalcara/InfiniteAPI" - }, - "devDependencies": { - "typescript": "^5.6.0", - "tsx": "^4.19.0", - "@types/node": "^22.0.0", - "@types/express": "^4.17.21", - "@types/qrcode": "^1.5.5" - } -} diff --git a/backend/plugins/whatsapp-v1/public/app.js b/backend/plugins/whatsapp-v1/public/app.js deleted file mode 100644 index 4a7e588..0000000 --- a/backend/plugins/whatsapp-v1/public/app.js +++ /dev/null @@ -1,628 +0,0 @@ -(function () { - const API = ''; - function headers() { - const h = { 'Content-Type': 'application/json' }; - const key = document.getElementById('apiKey').value.trim() || localStorage.getItem('rscara_api_key'); - if (key) { - h['x-api-key'] = key; - localStorage.setItem('rscara_api_key', key); - } - return h; - } - - function show(el, visible) { - el.classList.toggle('hidden', !visible); - } - - // Tabs - document.querySelectorAll('.tab').forEach((tab) => { - tab.addEventListener('click', () => { - document.querySelectorAll('.tab').forEach((t) => t.classList.remove('active')); - document.querySelectorAll('.panel').forEach((p) => p.classList.remove('active')); - tab.classList.add('active'); - document.getElementById(tab.getAttribute('data-tab')).classList.add('active'); - }); - }); - - // Tipo de disparo - const dispatchForms = { - menu: document.getElementById('formMenu'), - buttons: document.getElementById('formButtons'), - interactive: document.getElementById('formInteractive'), - list: document.getElementById('formList'), - poll: document.getElementById('formPoll'), - carousel: document.getElementById('formCarousel'), - }; - document.getElementById('dispatchType').addEventListener('change', () => { - const type = document.getElementById('dispatchType').value; - Object.values(dispatchForms).forEach((f) => f && f.classList.add('hidden')); - if (dispatchForms[type]) dispatchForms[type].classList.remove('hidden'); - if (type === 'list' && !document.getElementById('listSectionsList').querySelector('.block-section')) addListSection(); - if (type === 'carousel' && !document.getElementById('carouselCardsList').querySelector('.block-section')) addCarouselCard(); - }); - dispatchForms.menu.classList.remove('hidden'); - - // Instância que estamos conectando (para atualizar QR e status em tempo real) - let connectingInstanceName = null; - - // --- Conexões: listar salvas e conectar ao clicar --- - function renderSavedList(saved) { - const ul = document.getElementById('savedList'); - if (!saved || saved.length === 0) { - ul.innerHTML = '
  • Nenhuma conexão salva. Conecte uma vez por nome e ela aparecerá aqui.
  • '; - return; - } - ul.innerHTML = saved - .map( - (name) => - `
  • - ${name} -
    - - -
    -
  • ` - ) - .join(''); - ul.querySelectorAll('[data-connect-name]').forEach((btn) => { - btn.addEventListener('click', () => { - const name = btn.getAttribute('data-connect-name'); - document.getElementById('connectInstanceSelect').value = name; - document.getElementById('instanceName').value = name; - connectNewNameRow.style.display = 'none'; - doConnect(name); - }); - }); - ul.querySelectorAll('[data-delete-saved-name]').forEach((btn) => { - btn.addEventListener('click', async () => { - const name = btn.getAttribute('data-delete-saved-name'); - if (!name || !confirm(`Excluir a conexão salva "${name}"? Será necessário escanear o QR de novo para conectar.`)) return; - try { - const res = await fetch(`${API}/v1/instances/${encodeURIComponent(name)}/logout`, { - method: 'POST', - headers: headers(), - }); - const data = await res.json(); - if (data.ok) refreshInstanceList(); - } catch (_) { - refreshInstanceList(); - } - }); - }); - } - - async function doConnect(name) { - connectingInstanceName = name; - const statusEl = document.getElementById('connectStatus'); - const qrContainer = document.getElementById('qrContainer'); - const qrImage = document.getElementById('qrImage'); - show(statusEl, false); - try { - const res = await fetch(`${API}/v1/instances`, { - method: 'POST', - headers: headers(), - body: JSON.stringify({ instance: name }), - }); - const data = await res.json(); - if (!res.ok) { - statusEl.textContent = data.error || 'Erro ao conectar'; - statusEl.className = 'status error'; - show(statusEl, true); - connectingInstanceName = null; - return; - } - if (data.qr) { - qrImage.src = data.qr; - show(qrContainer, true); - statusEl.textContent = 'Escaneie o QR no WhatsApp.'; - statusEl.className = 'status success'; - } else if (data.status === 'connected') { - show(qrContainer, false); - statusEl.textContent = 'Conectado.'; - statusEl.className = 'status success'; - connectingInstanceName = null; - } else { - statusEl.textContent = 'Aguardando QR...'; - statusEl.className = 'status'; - show(qrContainer, false); - } - show(statusEl, true); - refreshInstanceList(); - } catch (e) { - statusEl.textContent = e.message || 'Erro de rede'; - statusEl.className = 'status error'; - show(statusEl, true); - connectingInstanceName = null; - } - } - - const connectInstanceSelect = document.getElementById('connectInstanceSelect'); - const connectNewNameRow = document.getElementById('connectNewNameRow'); - - connectInstanceSelect.addEventListener('change', () => { - const isNew = connectInstanceSelect.value === ''; - connectNewNameRow.style.display = isNew ? '' : 'none'; - }); - - document.getElementById('btnConnect').addEventListener('click', () => { - const selected = connectInstanceSelect.value; - const name = selected ? selected : (document.getElementById('instanceName').value.trim() || 'main'); - doConnect(name); - }); - - async function fetchQrAndShow(name, qrImage, qrContainer) { - try { - const res = await fetch(`${API}/v1/instances/${encodeURIComponent(name)}/qr`, { headers: headers() }); - const data = await res.json(); - if (data.qr) { - qrImage.src = data.qr; - show(qrContainer, true); - } - } catch (_) {} - } - - function renderInstanceList(list) { - const ul = document.getElementById('instanceList'); - if (!list.length) { - ul.innerHTML = '
  • Nenhuma instância ativa.
  • '; - return; - } - ul.innerHTML = list - .map( - (i) => - `
  • - ${i.instance} - ${i.status} -
    - ${i.status === 'qr' ? `` : ''} - ${i.status === 'connected' ? `` : ''} - - -
    -
  • ` - ) - .join(''); - ul.querySelectorAll('[data-action]').forEach((btn) => { - btn.addEventListener('click', async () => { - const action = btn.getAttribute('data-action'); - const name = btn.getAttribute('data-name'); - if (!name) return; - const base = `${API}/v1/instances/${encodeURIComponent(name)}`; - try { - if (action === 'qr') { - const res = await fetch(`${base}/qr`, { headers: headers() }); - const data = await res.json(); - if (data.qr) { - document.getElementById('qrImage').src = data.qr; - document.getElementById('instanceName').value = name; - show(document.getElementById('qrContainer'), true); - show(document.getElementById('connectStatus'), false); - } - } else if (action === 'disconnect') { - await fetch(`${base}/disconnect`, { method: 'POST', headers: headers() }); - refreshInstanceList(); - } else if (action === 'logout') { - await fetch(`${base}/logout`, { method: 'POST', headers: headers() }); - refreshInstanceList(); - } else if (action === 'delete') { - await fetch(base, { method: 'DELETE', headers: headers() }); - refreshInstanceList(); - } - } catch (_) {} - refreshInstanceList(); - }); - }); - } - - function updateConnectSelect(saved) { - const sel = document.getElementById('connectInstanceSelect'); - const current = sel.value; - const options = ['— Nova conexão —', ...(saved || [])]; - sel.innerHTML = '' + - (saved || []).map((n) => ``).join(''); - connectNewNameRow.style.display = sel.value === '' ? '' : 'none'; - } - - async function refreshInstanceList() { - const statusEl = document.getElementById('connectStatus'); - const qrContainer = document.getElementById('qrContainer'); - const qrImage = document.getElementById('qrImage'); - try { - const res = await fetch(`${API}/v1/instances`, { headers: headers() }); - const data = await res.json(); - if (data.saved) { - renderSavedList(data.saved); - updateConnectSelect(data.saved); - } else { - renderSavedList([]); - updateConnectSelect([]); - } - if (data.instances) { - renderInstanceList(data.instances); - const sel = document.getElementById('dispatchInstance'); - const current = sel.value; - const names = [...new Set([...data.instances.map((i) => i.instance), ...(data.saved || [])])]; - sel.innerHTML = names.map((n) => ``).join(''); - if (!names.includes(current)) sel.selectedIndex = 0; - - // Atualização ativa: se estamos conectando uma instância, atualizar QR e status - if (connectingInstanceName) { - const inst = data.instances.find((i) => i.instance === connectingInstanceName); - if (inst) { - if (inst.status === 'qr') { - try { - const qrRes = await fetch(`${API}/v1/instances/${encodeURIComponent(connectingInstanceName)}/qr`, { headers: headers() }); - const qrData = await qrRes.json(); - if (qrData.qr) { - qrImage.src = qrData.qr; - show(qrContainer, true); - statusEl.textContent = 'Escaneie o QR no WhatsApp.'; - statusEl.className = 'status success'; - show(statusEl, true); - } - } catch (_) {} - } else if (inst.status === 'connected') { - show(qrContainer, false); - statusEl.textContent = 'Conectado.'; - statusEl.className = 'status success'; - show(statusEl, true); - connectingInstanceName = null; - } else if (inst.status === 'disconnected') { - statusEl.textContent = 'Desconectado. Clique em Conectar novamente.'; - statusEl.className = 'status error'; - show(statusEl, true); - } - } - } - } - } catch (_) { - renderSavedList([]); - renderInstanceList([]); - updateConnectSelect([]); - } - } - - document.getElementById('btnRefreshList').addEventListener('click', refreshInstanceList); - refreshInstanceList(); - - // Polling ativo: atualizar lista, QR e status a cada 2s quando a aba Conexões estiver visível - setInterval(() => { - if (document.getElementById('conexoes').classList.contains('active')) { - refreshInstanceList(); - } - }, 2000); - - // --- Formulários dinâmicos (add/remove e montagem do payload) --- - function addRow(containerId, html, removeClass) { - const container = document.getElementById(containerId); - if (!container) return; - const div = document.createElement('div'); - div.className = removeClass || 'item-row'; - div.innerHTML = html + (removeClass ? '' : ' '); - const removeBtn = div.querySelector('.btn-remove'); - if (removeBtn) removeBtn.addEventListener('click', () => div.remove()); - container.appendChild(div); - } - - function addMenuOption() { - addRow('menuOptionsList', ''); - } - function addButtonRow() { - addRow('buttonsList', ''); - } - function addInteractiveRow() { - addRow( - 'interactiveList', - ` - - ` - ); - } - function addPollOption() { - addRow('pollOptionsList', ''); - } - - function addListSection() { - const container = document.getElementById('listSectionsList'); - const block = document.createElement('div'); - block.className = 'block-section'; - block.innerHTML = ` -
    Seção
    - -
    - - - `; - block.querySelector('.add-row-in-section').addEventListener('click', () => { - const row = document.createElement('div'); - row.className = 'item-row'; - row.innerHTML = ` - - - - - `; - row.querySelector('.btn-remove').onclick = () => row.remove(); - block.querySelector('.section-rows').appendChild(row); - }); - block.querySelector('.btn-remove-block').onclick = () => block.remove(); - container.appendChild(block); - } - - function addCarouselCard() { - const container = document.getElementById('carouselCardsList'); - const block = document.createElement('div'); - block.className = 'block-section'; - block.innerHTML = ` -
    Card
    -
    -
    -
    -
    -
    - - - `; - block.querySelector('.add-card-btn').addEventListener('click', () => { - const row = document.createElement('div'); - row.className = 'item-row'; - row.innerHTML = ` - - - - `; - row.querySelector('.btn-remove').onclick = () => row.remove(); - block.querySelector('.card-buttons').appendChild(row); - }); - block.querySelector('.btn-remove-block').onclick = () => block.remove(); - container.appendChild(block); - } - - document.querySelectorAll('.add-item').forEach((btn) => { - btn.addEventListener('click', () => { - const forId = btn.getAttribute('data-for'); - if (forId === 'menuOptions') addMenuOption(); - else if (forId === 'buttons') addButtonRow(); - else if (forId === 'interactive') addInteractiveRow(); - else if (forId === 'listSections') addListSection(); - else if (forId === 'pollOptions') addPollOption(); - else if (forId === 'carouselCards') addCarouselCard(); - }); - }); - - // Inicializar um item vazio por tipo - addMenuOption(); - addButtonRow(); - addInteractiveRow(); - addPollOption(); - - // Coletar dados dos formulários e montar payload - function getMenuPayload() { - const options = []; - document.querySelectorAll('#menuOptionsList .item-row input[data-field="opt"]').forEach((inp) => { - const v = inp.value.trim(); - if (v) options.push(v); - }); - return { - url: '/v1/messages/send_menu', - body: { - instance: document.getElementById('dispatchInstance').value, - to: document.getElementById('dispatchTo').value.trim(), - title: document.getElementById('menuTitle').value.trim() || 'Menu', - text: document.getElementById('menuText').value.trim() || 'Escolha uma opção:', - options: options.length ? options : ['Opção 1'], - footer: document.getElementById('menuFooter').value.trim() || undefined, - }, - }; - } - function getButtonsPayload() { - const buttons = []; - document.querySelectorAll('#buttonsList .item-row').forEach((row) => { - const id = row.querySelector('[data-field="id"]')?.value?.trim(); - const text = row.querySelector('[data-field="text"]')?.value?.trim(); - if (id && text) buttons.push({ id, text }); - }); - return { - url: '/v1/messages/send_buttons_helpers', - body: { - instance: document.getElementById('dispatchInstance').value, - to: document.getElementById('dispatchTo').value.trim(), - text: document.getElementById('buttonsText').value.trim() || 'Escolha:', - footer: document.getElementById('buttonsFooter').value.trim() || undefined, - buttons: buttons.length ? buttons.slice(0, 3) : [{ id: 'btn1', text: 'Opção 1' }], - }, - }; - } - function getInteractivePayload() { - const buttons = []; - document.querySelectorAll('#interactiveList .item-row').forEach((row) => { - const type = row.querySelector('[data-field="type"]')?.value || 'url'; - const text = row.querySelector('[data-field="text"]')?.value?.trim(); - const extra = row.querySelector('[data-field="extra"]')?.value?.trim(); - if (!text || !extra) return; - const btn = { type, text }; - if (type === 'url') btn.url = extra; - else if (type === 'copy') btn.copyCode = extra; - else if (type === 'call') btn.phoneNumber = extra; - buttons.push(btn); - }); - return { - url: '/v1/messages/send_interactive_helpers', - body: { - instance: document.getElementById('dispatchInstance').value, - to: document.getElementById('dispatchTo').value.trim(), - text: document.getElementById('interactiveText').value.trim() || 'Confira:', - footer: document.getElementById('interactiveFooter').value.trim() || undefined, - buttons, - }, - }; - } - function getListPayload() { - const sections = []; - document.querySelectorAll('#listSectionsList .block-section').forEach((block) => { - const title = block.querySelector('.section-title')?.value?.trim() || 'Seção'; - const rows = []; - block.querySelectorAll('.section-rows .item-row').forEach((row) => { - const id = row.querySelector('[data-field="id"]')?.value?.trim(); - const titleR = row.querySelector('[data-field="title"]')?.value?.trim(); - const desc = row.querySelector('[data-field="desc"]')?.value?.trim(); - if (id && titleR) rows.push({ id, title: titleR, description: desc || '' }); - }); - if (rows.length) sections.push({ title, rows }); - }); - return { - url: '/v1/messages/send_list_helpers', - body: { - instance: document.getElementById('dispatchInstance').value, - to: document.getElementById('dispatchTo').value.trim(), - text: document.getElementById('listText').value.trim() || 'Escolha:', - buttonText: document.getElementById('listButtonText').value.trim() || 'Ver opções', - footer: document.getElementById('listFooter').value.trim() || undefined, - sections: sections.length ? sections : [{ title: 'Opções', rows: [{ id: 'opt1', title: 'Opção 1', description: '' }] }], - }, - }; - } - function getPollPayload() { - const options = []; - document.querySelectorAll('#pollOptionsList .item-row input[data-field="opt"]').forEach((inp) => { - const v = inp.value.trim(); - if (v) options.push(v); - }); - return { - url: '/v1/messages/send_poll', - body: { - instance: document.getElementById('dispatchInstance').value, - to: document.getElementById('dispatchTo').value.trim(), - name: document.getElementById('pollName').value.trim() || 'Enquete', - options: options.length >= 2 ? options : ['Sim', 'Não'], - selectableCount: parseInt(document.getElementById('pollSelectable').value, 10) || 1, - }, - }; - } - function getCarouselPayload() { - const cards = []; - document.querySelectorAll('#carouselCardsList .block-section').forEach((block) => { - const title = block.querySelector('[data-field="title"]')?.value?.trim(); - const body = block.querySelector('[data-field="body"]')?.value?.trim(); - const footer = block.querySelector('[data-field="footer"]')?.value?.trim(); - const imageUrl = block.querySelector('[data-field="imageUrl"]')?.value?.trim(); - const buttons = []; - block.querySelectorAll('.card-buttons .item-row').forEach((row) => { - const id = row.querySelector('[data-field="id"]')?.value?.trim(); - const text = row.querySelector('[data-field="text"]')?.value?.trim(); - if (id && text) buttons.push({ id, text }); - }); - cards.push({ - title: title || '', - body: body || '', - footer: footer || undefined, - imageUrl: imageUrl || undefined, - buttons: buttons.length ? buttons : [{ id: 'btn1', text: 'Ver' }], - }); - }); - return { - url: '/v1/messages/send_carousel_helpers', - body: { - instance: document.getElementById('dispatchInstance').value, - to: document.getElementById('dispatchTo').value.trim(), - text: document.getElementById('carouselText').value.trim() || undefined, - footer: document.getElementById('carouselFooter').value.trim() || undefined, - cards: cards.length ? cards : [{ title: 'Card', body: '', buttons: [{ id: 'b1', text: 'Botão' }] }], - }, - }; - } - - /** - * Lê destinatários do campo e normaliza: aceita +55, espaços, traços, vírgulas etc. - * Ex: "+55 35 9882-8503," vira "553598828503". - */ - function getRecipients() { - const raw = document.getElementById('dispatchTo').value.trim(); - if (!raw) return []; - return raw - .split(/[\r\n,;]+/) - .map((s) => s.replace(/\D/g, '')) - .filter((n) => n.length >= 10); - } - - function delayMs(minSec, maxSec) { - const min = Math.max(0, Number(minSec) || 0); - const max = Math.max(min, Number(maxSec) || min); - const sec = min + Math.random() * (max - min); - return Math.round(sec * 1000); - } - - document.getElementById('btnSend').addEventListener('click', async () => { - const recipients = getRecipients(); - const resultEl = document.getElementById('sendResult'); - const btnSend = document.getElementById('btnSend'); - if (!recipients.length) { - resultEl.textContent = 'Informe ao menos um número (um por linha, com DDI).'; - resultEl.className = 'result error'; - show(resultEl, true); - return; - } - const type = document.getElementById('dispatchType').value; - let payload; - switch (type) { - case 'menu': payload = getMenuPayload(); break; - case 'buttons': payload = getButtonsPayload(); break; - case 'interactive': payload = getInteractivePayload(); break; - case 'list': payload = getListPayload(); break; - case 'poll': payload = getPollPayload(); break; - case 'carousel': payload = getCarouselPayload(); break; - default: - resultEl.textContent = 'Tipo não implementado.'; - resultEl.className = 'result error'; - show(resultEl, true); - return; - } - if (type === 'interactive' && (!payload.body.buttons || payload.body.buttons.length === 0)) { - resultEl.textContent = 'Adicione ao menos um botão CTA.'; - resultEl.className = 'result error'; - show(resultEl, true); - return; - } - - const delayMin = document.getElementById('dispatchDelayMin').value; - const delayMax = document.getElementById('dispatchDelayMax').value; - let sent = 0; - let failed = 0; - btnSend.disabled = true; - show(resultEl, true); - resultEl.className = 'result'; - - for (let i = 0; i < recipients.length; i++) { - const to = recipients[i]; - payload.body.to = to; - resultEl.textContent = `Enviando ${i + 1}/${recipients.length}... (${to})`; - try { - const res = await fetch(`${API}${payload.url}`, { - method: 'POST', - headers: headers(), - body: JSON.stringify(payload.body), - }); - const data = await res.json(); - if (res.ok) { - sent++; - } else { - failed++; - } - } catch (_) { - failed++; - } - if (i < recipients.length - 1) { - const wait = delayMs(delayMin, delayMax); - resultEl.textContent = `Aguardando ${wait / 1000}s antes do próximo... (${i + 1}/${recipients.length})`; - await new Promise((r) => setTimeout(r, wait)); - } - } - - resultEl.textContent = `Concluído: ${sent} enviados${failed ? `, ${failed} falhas` : ''}.`; - resultEl.className = failed === 0 ? 'result success' : failed === recipients.length ? 'result error' : 'result'; - btnSend.disabled = false; - }); - - const savedKey = localStorage.getItem('rscara_api_key'); - if (savedKey) document.getElementById('apiKey').placeholder = '••••••••'; -})(); diff --git a/backend/plugins/whatsapp-v1/public/index.html b/backend/plugins/whatsapp-v1/public/index.html deleted file mode 100644 index f3af184..0000000 --- a/backend/plugins/whatsapp-v1/public/index.html +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - rsalcara — WhatsApp API - - - - - - -
    -
    -

    rsalcara

    -

    WhatsApp · Múltiplas contas e disparos

    -
    - - -
    -
    - - - -
    -
    -
    -

    Conexões salvas

    -

    Clique em Conectar para abrir a sessão (QR se necessário).

    -
      -
      -
      -

      Conectar por nome

      -
      - - -
      -
      - - -
      - - - -
      -
      -

      Instâncias ativas

      -
        - -
        -
        - -
        -
        -
        - - -
        -
        - - - Use um número por linha. Para um único número, digite uma linha. -
        -
        - -
        - a - -
        - Tempo aleatório entre cada envio (evita bloqueio). -
        -
        - - -
        - - -
        -
        -
        -
        - - - -
        -
        -
        - - - - - - - - - - - - - - - - - - -
        -
        -
        -
        - - - diff --git a/backend/plugins/whatsapp-v1/public/styles.css b/backend/plugins/whatsapp-v1/public/styles.css deleted file mode 100644 index d053bc2..0000000 --- a/backend/plugins/whatsapp-v1/public/styles.css +++ /dev/null @@ -1,499 +0,0 @@ -:root { - --bg: #0f0f12; - --bg-card: #18181c; - --bg-input: #222228; - --border: #2d2d35; - --text: #e4e4e7; - --text-muted: #71717a; - --accent: #22c55e; - --accent-hover: #16a34a; - --danger: #ef4444; - --radius: 12px; - --font: 'DM Sans', system-ui, sans-serif; - --font-mono: 'JetBrains Mono', monospace; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-height: 100vh; - font-family: var(--font); - background: var(--bg); - color: var(--text); - line-height: 1.5; -} - -.app { - max-width: 720px; - margin: 0 auto; - padding: 1.5rem; -} - -.header { - text-align: center; - margin-bottom: 1.5rem; -} - -.logo { - font-size: 1.75rem; - font-weight: 700; - letter-spacing: -0.02em; - margin: 0 0 0.25rem 0; -} - -.tagline { - color: var(--text-muted); - font-size: 0.9rem; - margin: 0 0 1rem 0; -} - -.api-key-wrap { - display: inline-flex; - flex-direction: column; - align-items: flex-start; - gap: 0.35rem; -} - -.api-key-wrap label { - font-size: 0.75rem; - color: var(--text-muted); -} - -#apiKey { - width: 220px; - padding: 0.5rem 0.75rem; - border: 1px solid var(--border); - border-radius: 8px; - background: var(--bg-input); - color: var(--text); - font-family: var(--font-mono); - font-size: 0.85rem; -} - -#apiKey:focus { - outline: none; - border-color: var(--accent); -} - -.tabs { - display: flex; - gap: 0.5rem; - margin-bottom: 1.5rem; -} - -.tab { - padding: 0.6rem 1.2rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--bg-card); - color: var(--text); - font-family: var(--font); - font-size: 0.95rem; - cursor: pointer; - transition: background 0.15s, border-color 0.15s; -} - -.tab:hover { - background: var(--bg-input); -} - -.tab.active { - background: var(--accent); - border-color: var(--accent); - color: #0a0a0c; -} - -.panel { - display: none; -} - -.panel.active { - display: block; -} - -.card { - background: var(--bg-card); - border: 1px solid var(--border); - border-radius: var(--radius); - padding: 1.25rem; - margin-bottom: 1rem; -} - -.card h2 { - font-size: 1.1rem; - font-weight: 600; - margin: 0 0 0.5rem 0; -} - -.card-hint { - font-size: 0.85rem; - color: var(--text-muted); - margin: 0 0 0.75rem 0; -} - -.saved-list { - list-style: none; - padding: 0; - margin: 0 0 0.75rem 0; -} - -.saved-list li { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.5rem 0; - border-bottom: 1px solid var(--border); -} - -.saved-list li:last-child { border-bottom: none; } - -.saved-list .saved-item-row { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 0.5rem; -} - -.saved-list .saved-item-actions { - display: flex; - align-items: center; - gap: 0.4rem; -} - -.saved-list .btn-connect-saved { - padding: 0.4rem 0.9rem; - font-size: 0.85rem; -} - -.dynamic-list { - margin-bottom: 0.5rem; -} - -.dynamic-list .item-row { - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 0.5rem; - flex-wrap: wrap; -} - -.text-muted { - color: var(--text-muted); - font-size: 0.9rem; -} - -.dynamic-list .item-row input, -.dynamic-list .item-row select { - flex: 1; - min-width: 100px; - padding: 0.45rem 0.6rem; - font-size: 0.9rem; -} - -.dynamic-list .item-row .btn-remove { - flex-shrink: 0; -} - -.block-section { - border: 1px solid var(--border); - border-radius: 8px; - padding: 0.75rem; - margin-bottom: 0.75rem; - background: var(--bg-input); -} - -.block-section .block-title { - font-size: 0.85rem; - font-weight: 500; - margin-bottom: 0.5rem; -} - -.block-section .sub-list { - margin-top: 0.5rem; -} - -.block-section .sub-list .item-row { margin-bottom: 0.35rem; } -.block-section .btn-remove-block { margin-top: 0.5rem; } - -.form-row { - margin-bottom: 0.9rem; -} - -.form-row label { - display: block; - font-size: 0.8rem; - color: var(--text-muted); - margin-bottom: 0.35rem; -} - -.form-row input, -.form-row select, -.form-row textarea { - width: 100%; - padding: 0.55rem 0.75rem; - border: 1px solid var(--border); - border-radius: 8px; - background: var(--bg-input); - color: var(--text); - font-family: var(--font); - font-size: 0.9rem; -} - -.form-row textarea { - min-height: 80px; - resize: vertical; -} - -.form-row input:focus, -.form-row select:focus, -.form-row textarea:focus { - outline: none; - border-color: var(--accent); -} - -.field-hint { - display: block; - font-size: 0.75rem; - color: var(--text-muted); - margin-top: 0.35rem; -} - -.delay-inputs { - display: flex; - align-items: center; - gap: 0.5rem; - flex-wrap: wrap; -} - -.delay-inputs input { - width: 5rem; -} - -.delay-inputs span { - color: var(--text-muted); - font-size: 0.9rem; -} - -.form-row input[type="number"] { - max-width: 6rem; -} - -.btn { - padding: 0.6rem 1.2rem; - border-radius: 8px; - font-family: var(--font); - font-size: 0.95rem; - font-weight: 500; - cursor: pointer; - border: none; - transition: background 0.15s, opacity 0.15s; -} - -.btn-primary { - background: var(--accent); - color: #0a0a0c; -} - -.btn-primary:hover { - background: var(--accent-hover); -} - -.btn-ghost { - background: transparent; - color: var(--text-muted); - border: 1px solid var(--border); -} - -.btn-ghost:hover { - color: var(--text); - border-color: var(--text-muted); -} - -.btn-send { - margin-top: 0.5rem; -} - -.btn-small { - padding: 0.35rem 0.6rem; - font-size: 0.8rem; -} - -.btn-danger { - background: transparent; - color: #f87171; - border: 1px solid rgba(248, 113, 113, 0.4); -} - -.btn-danger:hover { - background: rgba(239, 68, 68, 0.15); - color: #fca5a5; - border-color: var(--danger); -} - -.qr-container { - margin-top: 1rem; - text-align: center; -} - -.qr-container p { - margin: 0 0 0.5rem 0; - font-size: 0.9rem; - color: var(--text-muted); -} - -.qr-image { - max-width: 280px; - width: 100%; - height: auto; - border-radius: 8px; - border: 1px solid var(--border); -} - -.qr-hint { - margin-top: 0.75rem !important; - font-size: 0.8rem !important; -} - -.status { - margin-top: 0.75rem; - padding: 0.5rem; - border-radius: 8px; - font-size: 0.85rem; -} - -.status.error { - background: rgba(239, 68, 68, 0.15); - color: #fca5a5; -} - -.status.success { - background: rgba(34, 197, 94, 0.15); - color: #86efac; -} - -.hidden { - display: none !important; -} - -.instance-list { - list-style: none; - padding: 0; - margin: 0 0 0.75rem 0; -} - -.instance-list li { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 0.5rem; - padding: 0.5rem 0; - border-bottom: 1px solid var(--border); - font-size: 0.9rem; -} - -.instance-list .instance-name { - font-weight: 500; -} - -.instance-list .instance-actions { - display: flex; - flex-wrap: wrap; - gap: 0.35rem; - align-items: center; -} - -.instance-list li:last-child { - border-bottom: none; -} - -.instance-list .badge { - font-size: 0.7rem; - padding: 0.2rem 0.5rem; - border-radius: 6px; - text-transform: uppercase; -} - -.instance-list .badge.connected { - background: rgba(34, 197, 94, 0.2); - color: var(--accent); -} - -.instance-list .badge.qr { - background: rgba(234, 179, 8, 0.2); - color: #eab308; -} - -.instance-list .badge.disconnected { - background: rgba(239, 68, 68, 0.2); - color: var(--danger); -} - -#btnRefreshList { - margin-top: 0.25rem; -} - -.dispatch-form { - margin-top: 1rem; - padding-top: 1rem; - border-top: 1px solid var(--border); -} - -.dispatch-form.hidden { - display: none; -} - -.form-card .form-row:first-of-type { - margin-top: 0; -} - -.result { - margin-top: 1rem; - padding: 0.75rem; - border-radius: 8px; - font-size: 0.9rem; - font-family: var(--font-mono); -} - -.result.success { - background: rgba(34, 197, 94, 0.15); - color: #86efac; -} - -.result.error { - background: rgba(239, 68, 68, 0.15); - color: #fca5a5; -} - -/* CTA row: tipo + texto + extra */ -#formInteractive .form-row select, -#formInteractive .form-row input { - width: auto; - min-width: 0; -} - -#formInteractive .form-row { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - align-items: center; -} - -#formInteractive .form-row label { - margin-bottom: 0; - min-width: 3rem; -} - -#formInteractive .form-row input:nth-of-type(2) { - flex: 1; - min-width: 100px; -} - -#formInteractive .form-row input:nth-of-type(3) { - flex: 1; - min-width: 120px; -} diff --git a/backend/plugins/whatsapp-v1/src/config.ts b/backend/plugins/whatsapp-v1/src/config.ts deleted file mode 100644 index 6f7cd28..0000000 --- a/backend/plugins/whatsapp-v1/src/config.ts +++ /dev/null @@ -1,18 +0,0 @@ -import dotenv from 'dotenv'; - -dotenv.config(); - -export const config = { - port: parseInt(process.env.PORT ?? '8787', 10), - apiKey: process.env.API_KEY ?? 'ACFH4RFOTME4RU50R4FKGNW34LDFG8DSQ', - authFolder: process.env.AUTH_FOLDER ?? 'auth', - limits: { - maxButtons: 3, - maxCarouselCards: 10, - maxListSections: 10, - maxListRowsPerSection: 10, - maxPollOptions: 12, - }, -} as const; - -export type Config = typeof config; diff --git a/backend/plugins/whatsapp-v1/src/handlers/superAdminMenu.ts b/backend/plugins/whatsapp-v1/src/handlers/superAdminMenu.ts deleted file mode 100644 index 1209bd3..0000000 --- a/backend/plugins/whatsapp-v1/src/handlers/superAdminMenu.ts +++ /dev/null @@ -1,267 +0,0 @@ -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import type { WASocketLike } from '../types/whatsapp.js'; - -type MenuKey = 'main' | 'users' | 'admins' | 'system' | 'session' | 'logs' | 'database'; - -type MenuOption = { - label: string; - next?: MenuKey; - action?: () => Promise; - exit?: boolean; - back?: boolean; -}; - -type MenuDefinition = { - title: string; - options: MenuOption[]; -}; - -const SESSION_TTL_MS = 5 * 60 * 1000; -const sessions = new Map(); - -function sanitizePhone(input: string): string { - return input.replace(/\D/g, ''); -} - -function getSenderJid(msg: any): string | null { - return msg?.key?.remoteJid ?? null; -} - -function getSenderNumber(jid: string): string { - const raw = jid.includes('@') ? jid.split('@')[0] : jid; - return sanitizePhone(raw); -} - -function isSuperAdmin(jid: string): boolean { - const admin = sanitizePhone(process.env.SUPER_ADMIN || ''); - if (!admin) return false; - const sender = getSenderNumber(jid); - return sender === admin; -} - -function extractText(msg: any): string | null { - const message = msg?.message; - if (!message) return null; - if (message.conversation) return message.conversation; - if (message.extendedTextMessage?.text) return message.extendedTextMessage.text; - if (message.imageMessage?.caption) return message.imageMessage.caption; - if (message.videoMessage?.caption) return message.videoMessage.caption; - if (message.buttonsResponseMessage?.selectedButtonId) return message.buttonsResponseMessage.selectedButtonId; - if (message.listResponseMessage?.singleSelectReply?.selectedRowId) return message.listResponseMessage.singleSelectReply.selectedRowId; - return null; -} - -function buildMenuText(menu: MenuDefinition): string { - let text = `*${menu.title}*\n\n`; - menu.options.forEach((opt, idx) => { - text += `*${idx + 1}.* ${opt.label}\n`; - }); - text += '\nResponda com o número desejado.'; - return text.trim(); -} - -function isExpired(ts: number): boolean { - return Date.now() - ts > SESSION_TTL_MS; -} - -function setSession(jid: string, menu: MenuKey) { - sessions.set(jid, { menu, updatedAt: Date.now() }); -} - -function clearSession(jid: string) { - sessions.delete(jid); -} - -function getSession(jid: string): { menu: MenuKey; updatedAt: number } | null { - const session = sessions.get(jid); - if (!session) return null; - if (isExpired(session.updatedAt)) { - sessions.delete(jid); - return null; - } - return session; -} - -function tailFile(filePath: string, maxLines: number): string { - try { - if (!fs.existsSync(filePath)) return 'Arquivo de log nao encontrado.'; - const data = fs.readFileSync(filePath, 'utf8'); - const lines = data.split(/\r?\n/).filter(Boolean); - return lines.slice(-maxLines).join('\n') || 'Sem logs recentes.'; - } catch { - return 'Erro ao ler logs.'; - } -} - -function formatBytes(bytes: number): string { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; -} - -function getPm2LogPath(kind: 'out' | 'error'): string { - const pm2Name = process.env.WHATSAPP_PM2_NAME || 'whatsapp-v1'; - return path.join('/root/.pm2/logs', `${pm2Name}-${kind}.log`); -} - -export function attachSuperAdminMenu(sock: WASocketLike, actions: { - getSessionStatus: () => string; - resetSession: () => Promise; - logoutSession: () => Promise; -}): void { - if ((sock as any).__superAdminMenuAttached) return; - (sock as any).__superAdminMenuAttached = true; - - const menus: Record = { - main: { - title: '🧠 PAINEL SUPER ADMIN', - options: [ - { label: 'Usuarios', next: 'users' }, - { label: 'Admins', next: 'admins' }, - { label: 'Sistema', next: 'system' }, - { label: 'Sessao WhatsApp', next: 'session' }, - { label: 'Logs', next: 'logs' }, - { label: 'Banco de Dados', next: 'database' }, - { label: 'Sair', exit: true }, - ], - }, - users: { - title: '1️⃣ Usuarios', - options: [ - { label: 'Listar usuarios', action: async () => 'Funcionalidade em desenvolvimento.' }, - { label: 'Buscar usuario', action: async () => 'Funcionalidade em desenvolvimento.' }, - { label: 'Remover usuario', action: async () => 'Funcionalidade em desenvolvimento.' }, - { label: 'Voltar', back: true }, - ], - }, - admins: { - title: '2️⃣ Admins', - options: [ - { label: 'Listar admins', action: async () => 'Funcionalidade em desenvolvimento.' }, - { label: 'Adicionar admin', action: async () => 'Funcionalidade em desenvolvimento.' }, - { label: 'Remover admin', action: async () => 'Funcionalidade em desenvolvimento.' }, - { label: 'Voltar', back: true }, - ], - }, - system: { - title: '3️⃣ Sistema', - options: [ - { label: 'Status do servidor', action: async () => { - const uptime = Math.floor(os.uptime() / 60); - return `Servidor online. Uptime: ${uptime} min. Node: ${process.version}.`; - }}, - { label: 'Uso de memoria', action: async () => { - const mem = process.memoryUsage(); - return `Memoria RSS: ${formatBytes(mem.rss)} | Heap: ${formatBytes(mem.heapUsed)} / ${formatBytes(mem.heapTotal)}`; - }}, - { label: 'Reiniciar bot', action: async () => { - setTimeout(() => process.exit(0), 500); - return 'Reiniciando bot...'; - }}, - { label: 'Voltar', back: true }, - ], - }, - session: { - title: '4️⃣ Sessao WhatsApp', - options: [ - { label: 'Status conexao', action: async () => `Status: ${actions.getSessionStatus()}` }, - { label: 'Resetar sessao', action: actions.resetSession }, - { label: 'Deslogar WhatsApp', action: actions.logoutSession }, - { label: 'Voltar', back: true }, - ], - }, - logs: { - title: '5️⃣ Logs', - options: [ - { label: 'Ultimos 50 logs', action: async () => tailFile(getPm2LogPath('out'), 50) }, - { label: 'Erros recentes', action: async () => tailFile(getPm2LogPath('error'), 50) }, - { label: 'Exportar log', action: async () => tailFile(getPm2LogPath('out'), 50) }, - { label: 'Voltar', back: true }, - ], - }, - database: { - title: '6️⃣ Banco de Dados', - options: [ - { label: 'Exportar users.json', action: async () => 'Funcionalidade em desenvolvimento.' }, - { label: 'Backup completo', action: async () => 'Funcionalidade em desenvolvimento.' }, - { label: 'Limpar registros inativos', action: async () => 'Funcionalidade em desenvolvimento.' }, - { label: 'Voltar', back: true }, - ], - }, - }; - - sock.ev.on('messages.upsert', async (payload: any) => { - const list = payload?.messages || []; - for (const msg of list) { - if (!msg?.message) continue; - if (msg?.key?.fromMe) continue; - - const jid = getSenderJid(msg); - if (!jid || jid.endsWith('@g.us')) continue; - - const text = extractText(msg); - if (!text) continue; - const body = text.trim(); - if (!body) continue; - - const lower = body.toLowerCase(); - if (lower === '.menu' || lower === '.panel') { - if (!isSuperAdmin(jid)) { - await sock.sendMessage(jid, { text: 'Acesso restrito.' }); - continue; - } - setSession(jid, 'main'); - await sock.sendMessage(jid, { text: buildMenuText(menus.main) }); - continue; - } - - if (!isSuperAdmin(jid)) continue; - - const session = getSession(jid); - if (!session) continue; - - if (isExpired(session.updatedAt)) { - clearSession(jid); - await sock.sendMessage(jid, { text: 'Sessao expirada. Envie .menu novamente.' }); - continue; - } - - const menu = menus[session.menu]; - const choice = parseInt(body, 10); - if (Number.isNaN(choice) || choice < 1 || choice > menu.options.length) { - await sock.sendMessage(jid, { text: 'Opcao invalida. Responda com um numero da lista.' }); - continue; - } - - const option = menu.options[choice - 1]; - if (option.exit) { - clearSession(jid); - await sock.sendMessage(jid, { text: 'Sessao encerrada.' }); - continue; - } - - if (option.back) { - setSession(jid, 'main'); - await sock.sendMessage(jid, { text: buildMenuText(menus.main) }); - continue; - } - - if (option.next) { - setSession(jid, option.next); - await sock.sendMessage(jid, { text: buildMenuText(menus[option.next]) }); - continue; - } - - if (option.action) { - setSession(jid, session.menu); - const reply = await option.action(); - await sock.sendMessage(jid, { text: reply }); - continue; - } - } - }); -} diff --git a/backend/plugins/whatsapp-v1/src/index.ts b/backend/plugins/whatsapp-v1/src/index.ts deleted file mode 100644 index 2dc8c7c..0000000 --- a/backend/plugins/whatsapp-v1/src/index.ts +++ /dev/null @@ -1,42 +0,0 @@ -import express from 'express'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { config } from './config.js'; -import instancesRouter from './routes/instances.js'; -import messagesRouter from './routes/messages.js'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const app = express(); - -app.use(express.json({ limit: '2mb' })); - -function apiKeyMiddleware(req: express.Request, res: express.Response, next: express.NextFunction) { - const key = req.headers['x-api-key']; - if (!config.apiKey || config.apiKey === '') { - return next(); - } - if (key !== config.apiKey) { - return res.status(401).json({ ok: false, error: 'invalid_api_key' }); - } - next(); -} - -app.get('/health', (_req, res) => { - res.json({ ok: true, service: 'rsalcara' }); -}); - -// API key só nas rotas /v1 (a interface em / carrega sem key) -app.use('/v1/instances', apiKeyMiddleware, instancesRouter); -app.use('/v1/messages', apiKeyMiddleware, messagesRouter); - -const publicDir = path.join(__dirname, '..', 'public'); -app.use(express.static(publicDir)); -app.get('/', (_req, res) => res.sendFile(path.join(publicDir, 'index.html'))); - -app.listen(config.port, () => { - console.log(`[rsalcara] API rodando em http://localhost:${config.port}`); - console.log(`[rsalcara] Interface: http://localhost:${config.port}`); - if (config.apiKey) { - console.log('[rsalcara] API Key ativa. Use header: x-api-key'); - } -}); diff --git a/backend/plugins/whatsapp-v1/src/routes/instances.ts b/backend/plugins/whatsapp-v1/src/routes/instances.ts deleted file mode 100644 index 7445f4c..0000000 --- a/backend/plugins/whatsapp-v1/src/routes/instances.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { Router, type Request, type Response } from 'express'; -import fs from 'node:fs'; -import path from 'node:path'; -import QRCode from 'qrcode'; -import { - createInstance, - getInstance, - getAllInstances, - removeInstance, - disconnectInstance, - logoutInstance, -} from '../services/whatsapp.js'; -import { config } from '../config.js'; - -const router = Router(); - -/** - * POST /v1/instances - * Cria uma nova instância e retorna o QR code em base64 (ou status se já conectada). - */ -router.post('/', async (req: Request, res: Response) => { - try { - const { instance = 'main' } = req.body as { instance?: string }; - const name = String(instance).trim() || 'main'; - - const result = await createInstance(name, config.authFolder); - - if (!result.ok) { - return res.status(500).json({ ok: false, error: result.error }); - } - - let qrBase64: string | undefined; - if (result.qr) { - qrBase64 = await QRCode.toDataURL(result.qr, { width: 400, margin: 2 }); - } - - const ctx = getInstance(name); - return res.json({ - ok: true, - instance: name, - status: ctx?.status ?? 'connecting', - qr: qrBase64 ?? null, - }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return res.status(500).json({ ok: false, error: message }); - } -}); - -/** - * GET /v1/instances - * Lista instâncias ativas e salvas (pastas em auth/). - */ -router.get('/', (_req: Request, res: Response) => { - const list = getAllInstances().map((ctx) => ({ - instance: ctx.name, - status: ctx.status, - hasQr: Boolean(ctx.qr), - createdAt: ctx.createdAt.toISOString(), - })); - - let saved: string[] = []; - const authDir = path.resolve(process.cwd(), config.authFolder); - try { - if (fs.existsSync(authDir)) { - saved = fs.readdirSync(authDir, { withFileTypes: true }) - .filter((d) => d.isDirectory()) - .map((d) => d.name); - } - } catch { - saved = []; - } - - return res.json({ ok: true, instances: list, saved }); -}); - -/** - * GET /v1/instances/saved - * Lista apenas nomes das conexões salvas (pastas em auth/). - */ -router.get('/saved', (_req: Request, res: Response) => { - const authDir = path.resolve(process.cwd(), config.authFolder); - let saved: string[] = []; - try { - if (fs.existsSync(authDir)) { - saved = fs.readdirSync(authDir, { withFileTypes: true }) - .filter((d) => d.isDirectory()) - .map((d) => d.name); - } - } catch { - saved = []; - } - return res.json({ ok: true, saved }); -}); - -/** - * GET /v1/instances/:name/qr - * Retorna o QR code da instância em base64 (se estiver em estado qr). - */ -router.get('/:name/qr', async (req: Request, res: Response) => { - const { name } = req.params; - const ctx = getInstance(name); - if (!ctx) { - return res.status(404).json({ ok: false, error: 'instance_not_found' }); - } - if (ctx.status !== 'qr' || !ctx.qr) { - return res.status(400).json({ ok: false, error: 'no_qr_available', status: ctx.status }); - } - const qrBase64 = await QRCode.toDataURL(ctx.qr, { width: 400, margin: 2 }); - return res.json({ ok: true, instance: name, qr: qrBase64 }); -}); - -/** - * GET /v1/instances/:name - * Status de uma instância. - */ -router.get('/:name', (req: Request, res: Response) => { - const { name } = req.params; - const ctx = getInstance(name); - if (!ctx) { - return res.status(404).json({ ok: false, error: 'instance_not_found' }); - } - return res.json({ - ok: true, - instance: ctx.name, - status: ctx.status, - hasQr: Boolean(ctx.qr), - createdAt: ctx.createdAt.toISOString(), - }); -}); - -/** - * POST /v1/instances/:name/disconnect - * Desconecta e remove a instância da memória (credenciais ficam em disco; reconectar pode usar sessão salva). - */ -router.post('/:name/disconnect', (req: Request, res: Response) => { - const { name } = req.params; - const removed = disconnectInstance(name); - return res.json({ ok: removed, instance: name }); -}); - -/** - * POST /v1/instances/:name/logout - * Logout + apaga pasta de auth. Próxima conexão gera novo QR. - */ -router.post('/:name/logout', async (req: Request, res: Response) => { - try { - const { name } = req.params; - const result = await logoutInstance(name, config.authFolder); - if (!result.ok) { - return res.status(500).json({ ok: false, instance: name, error: result.error }); - } - return res.json({ ok: true, instance: name }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return res.status(500).json({ ok: false, error: message }); - } -}); - -/** - * DELETE /v1/instances/:name - * Remove a instância (fecha socket, não apaga credenciais em disco). - */ -router.delete('/:name', (req: Request, res: Response) => { - const { name } = req.params; - const removed = removeInstance(name); - return res.json({ ok: removed, instance: name }); -}); - -export default router; diff --git a/backend/plugins/whatsapp-v1/src/routes/messages.ts b/backend/plugins/whatsapp-v1/src/routes/messages.ts deleted file mode 100644 index 726edca..0000000 --- a/backend/plugins/whatsapp-v1/src/routes/messages.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { Router, type Request, type Response } from 'express'; -import { getInstance } from '../services/whatsapp.js'; -import { toJid, isConnected } from '../utils/helpers.js'; -import { config } from '../config.js'; - -const router = Router(); - -function validateInstance(instanceName: string, res: Response): ReturnType { - const ctx = getInstance(instanceName); - if (!ctx) { - res.status(404).json({ ok: false, error: 'instance_not_found' }); - return undefined; - } - if (!isConnected(ctx)) { - res.status(400).json({ ok: false, error: 'instance_not_connected', status: ctx.status }); - return undefined; - } - return ctx; -} - -// --- 1. MENU TEXTO (opções numeradas) --- -router.post('/send_menu', async (req: Request, res: Response) => { - try { - const { instance = 'main', to, title, text, options, footer } = req.body as { - instance?: string; - to: string; - title?: string; - text?: string; - options: string[]; - footer?: string; - }; - - if (!to || !Array.isArray(options) || options.length === 0) { - return res.status(400).json({ ok: false, error: 'missing to/options' }); - } - - const ctx = validateInstance(instance, res); - if (!ctx) return; - - const jid = toJid(to); - if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' }); - - let menuText = ''; - if (title) menuText += `*${title}*\n\n`; - if (text) menuText += `${text}\n\n`; - options.forEach((opt, idx) => { - const label = typeof opt === 'string' ? opt : (opt as { text?: string }).text ?? `Opção ${idx + 1}`; - menuText += `*${idx + 1}.* ${label}\n`; - }); - if (footer) menuText += `\n_${footer}_`; - - await ctx.sock.sendMessage(jid, { text: menuText.trim() }); - return res.json({ ok: true, hint: 'User should reply with the option number (1, 2, 3...)' }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return res.status(500).json({ ok: false, error: message }); - } -}); - -// --- 2. BOTÕES QUICK REPLY (nativeButtons) --- -router.post('/send_buttons_helpers', async (req: Request, res: Response) => { - try { - const { instance = 'main', to, text, buttons, footer } = req.body as { - instance?: string; - to: string; - text: string; - buttons: Array<{ id: string; text: string }>; - footer?: string; - }; - - if (!to || !text || !Array.isArray(buttons) || buttons.length === 0) { - return res.status(400).json({ ok: false, error: 'missing to/text/buttons' }); - } - const limited = buttons.slice(0, config.limits.maxButtons); - - const ctx = validateInstance(instance, res); - if (!ctx) return; - - const jid = toJid(to); - if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' }); - - const nativeButtons = limited.map((btn, idx) => ({ - type: 'reply' as const, - id: btn.id ?? `btn_${idx}`, - text: btn.text ?? `Botão ${idx + 1}`, - })); - - const result = await ctx.sock.sendMessage(jid, { - nativeButtons, - text: String(text), - footer: footer ? String(footer) : undefined, - }); - - return res.json({ ok: true, format: 'nativeButtons', messageId: result?.key?.id }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return res.status(500).json({ ok: false, error: message }); - } -}); - -// --- 3. BOTÕES CTA (URL, COPY, CALL) --- -router.post('/send_interactive_helpers', async (req: Request, res: Response) => { - try { - const { instance = 'main', to, text, buttons, footer } = req.body as { - instance?: string; - to: string; - text: string; - buttons: Array<{ - type: 'url' | 'copy' | 'call'; - text: string; - url?: string; - copyCode?: string; - copyText?: string; - phoneNumber?: string; - }>; - footer?: string; - }; - - if (!to || !text || !Array.isArray(buttons) || buttons.length === 0) { - return res.status(400).json({ ok: false, error: 'missing to/text/buttons' }); - } - - const ctx = validateInstance(instance, res); - if (!ctx) return; - - const jid = toJid(to); - if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' }); - - const nativeButtons = buttons.slice(0, config.limits.maxButtons).map((btn, idx) => { - const type = (btn.type ?? 'reply').toLowerCase(); - if (type === 'url' || btn.url) { - return { type: 'url' as const, text: btn.text ?? 'Abrir', url: btn.url! }; - } - if (type === 'copy' || btn.copyCode || btn.copyText) { - return { type: 'copy' as const, text: btn.text ?? 'Copiar', copyText: btn.copyCode ?? btn.copyText ?? '' }; - } - if (type === 'call' || btn.phoneNumber) { - return { type: 'call' as const, text: btn.text ?? 'Ligar', phoneNumber: btn.phoneNumber! }; - } - return { type: 'reply' as const, id: `btn_${idx}`, text: btn.text ?? 'Botão' }; - }); - - const result = await ctx.sock.sendMessage(jid, { - nativeButtons, - text: String(text), - footer: footer ? String(footer) : undefined, - }); - - return res.json({ ok: true, format: 'nativeButtons', messageId: result?.key?.id }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return res.status(500).json({ ok: false, error: message }); - } -}); - -// --- 4. LISTA DROPDOWN (nativeList) --- -router.post('/send_list_helpers', async (req: Request, res: Response) => { - try { - const { instance = 'main', to, text, footer, buttonText, sections } = req.body as { - instance?: string; - to: string; - text: string; - footer?: string; - buttonText: string; - sections: Array<{ title: string; rows: Array<{ id: string; title: string; description?: string }> }>; - }; - - if (!to || !text || !buttonText || !Array.isArray(sections) || sections.length === 0) { - return res.status(400).json({ ok: false, error: 'missing to/text/buttonText/sections' }); - } - - const ctx = validateInstance(instance, res); - if (!ctx) return; - - const jid = toJid(to); - if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' }); - - const result = await ctx.sock.sendMessage(jid, { - nativeList: { - buttonText: String(buttonText), - sections: sections.map((s) => ({ - title: s.title ?? 'Opções', - rows: (s.rows ?? []).map((row, idx) => ({ - id: row.id ?? `row_${idx}`, - title: row.title ?? 'Item', - description: row.description ?? '', - })), - })), - }, - text: String(text), - footer: footer ? String(footer) : undefined, - }); - - return res.json({ ok: true, format: 'nativeList', messageId: result?.key?.id }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return res.status(500).json({ ok: false, error: message }); - } -}); - -// --- 5. ENQUETE / POLL --- -router.post('/send_poll', async (req: Request, res: Response) => { - try { - const { instance = 'main', to, name, options, selectableCount } = req.body as { - instance?: string; - to: string; - name: string; - options: string[]; - selectableCount?: number; - }; - - if (!to || !name || !Array.isArray(options) || options.length < 2) { - return res.status(400).json({ ok: false, error: 'missing to/name/options (min 2)' }); - } - const opts = options.slice(0, config.limits.maxPollOptions).map((o) => (typeof o === 'string' ? o : String(o))); - - const ctx = validateInstance(instance, res); - if (!ctx) return; - - const jid = toJid(to); - if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' }); - - // Formato que funciona no InfiniteAPI/Baileys: poll com name, values e selectableCount - const result = await ctx.sock.sendMessage(jid, { - poll: { - name: String(name), - values: opts, - selectableCount: Math.min(Math.max(1, selectableCount ?? 1), opts.length), - }, - }); - return res.json({ ok: true, messageId: result?.key?.id }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return res.status(500).json({ ok: false, error: message }); - } -}); - -// --- 6. CARROSSEL (nativeCarousel) --- -router.post('/send_carousel_helpers', async (req: Request, res: Response) => { - try { - const { instance = 'main', to, text, footer, cards } = req.body as { - instance?: string; - to: string; - text?: string; - footer?: string; - cards: Array<{ - title?: string; - body?: string; - footer?: string; - imageUrl?: string; - buttons?: Array<{ id: string; text: string }>; - }>; - }; - - if (!to || !Array.isArray(cards) || cards.length === 0) { - return res.status(400).json({ ok: false, error: 'missing to/cards' }); - } - const limited = cards.slice(0, config.limits.maxCarouselCards); - - const ctx = validateInstance(instance, res); - if (!ctx) return; - - const jid = toJid(to); - if (!jid) return res.status(400).json({ ok: false, error: 'invalid_phone' }); - - const formattedCards = limited.map((card, idx) => ({ - title: card.title ?? `Card ${idx + 1}`, - body: card.body ?? '', - footer: card.footer, - image: card.imageUrl ? { url: card.imageUrl } : undefined, - buttons: (card.buttons ?? []).map((btn, bIdx) => ({ - type: 'reply' as const, - id: btn.id ?? `card${idx}_btn${bIdx}`, - text: btn.text ?? 'Botão', - })), - })); - - const result = await ctx.sock.sendMessage(jid, { - nativeCarousel: { cards: formattedCards }, - text: text ? String(text) : undefined, - footer: footer ? String(footer) : undefined, - }); - - return res.json({ ok: true, format: 'nativeCarousel', messageId: result?.key?.id }); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return res.status(500).json({ ok: false, error: message }); - } -}); - -export default router; diff --git a/backend/plugins/whatsapp-v1/src/services/whatsapp.ts b/backend/plugins/whatsapp-v1/src/services/whatsapp.ts deleted file mode 100644 index 5bebd40..0000000 --- a/backend/plugins/whatsapp-v1/src/services/whatsapp.ts +++ /dev/null @@ -1,198 +0,0 @@ -import path from 'node:path'; -import fs from 'node:fs'; -import type { InstanceContext } from '../types/whatsapp.js'; -import { attachSuperAdminMenu } from '../handlers/superAdminMenu.js'; - -const instances = new Map(); - -function closeSocket(sock: InstanceContext['sock']): void { - try { - (sock as InstanceContext['sock']).ws?.close?.(); - } catch { - // ignore - } -} - -/** - * Retorna o contexto da instância pelo nome, ou undefined se não existir. - */ -export function getInstance(name: string): InstanceContext | undefined { - return instances.get(name); -} - -/** - * Retorna todas as instâncias. - */ -export function getAllInstances(): InstanceContext[] { - return Array.from(instances.values()); -} - -/** - * Cria e inicia uma nova instância WhatsApp (InfiniteAPI/Baileys). - * Gera QR code até o usuário escanear e conectar. - * Em 515 (restartRequired) recria o socket automaticamente após 2s. - */ -export async function createInstance( - name: string, - authFolder: string -): Promise<{ ok: boolean; instance: string; qr?: string; error?: string }> { - if (instances.has(name)) { - const ctx = instances.get(name)!; - if (ctx.status === 'connected') { - return { ok: true, instance: name }; - } - if (ctx.status === 'qr' && ctx.qr) { - return { ok: true, instance: name, qr: ctx.qr }; - } - // disconnected ou connecting: remove e recria para nova tentativa - closeSocket(ctx.sock); - instances.delete(name); - } - - try { - const { - default: makeWASocket, - useMultiFileAuthState, - DisconnectReason, - fetchLatestWaWebVersion, - Browsers, - } = await import('baileys'); - const authPath = path.resolve(process.cwd(), authFolder, name); - - const { state, saveCreds } = await useMultiFileAuthState(authPath); - - let version: [number, number, number]; - try { - const wa = await fetchLatestWaWebVersion({}); - const v = wa.version; - version = Array.isArray(v) && v.length >= 3 ? [v[0], v[1], v[2]] : [2, 3000, 1032884366]; - } catch { - version = [2, 3000, 1032884366]; - } - - const sock = makeWASocket({ - auth: state, - printQRInTerminal: false, - version, - browser: Browsers.windows('Chrome'), - }) as InstanceContext['sock']; - - const ctx: InstanceContext = { - name, - sock, - status: 'connecting', - qr: null, - createdAt: new Date(), - authFolder, - }; - instances.set(name, ctx); - - sock.ev.on('creds.update', saveCreds); - - sock.ev.on('connection.update', ((update: unknown) => { - const { connection, qr, lastDisconnect } = (update ?? {}) as { - connection?: string; - qr?: string; - lastDisconnect?: { error?: { output?: { statusCode?: number } } }; - }; - - if (qr) { - ctx.status = 'qr'; - ctx.qr = qr; - } - - if (connection === 'open') { - ctx.status = 'connected'; - ctx.qr = null; - } - - if (connection === 'close') { - const code = (lastDisconnect?.error as { output?: { statusCode?: number } } | undefined)?.output?.statusCode; - ctx.status = 'disconnected'; - ctx.qr = null; - - if (code === DisconnectReason.loggedOut || code === DisconnectReason.connectionReplaced) { - closeSocket(ctx.sock); - instances.delete(name); - return; - } - - // 515 = restartRequired: pairing concluído, WA pede reinício. Recriar socket com o mesmo auth. - if (code === DisconnectReason.restartRequired) { - const folder = ctx.authFolder; - closeSocket(ctx.sock); - instances.delete(name); - setTimeout(() => { - createInstance(name, folder).catch(() => {}); - }, 2000); - } - } - })); - - attachSuperAdminMenu(sock, { - getSessionStatus: () => ctx.status, - resetSession: async () => { - await logoutInstance(name, authFolder); - const created = await createInstance(name, authFolder); - if (!created.ok) return `Erro ao resetar: ${created.error || 'desconhecido'}`; - return created.qr ? 'Sessao resetada. Novo QR gerado.' : 'Sessao resetada. Aguarde novo QR.'; - }, - logoutSession: async () => { - const result = await logoutInstance(name, authFolder); - if (!result.ok) return `Erro ao deslogar: ${result.error || 'desconhecido'}`; - return 'WhatsApp deslogado. Novo QR sera necessario.'; - }, - }); - - return { ok: true, instance: name, qr: ctx.qr ?? undefined }; - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return { ok: false, instance: name, error: message }; - } -} - -/** - * Desconecta e remove a instância da memória (credenciais permanecem em disco). - */ -export function disconnectInstance(name: string): boolean { - const ctx = instances.get(name); - if (!ctx) return false; - closeSocket(ctx.sock); - instances.delete(name); - return true; -} - -/** - * Logout + apaga pasta de auth e remove instância. Próxima conexão gerará novo QR. - */ -export async function logoutInstance(name: string, authFolder: string): Promise<{ ok: boolean; error?: string }> { - const ctx = instances.get(name); - if (ctx) { - try { - if (typeof ctx.sock.logout === 'function') { - await ctx.sock.logout(); - } - } catch { - // ignore - } - closeSocket(ctx.sock); - instances.delete(name); - } - const authPath = path.resolve(process.cwd(), authFolder, name); - try { - if (fs.existsSync(authPath)) { - fs.rmSync(authPath, { recursive: true }); - } - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - return { ok: false, error: message }; - } - return { ok: true }; -} - -/** - * Remove a instância (fecha socket e remove do mapa). Não apaga credenciais. - */ -export function removeInstance(name: string): boolean { - return disconnectInstance(name); -} diff --git a/backend/plugins/whatsapp-v1/src/types/whatsapp.ts b/backend/plugins/whatsapp-v1/src/types/whatsapp.ts deleted file mode 100644 index 4800a97..0000000 --- a/backend/plugins/whatsapp-v1/src/types/whatsapp.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Tipos para o socket InfiniteAPI/Baileys. - * O socket expõe sendMessage com nativeButtons, nativeList, nativeCarousel, etc. - */ -export interface InstanceContext { - name: string; - sock: WASocketLike; - status: 'connecting' | 'connected' | 'disconnected' | 'qr'; - qr: string | null; - createdAt: Date; - authFolder: string; -} - -export interface WASocketLike { - sendMessage: (jid: string, content: MessageContent) => Promise<{ key?: { id?: string }; messageTimestamp?: number }>; - relayMessage?: (jid: string, content: unknown, opts?: unknown) => Promise; - ev: { on: (event: string, handler: (...args: unknown[]) => void) => void }; - logout?: () => Promise; - ws?: { close: () => void }; -} - -export interface NativeButtonReply { - type: 'reply'; - id: string; - text: string; -} - -export interface NativeButtonUrl { - type: 'url'; - text: string; - url: string; -} - -export interface NativeButtonCopy { - type: 'copy'; - text: string; - copyText: string; -} - -export interface NativeButtonCall { - type: 'call'; - text: string; - phoneNumber: string; -} - -export type NativeButton = NativeButtonReply | NativeButtonUrl | NativeButtonCopy | NativeButtonCall; - -export interface NativeListRow { - id: string; - title: string; - description?: string; -} - -export interface NativeListSection { - title: string; - rows: NativeListRow[]; -} - -export interface NativeCarouselCard { - title?: string; - body?: string; - footer?: string; - image?: { url: string }; - imageUrl?: string; - buttons?: Array<{ type?: string; id: string; text: string }>; -} - -export interface MessageContent { - text?: string; - footer?: string; - nativeButtons?: NativeButton[]; - nativeList?: { - buttonText: string; - sections: NativeListSection[]; - }; - nativeCarousel?: { - cards: NativeCarouselCard[]; - }; - poll?: { - name: string; - values: string[]; - selectableCount?: number; - }; - pollCreationMessage?: { - name: string; - options: Array<{ optionName: string }>; - selectableOptionsCount?: number; - }; - [key: string]: unknown; -} diff --git a/backend/plugins/whatsapp-v1/src/utils/helpers.ts b/backend/plugins/whatsapp-v1/src/utils/helpers.ts deleted file mode 100644 index 3846145..0000000 --- a/backend/plugins/whatsapp-v1/src/utils/helpers.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Formata número para JID do WhatsApp (5511999999999@s.whatsapp.net) - */ -export function toJid(phone: string | null | undefined): string | null { - if (!phone || typeof phone !== 'string') return null; - const cleaned = phone.replace(/\D/g, ''); - if (cleaned.length < 10) return null; - if (phone.includes('@')) return phone; - return `${cleaned}@s.whatsapp.net`; -} - -/** - * Verifica se a instância está conectada - */ -export function isConnected(ctx: { sock?: unknown; status?: string } | null): boolean { - return Boolean(ctx?.sock && ctx.status === 'connected'); -} - -/** - * Verifica se string é URL - */ -export function isUrl(str: unknown): str is string { - if (typeof str !== 'string') return false; - return /^https?:\/\//i.test(str.trim()); -} diff --git a/backend/plugins/whatsapp-v1/tsconfig.json b/backend/plugins/whatsapp-v1/tsconfig.json deleted file mode 100644 index c64a0d8..0000000 --- a/backend/plugins/whatsapp-v1/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "lib": ["ES2022"], - "outDir": "dist", - "rootDir": "src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true - }, - "include": ["src/**/*.ts"], - "exclude": ["node_modules", "dist"] -} diff --git a/backend/schema.sql b/backend/schema.sql new file mode 100644 index 0000000..fa575bb --- /dev/null +++ b/backend/schema.sql @@ -0,0 +1,282 @@ +-- Clube de Benefícios - Production Schema +-- Generated for MySQL (sis_cb67) + +SET FOREIGN_KEY_CHECKS = 0; + +DROP TABLE IF EXISTS benefit_partner_links; +DROP TABLE IF EXISTS benefit_share_requests; +DROP TABLE IF EXISTS webhooks; +DROP TABLE IF EXISTS plugins; +DROP TABLE IF EXISTS logs; +DROP TABLE IF EXISTS sessions; +DROP TABLE IF EXISTS favorites; +DROP TABLE IF EXISTS transactions; +DROP TABLE IF EXISTS benefits; +DROP TABLE IF EXISTS partners; +DROP TABLE IF EXISTS categories; +DROP TABLE IF EXISTS oauth_accounts; +DROP TABLE IF EXISTS knex_migrations; +DROP TABLE IF EXISTS knex_migrations_lock; +DROP TABLE IF EXISTS users; + +SET FOREIGN_KEY_CHECKS = 1; + +-- users +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NULL, + phone VARCHAR(30) NULL, + cpf VARCHAR(14) UNIQUE NULL DEFAULT NULL, + city VARCHAR(100) NULL, + neighborhood VARCHAR(100) NULL, + gender ENUM('M','F','O') NULL, + birth_date DATE NULL, + avatar_url VARCHAR(500) NULL, + role ENUM('user','partner_admin','secretary','clinical_manager','finance','super_admin') NOT NULL DEFAULT 'user', + status ENUM('lead','pending','active','inactive') NOT NULL DEFAULT 'active', + email_verified BOOLEAN DEFAULT FALSE, + refresh_token VARCHAR(500) NULL, + last_login TIMESTAMP NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_email (email), + INDEX idx_role (role), + INDEX idx_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- oauth_accounts +CREATE TABLE oauth_accounts ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT UNSIGNED NOT NULL, + provider VARCHAR(50) NOT NULL, + provider_user_id VARCHAR(255) NOT NULL, + access_token VARCHAR(1000) NULL, + refresh_token VARCHAR(1000) NULL, + token_expires_at TIMESTAMP NULL, + profile_data JSON NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uniq_provider_user (provider, provider_user_id), + INDEX idx_user_id (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- categories +CREATE TABLE categories ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(100) NOT NULL UNIQUE, + slug VARCHAR(100) NOT NULL UNIQUE, + icon VARCHAR(50) NULL, + color VARCHAR(50) NULL, + description TEXT NULL, + active BOOLEAN DEFAULT TRUE, + sort_order INT DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- partners +CREATE TABLE partners ( + id INT AUTO_INCREMENT PRIMARY KEY, + company_name VARCHAR(255) NOT NULL, + slug VARCHAR(255) UNIQUE NOT NULL, + cnpj VARCHAR(20) UNIQUE NULL DEFAULT NULL, + email VARCHAR(255) NULL, + phone VARCHAR(30) NULL, + address_street VARCHAR(255) NULL, + address_number VARCHAR(20) NULL, + address_neighborhood VARCHAR(100) NULL, + address_city VARCHAR(100) NULL, + address_state VARCHAR(2) NULL, + address_zip VARCHAR(10) NULL, + type VARCHAR(100) NOT NULL, + monthly_goal INT DEFAULT 0, + status ENUM('active','inactive') NOT NULL DEFAULT 'active', + gradient_from VARCHAR(50) NULL, + gradient_to VARCHAR(50) NULL, + icon VARCHAR(10) NULL, + description TEXT NULL, + logo_url VARCHAR(500) NULL, + images JSON NULL, + domain VARCHAR(255) NULL, + subdomain VARCHAR(255) NULL, + subscription_plan ENUM('basic','pro','enterprise') DEFAULT 'basic', + owner_user_id INT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_slug (slug), + INDEX idx_status (status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- benefits +CREATE TABLE benefits ( + id INT AUTO_INCREMENT PRIMARY KEY, + partner_id INT NOT NULL, + category_id INT NULL, + title VARCHAR(255) NOT NULL, + description TEXT NULL, + rules TEXT NULL, + contact VARCHAR(255) NULL, + type VARCHAR(50) NOT NULL, + delivery_type ENUM('local','external') NOT NULL DEFAULT 'local', + active BOOLEAN DEFAULT TRUE, + is_global BOOLEAN DEFAULT FALSE, + approved_by_admin BOOLEAN DEFAULT FALSE, + global_request_status ENUM('none','pending','approved') DEFAULT 'none', + priority INT DEFAULT 0, + discount_percent DECIMAL(5,2) NULL, + valid_until DATE NULL, + featured_image VARCHAR(500) NULL, + redemption_type ENUM('coupon','qrcode','card','app') DEFAULT 'coupon', + usage_count INT DEFAULT 0, + savings_amount DECIMAL(12,2) DEFAULT 0, + badge VARCHAR(50) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_partner (partner_id), + INDEX idx_category (category_id), + INDEX idx_active (active), + INDEX idx_type (type), + FOREIGN KEY (partner_id) REFERENCES partners(id) ON DELETE CASCADE, + FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- transactions +CREATE TABLE transactions ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + benefit_id INT NOT NULL, + partner_id INT NOT NULL, + type ENUM('redemption','view','favorite','share') NOT NULL, + status ENUM('pending','completed','cancelled','expired') DEFAULT 'pending', + redemption_code VARCHAR(100) NULL, + qr_code_data VARCHAR(500) NULL, + original_value DECIMAL(12,2) NULL, + discount_value DECIMAL(12,2) NULL, + redeemed_at TIMESTAMP NULL, + expires_at TIMESTAMP NULL, + metadata JSON NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user (user_id), + INDEX idx_benefit (benefit_id), + INDEX idx_status (status), + INDEX idx_code (redemption_code), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (benefit_id) REFERENCES benefits(id) ON DELETE CASCADE, + FOREIGN KEY (partner_id) REFERENCES partners(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- favorites +CREATE TABLE favorites ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + benefit_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uniq_user_benefit (user_id, benefit_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (benefit_id) REFERENCES benefits(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- sessions +CREATE TABLE sessions ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + token_hash VARCHAR(255) NOT NULL, + ip_address VARCHAR(45) NULL, + user_agent VARCHAR(500) NULL, + expires_at TIMESTAMP NOT NULL, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user (user_id), + INDEX idx_token (token_hash), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- logs (audit) +CREATE TABLE logs ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NULL, + action VARCHAR(100) NOT NULL, + entity_type VARCHAR(100) NULL, + entity_id INT NULL, + description TEXT NULL, + old_data JSON NULL, + new_data JSON NULL, + ip_address VARCHAR(45) NULL, + user_agent VARCHAR(500) NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_user (user_id), + INDEX idx_action (action), + INDEX idx_entity (entity_type), + INDEX idx_created (created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- plugins +CREATE TABLE plugins ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL UNIQUE, + slug VARCHAR(255) NOT NULL UNIQUE, + version VARCHAR(20) NOT NULL, + description TEXT NULL, + author VARCHAR(255) NULL, + entry_point VARCHAR(255) NOT NULL, + active BOOLEAN DEFAULT FALSE, + dependencies JSON NULL, + config JSON NULL, + hooks JSON NULL, + directory VARCHAR(500) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_slug (slug), + INDEX idx_active (active) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- webhooks +CREATE TABLE webhooks ( + id INT AUTO_INCREMENT PRIMARY KEY, + partner_id INT NULL, + url VARCHAR(500) NOT NULL, + secret VARCHAR(255) NULL, + events JSON NOT NULL, + active BOOLEAN DEFAULT TRUE, + retry_count INT DEFAULT 0, + max_retries INT DEFAULT 3, + last_triggered TIMESTAMP NULL, + last_status ENUM('success','failed','pending') NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + INDEX idx_partner (partner_id), + INDEX idx_active (active), + FOREIGN KEY (partner_id) REFERENCES partners(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- benefit_share_requests +CREATE TABLE benefit_share_requests ( + id INT AUTO_INCREMENT PRIMARY KEY, + benefit_id INT NOT NULL, + requesting_partner_id INT NOT NULL, + owner_partner_id INT NOT NULL, + status ENUM('pending','approved','rejected') DEFAULT 'pending', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (benefit_id) REFERENCES benefits(id) ON DELETE CASCADE, + FOREIGN KEY (requesting_partner_id) REFERENCES partners(id) ON DELETE CASCADE, + FOREIGN KEY (owner_partner_id) REFERENCES partners(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- benefit_partner_links +CREATE TABLE benefit_partner_links ( + id INT AUTO_INCREMENT PRIMARY KEY, + benefit_id INT NOT NULL, + partner_id INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uniq_benefit_partner (benefit_id, partner_id), + FOREIGN KEY (benefit_id) REFERENCES benefits(id) ON DELETE CASCADE, + FOREIGN KEY (partner_id) REFERENCES partners(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/backend/scripts/inspect_benefits.ts b/backend/scripts/inspect_benefits.ts deleted file mode 100644 index ac6aeba..0000000 --- a/backend/scripts/inspect_benefits.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { db } from '../src/config/database'; - -async function run() { - try { - const benefits = await db('benefits') - .select('id', 'title', 'public_title', 'public_description', 'public_hide_values') - .limit(5) - .orderBy('updated_at', 'desc'); - - console.log('Recent Benefits Data:', JSON.stringify(benefits, null, 2)); - } catch (error) { - console.error('Error:', error); - } finally { - await db.destroy(); - } -} - -run(); diff --git a/backend/scripts/test_update.ts b/backend/scripts/test_update.ts deleted file mode 100644 index 045b6f6..0000000 --- a/backend/scripts/test_update.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { db } from '../src/config/database'; - -async function run() { - try { - const id = 1; // Assuming ID 1 exists - const payload = { - public_title: "Teste de Título Público", - public_description: "Descrição pública de teste via script", - public_hide_values: true - }; - - console.log(`Updating benefit ${id} with:`, payload); - - await db('benefits').where({ id }).update(payload); - - const updated = await db('benefits').where({ id }).first(); - console.log('Updated Benefit:', JSON.stringify(updated, null, 2)); - - } catch (error) { - console.error('Error:', error); - } finally { - await db.destroy(); - } -} - -run(); diff --git a/backend/seed.sql b/backend/seed.sql new file mode 100644 index 0000000..fc7093f --- /dev/null +++ b/backend/seed.sql @@ -0,0 +1,36 @@ +-- Clube de Benefícios - Seed Data + +-- Super Admin (password: Rc362514 hashed with bcrypt) +INSERT INTO users (name, email, password_hash, role, status, email_verified) VALUES +('Super Admin', 'ruibto@gmail.com', '$2a$12$PLACEHOLDER_HASH', 'super_admin', 'active', TRUE); + +-- Categories +INSERT INTO categories (name, slug, icon, color, sort_order) VALUES +('Gastronomia', 'gastronomia', '🍽️', '#FF6B35', 1), +('Saúde', 'saude', '🏥', '#4CAF50', 2), +('Educação', 'educacao', '📚', '#2196F3', 3), +('Lazer', 'lazer', '🎭', '#9C27B0', 4), +('Esportes', 'esportes', '⚽', '#FF9800', 5), +('Compras', 'compras', '🛍️', '#E91E63', 6), +('Serviços', 'servicos', '🔧', '#607D8B', 7), +('Viagens', 'viagens', '✈️', '#00BCD4', 8), +('Beleza & Estética', 'beleza-estetica', '💅', '#F06292', 9), +('Odontologia', 'odontologia', '🦷', '#26A69A', 10), +('Nutrição', 'nutricao', '🥗', '#8BC34A', 11), +('Farmácia', 'farmacia', '💊', '#EF5350', 12); + +-- Partners +INSERT INTO partners (company_name, slug, cnpj, phone, address_street, address_number, address_neighborhood, address_city, address_state, address_zip, type, monthly_goal, status, gradient_from, gradient_to, icon, description, owner_user_id) VALUES +('Escola Coração de Maria', 'coracaodemaria', '11.222.333/0001-44', '(67) 3333-1111', 'Rua das Flores', '123', 'Centro', 'Campo Grande', 'MS', '79000-001', 'Escola', 50, 'active', '#3B82F6', '#1D4ED8', '🏫', 'Instituição de ensino fundamental e médio.', 1), +('Drogasil Filial Centro', 'drogasilcentro', '44.555.666/0001-77', '(67) 3333-2222', 'Avenida Principal', '456', 'Tiradentes', 'Campo Grande', 'MS', '79000-002', 'Farmácia', 75, 'active', '#EF4444', '#B91C1C', '⚕️', 'Rede de farmácias com ampla variedade.', 1), +('SmartFit Unidade Centro', 'smartfitcentro', NULL, '(67) 3333-3333', 'Travessa Esportiva', '789', 'Aero Rancho', 'Campo Grande', 'MS', '79000-003', 'Academia', 100, 'active', '#EAB308', '#F97316', '🏋️', 'Academia com equipamentos modernos.', 1), +('Consultt Clinic', 'consulttclinic', NULL, '(67) 9999-0000', 'Rua Odonto', '100', 'Centro', 'Campo Grande', 'MS', '79000-100', 'Odontologia', 30, 'active', '#06B6D4', '#3B82F6', '🦷', 'Clínica odontológica de referência.', 1), +('Escola de Música Som do Coração', 'somdocoracao', NULL, '(67) 9999-1112', 'Rua da Harmonia', '200', 'Centro', 'Campo Grande', 'MS', '79000-200', 'Curso', 20, 'active', '#A855F7', '#6366F1', '🎵', 'Aulas de música para todas as idades.', 1), +('Ótica Central', 'oticacentral', NULL, '(67) 9999-4444', 'Avenida da Visão', '300', 'Tiradentes', 'Campo Grande', 'MS', '79000-300', 'Ótica', 40, 'active', '#14B8A6', '#22C55E', '👓', 'Soluções para saúde visual.', 1); + +-- Benefits +INSERT INTO benefits (partner_id, category_id, title, description, rules, contact, type, delivery_type, active, is_global, approved_by_admin, global_request_status, priority, discount_percent, valid_until, redemption_type) VALUES +(4, 10, 'Benefício Saúde Bucal', 'Cuidado odontológico completo com condições especiais para membros.', 'Benefício válido para membros ativos. Descontos não acumulativos.', '(67) 9999-0000', 'dental', 'external', TRUE, TRUE, TRUE, 'approved', 1, 30.00, '2026-12-31', 'qrcode'), +(5, 3, 'Aula de Violão Gratuita', 'Experimente uma aula de violão com nossos melhores professores.', 'Válido para novos alunos.', '(67) 9999-1112', 'education', 'local', TRUE, FALSE, TRUE, 'none', 3, 100.00, '2026-06-30', 'coupon'), +(5, 3, '20% de Desconto na Matrícula', 'Comece seus estudos musicais com um super desconto.', 'Válido para planos semestrais.', '(67) 9999-1112', 'education', 'local', TRUE, FALSE, FALSE, 'none', 3, 20.00, '2026-12-31', 'coupon'), +(6, 2, '25% OFF em Armações', 'Escolha qualquer armação da loja com desconto exclusivo.', 'Válido para pagamentos à vista.', '(67) 9999-4444', 'health', 'external', TRUE, FALSE, TRUE, 'none', 3, 25.00, '2026-12-31', 'coupon'); diff --git a/backend/seed_admin.js b/backend/seed_admin.js deleted file mode 100644 index 9d17412..0000000 --- a/backend/seed_admin.js +++ /dev/null @@ -1,67 +0,0 @@ -const { Client } = require('pg'); -const bcrypt = require('bcryptjs'); -const dotenv = require('dotenv'); -const path = require('path'); - -dotenv.config({ path: path.join(__dirname, '.env') }); - -const client = new Client({ - host: process.env.DB_HOST, - port: parseInt(process.env.DB_PORT || '5432'), - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, -}); - -async function main() { - try { - await client.connect(); - console.log('Connected to PostgreSQL to seed Super Admin and initial configurations...'); - - const email = 'ruibto@gmail.com'; - const password = 'Rc362514'; - const name = 'Rui Dono'; - const hash = await bcrypt.hash(password, 10); - - // Check if user already exists - const check = await client.query('SELECT id FROM users WHERE email = $1', [email]); - if (check.rows.length > 0) { - console.log('Super Admin already exists!'); - } else { - // Insert Super Admin - await client.query( - 'INSERT INTO users (name, email, password_hash, role, status, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), NOW())', - [name, email, hash, 'super_admin', 'active'] - ); - console.log('Super Admin seeded successfully!'); - } - - // Seed default categories for Clube67 - const categories = [ - { name: 'Gastronomia', slug: 'gastronomia', icon: 'Utensils' }, - { name: 'Saúde & Bem-Estar', slug: 'saude-e-bem-estar', icon: 'HeartPulse' }, - { name: 'Educação', slug: 'educacao', icon: 'GraduationCap' }, - { name: 'Lazer & Viagens', slug: 'lazer-e-viagens', icon: 'Compass' }, - { name: 'Serviços', slug: 'servicos', icon: 'Wrench' }, - { name: 'Moda & Beleza', slug: 'moda-e-beleza', icon: 'Sparkles' } - ]; - - for (const cat of categories) { - const cCheck = await client.query('SELECT id FROM categories WHERE slug = $1', [cat.slug]); - if (cCheck.rows.length === 0) { - await client.query( - 'INSERT INTO categories (name, slug, icon, created_at, updated_at) VALUES ($1, $2, $3, NOW(), NOW())', - [cat.name, cat.slug, cat.icon] - ); - console.log(`Category '${cat.name}' seeded!`); - } - } - - await client.end(); - console.log('Database seeding successfully finished!'); - } catch (err) { - console.error('Seeding failed:', err); - } -} - -main(); diff --git a/backend/src/config/database.ts b/backend/src/config/database.ts index 2653e66..315b7de 100644 --- a/backend/src/config/database.ts +++ b/backend/src/config/database.ts @@ -1,23 +1,30 @@ import knex from 'knex'; import { config } from './index'; -import path from 'path'; -const knexConfig = { - client: 'pg', +const db = knex({ + client: 'mysql2', connection: { host: config.db.host, port: config.db.port, user: config.db.user, password: config.db.password, - database: config.db.database, + database: config.db.name, + charset: 'utf8mb4', + timezone: '+00:00', + }, + pool: { + min: 2, + max: 20, + acquireTimeoutMillis: 30000, + idleTimeoutMillis: 30000, }, - pool: { min: 2, max: 10 }, migrations: { - directory: path.join(__dirname, '../database/migrations'), - tableName: 'knex_migrations' - } -}; + directory: __dirname + '/../migrations', + tableName: 'knex_migrations', + }, + seeds: { + directory: __dirname + '/../seeds', + }, +}); -export const db = knex(knexConfig); - -export default knexConfig; +export default db; diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index c257c9f..df1ce32 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,24 +1,26 @@ -import dotenv from 'dotenv'; import path from 'path'; +import dotenv from 'dotenv'; -// Load .env with multiple fallback paths to support Knex CLI running in different subdirectories -dotenv.config({ path: path.join(process.cwd(), '.env') }); -dotenv.config({ path: path.join(__dirname, '../../.env') }); -dotenv.config({ path: path.join(__dirname, '../../../.env') }); +dotenv.config({ path: path.resolve(__dirname, '../../../.env') }); export const config = { - env: process.env.NODE_ENV || 'development', - port: parseInt(process.env.PORT || process.env.APP_PORT || '3001', 10), + app: { + name: process.env.APP_NAME || 'Clube de Benefícios', + url: process.env.APP_URL || 'http://localhost:3001', + port: parseInt(process.env.APP_PORT || '3001', 10), + env: process.env.NODE_ENV || 'development', + frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3000', + }, db: { - host: process.env.DB_HOST, + host: process.env.DB_HOST || 'localhost', port: parseInt(process.env.DB_PORT || '3306', 10), - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, + user: process.env.DB_USER || 'sis_cb67', + password: process.env.DB_PASSWORD || '', + name: process.env.DB_NAME || 'sis_cb67', }, jwt: { - secret: process.env.JWT_SECRET || 'secret', - refreshSecret: process.env.JWT_REFRESH_SECRET || 'refresh_secret', + secret: process.env.JWT_SECRET || 'change-me', + refreshSecret: process.env.JWT_REFRESH_SECRET || 'change-me-refresh', expiresIn: process.env.JWT_EXPIRES_IN || '15m', refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d', }, @@ -28,15 +30,15 @@ export const config = { password: process.env.REDIS_PASSWORD || undefined, }, google: { - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, - callbackUrl: process.env.GOOGLE_CALLBACK_URL, + clientId: process.env.GOOGLE_CLIENT_ID || '', + clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', + callbackUrl: process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3001/api/auth/google/callback', }, upload: { - dir: process.env.UPLOAD_DIR || path.join(__dirname, '../../../uploads'), + dir: process.env.UPLOAD_DIR || path.resolve(__dirname, '../../../uploads'), maxSize: parseInt(process.env.MAX_FILE_SIZE || '5242880', 10), }, - logging: { - dir: process.env.LOG_DIR || path.join(__dirname, '../../../logs'), - } + log: { + dir: process.env.LOG_DIR || path.resolve(__dirname, '../../../logs'), + }, }; diff --git a/backend/src/config/knexfile.ts b/backend/src/config/knexfile.ts index 15c4d06..f966c09 100644 --- a/backend/src/config/knexfile.ts +++ b/backend/src/config/knexfile.ts @@ -1,16 +1,44 @@ import { config } from './index'; -export default { - client: 'pg', - connection: { - host: config.db.host, - port: config.db.port, - user: config.db.user, - password: config.db.password, - database: config.db.database, +const knexConfig = { + development: { + client: 'mysql2', + connection: { + host: config.db.host, + port: config.db.port, + user: config.db.user, + password: config.db.password, + database: config.db.name, + charset: 'utf8mb4', + }, + migrations: { + directory: __dirname + '/../migrations', + tableName: 'knex_migrations', + }, + seeds: { + directory: __dirname + '/../seeds', + }, }, - migrations: { - directory: '../database/migrations', - extension: 'ts', + production: { + client: 'mysql2', + connection: { + host: config.db.host, + port: config.db.port, + user: config.db.user, + password: config.db.password, + database: config.db.name, + charset: 'utf8mb4', + }, + pool: { min: 2, max: 20 }, + migrations: { + directory: __dirname + '/../migrations', + tableName: 'knex_migrations', + }, + seeds: { + directory: __dirname + '/../seeds', + }, }, }; + +export default knexConfig; +module.exports = knexConfig; diff --git a/backend/src/config/logger.ts b/backend/src/config/logger.ts deleted file mode 100644 index 2009922..0000000 --- a/backend/src/config/logger.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { logger } from '../utils/logger'; -export { logger }; -export default logger; diff --git a/backend/src/config/redis.ts b/backend/src/config/redis.ts index 3a1edd1..70939d3 100644 --- a/backend/src/config/redis.ts +++ b/backend/src/config/redis.ts @@ -1,13 +1,23 @@ import Redis from 'ioredis'; import { config } from './index'; -import { logger } from '../utils/logger'; -export const redis = new Redis({ +const redis = new Redis({ host: config.redis.host, port: config.redis.port, password: config.redis.password, - retryStrategy: (times) => Math.min(times * 50, 2000), + retryStrategy(times) { + const delay = Math.min(times * 50, 2000); + return delay; + }, + maxRetriesPerRequest: 3, }); -redis.on('connect', () => logger.info('✅ Redis connected')); -redis.on('error', (err) => logger.error('❌ Redis error:', err)); +redis.on('connect', () => { + console.log('✅ Redis connected'); +}); + +redis.on('error', (err) => { + console.error('❌ Redis error:', err.message); +}); + +export default redis; diff --git a/backend/src/controllers/admin.controller.ts b/backend/src/controllers/admin.controller.ts new file mode 100644 index 0000000..6fcd1dd --- /dev/null +++ b/backend/src/controllers/admin.controller.ts @@ -0,0 +1,122 @@ +import { Response } from 'express'; +import db from '../config/database'; +import redis from '../config/redis'; +import { AuthRequest } from '../middleware/auth'; + +export const adminController = { + async getDashboard(req: AuthRequest, res: Response) { + try { + // Try cache first + const cacheKey = 'admin:dashboard'; + const cached = await redis.get(cacheKey).catch(() => null); + if (cached) return res.json(JSON.parse(cached)); + + const [users] = await db('users').count('* as total'); + const [activeUsers] = await db('users').where('status', 'active').count('* as total'); + const [partners] = await db('partners').count('* as total'); + const [activePartners] = await db('partners').where('status', 'active').count('* as total'); + const [benefits] = await db('benefits').count('* as total'); + const [activeBenefits] = await db('benefits').where('active', true).count('* as total'); + const [transactions] = await db('transactions').count('* as total'); + const [plugins] = await db('plugins').count('* as total'); + const [activePlugins] = await db('plugins').where('active', true).count('* as total'); + + // Recent registrations (last 30 days by day) + const recentUsers = await db('users') + .select(db.raw('DATE(created_at) as date')) + .count('* as count') + .where('created_at', '>=', db.raw('DATE_SUB(NOW(), INTERVAL 30 DAY)')) + .groupByRaw('DATE(created_at)') + .orderBy('date'); + + // Top categories + const topCategories = await db('benefits') + .leftJoin('categories', 'benefits.category_id', 'categories.id') + .select('categories.name') + .count('* as count') + .groupBy('categories.name') + .orderBy('count', 'desc') + .limit(10); + + const dashboard = { + stats: { + totalUsers: Number(users.total), + activeUsers: Number(activeUsers.total), + totalPartners: Number(partners.total), + activePartners: Number(activePartners.total), + totalBenefits: Number(benefits.total), + activeBenefits: Number(activeBenefits.total), + totalTransactions: Number(transactions.total), + totalPlugins: Number(plugins.total), + activePlugins: Number(activePlugins.total), + }, + charts: { + recentUsers, + topCategories, + }, + }; + + await redis.setex(cacheKey, 300, JSON.stringify(dashboard)).catch(() => { }); + res.json(dashboard); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar dashboard', details: err.message }); + } + }, + + async getAuditLogs(req: AuthRequest, res: Response) { + try { + const { page = 1, limit = 50, action, userId } = req.query; + const offset = (Number(page) - 1) * Number(limit); + + let query = db('logs') + .leftJoin('users', 'logs.user_id', 'users.id') + .select('logs.*', 'users.name as user_name', 'users.email as user_email'); + + if (action) query = query.where('logs.action', action as string); + if (userId) query = query.where('logs.user_id', userId as string); + + const [{ total }] = await db('logs').count('* as total'); + const logs = await query.orderBy('logs.created_at', 'desc').limit(Number(limit)).offset(offset); + + res.json({ + logs, + pagination: { page: Number(page), limit: Number(limit), total: Number(total) }, + }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar logs' }); + } + }, + + async getMetrics(req: AuthRequest, res: Response) { + try { + const period = (req.query.period as string) || '30d'; + const days = period === '7d' ? 7 : period === '90d' ? 90 : 30; + + const newUsers = await db('users') + .select(db.raw('DATE(created_at) as date')) + .count('* as count') + .where('created_at', '>=', db.raw(`DATE_SUB(NOW(), INTERVAL ${days} DAY)`)) + .groupByRaw('DATE(created_at)') + .orderBy('date'); + + const newPartners = await db('partners') + .select(db.raw('DATE(created_at) as date')) + .count('* as count') + .where('created_at', '>=', db.raw(`DATE_SUB(NOW(), INTERVAL ${days} DAY)`)) + .groupByRaw('DATE(created_at)') + .orderBy('date'); + + const redemptions = await db('transactions') + .select(db.raw('DATE(created_at) as date')) + .count('* as count') + .where('type', 'redemption') + .where('created_at', '>=', db.raw(`DATE_SUB(NOW(), INTERVAL ${days} DAY)`)) + .groupByRaw('DATE(created_at)') + .orderBy('date'); + + res.json({ metrics: { newUsers, newPartners, redemptions } }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar métricas' }); + } + }, +}; diff --git a/backend/src/controllers/auth.controller.ts b/backend/src/controllers/auth.controller.ts index bd96d01..7c9c1a5 100644 --- a/backend/src/controllers/auth.controller.ts +++ b/backend/src/controllers/auth.controller.ts @@ -1,155 +1,183 @@ -import { Request, Response } from 'express'; +import { Response } from 'express'; import bcrypt from 'bcryptjs'; -import { db } from '../config/database'; -import { generateTokens } from '../middleware/auth'; +import db from '../config/database'; +import { AuthRequest, generateTokens } from '../middleware/auth'; import { auditLog } from '../utils/audit'; -export async function register(req: Request, res: Response) { - const { name, email, password, type, whatsapp, address, pj_data } = req.body; - - // Validations (before transaction) - if (!name || !email || !password) { - return res.status(400).json({ error: 'Campos obrigatórios faltando (nome, email, senha)' }); - } - - const trx = await db.transaction(); - try { - const existingUser = await trx('users').where({ email }).first(); - if (existingUser) { - await trx.rollback(); - return res.status(400).json({ error: 'E-mail já cadastrado' }); - } - - const password_hash = await bcrypt.hash(password, 10); - - // Create User - const [userId] = await trx('users').insert({ - name, - email, - password_hash, - whatsapp, - role: type === 'partner' ? 'partner_admin' : 'user', - state: address?.state || null, - // city: address?.city, // Column doesn't exist yet, ignoring to avoid crash - // neighborhood: address?.neighborhood, // Column doesn't exist yet - status: 'active', // Users are active by default (partners rely on partner status) - created_at: new Date(), - updated_at: new Date() - }); - - // Create Partner (PJ) - if (type === 'partner' && pj_data) { - // Check existing CNPJ - if (pj_data.cnpj) { - const existingPartner = await trx('partners').where({ cnpj: pj_data.cnpj }).first(); - if (existingPartner) { - await trx.rollback(); - return res.status(400).json({ error: 'CNPJ já cadastrado' }); - } +export const authController = { + async register(req: AuthRequest, res: Response) { + try { + const { name, email, password, phone, cpf } = req.body; + if (!name || !email || !password) { + return res.status(400).json({ error: 'Nome, email e senha são obrigatórios' }); } - const [partnerId] = await trx('partners').insert({ - company_name: pj_data.fantasy_name, - slug: null, // Removed automatic random slug. User will choose via modal. - cnpj: pj_data.cnpj || null, - email, // Contact email - phone: whatsapp, - address_street: address?.street, - address_number: address?.number, - address_neighborhood: address?.neighborhood, - address_city: address?.city, - address_state: address?.state, - type: String(pj_data.category_id || 'Serviços'), - status: 'inactive', // 'pending' doesn't exist in enum ('active','inactive'), so using 'inactive' as pending approval - owner_user_id: userId, - created_at: new Date(), - updated_at: new Date(), - data_consent: 1 // Implicit consent + const existing = await db('users').where('email', email).first(); + if (existing) { + return res.status(409).json({ error: 'Email já cadastrado' }); + } + + const passwordHash = await bcrypt.hash(password, 12); + const [userId] = await db('users').insert({ + name, email, password_hash: passwordHash, phone, cpf, + role: 'user', status: 'active', email_verified: false, }); - // Link user to partner - await trx('users').where({ id: userId }).update({ partner_id: partnerId }); + const user = await db('users').where('id', userId).first(); + const tokens = generateTokens(user); + await db('users').where('id', userId).update({ refresh_token: tokens.refreshToken }); + + await auditLog(req, { + user_id: userId, action: 'user.register', + entity_type: 'users', entity_id: userId, + description: `Novo usuário registrado: ${email}`, + }); + + const { password_hash: _, refresh_token: __, ...safeUser } = user; + res.status(201).json({ user: safeUser, ...tokens }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao registrar', details: err.message }); } + }, - await trx.commit(); + async login(req: AuthRequest, res: Response) { + try { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ error: 'Email e senha são obrigatórios' }); + } - // Auto-login response - const user = await db('users').where({ id: userId }).first(); - const tokens = generateTokens(user); - const redirectPath = getRedirectPath(user); + const user = await db('users').where('email', email).first(); + if (!user || !user.password_hash) { + return res.status(401).json({ error: 'Credenciais inválidas' }); + } - // Log action (outside transaction) - await auditLog(user.id, 'REGISTER', 'user', user.id, `Novo cadastro: ${type}`, req.ip); + const valid = await bcrypt.compare(password, user.password_hash); + if (!valid) { + return res.status(401).json({ error: 'Credenciais inválidas' }); + } - res.status(201).json({ user, redirectPath, ...tokens }); + const tokens = generateTokens(user); + await db('users').where('id', user.id).update({ + refresh_token: tokens.refreshToken, + last_login: new Date(), + }); - } catch (err: any) { - await trx.rollback(); - console.error('Register Error:', err); - res.status(500).json({ error: 'Erro ao criar conta: ' + (err.message || 'Erro interno') }); - } -} + await auditLog(req, { + user_id: user.id, action: 'user.login', + entity_type: 'users', entity_id: user.id, + description: `Login realizado: ${email}`, + }); -export async function login(req: Request, res: Response) { - try { - const { email, password } = req.body; - const user = await db('users').where({ email }).first(); - if (!user || user.deleted_at || user.status === 'banned') { - return res.status(401).json({ error: 'Credenciais inválidas ou conta desativada' }); + const { password_hash: _, refresh_token: __, ...safeUser } = user; + res.json({ user: safeUser, ...tokens }); + } catch (err: any) { + res.status(500).json({ error: 'Erro no login', details: err.message }); } - if (!(await bcrypt.compare(password, user.password_hash))) { - return res.status(401).json({ error: 'Credenciais inválidas' }); + }, + + async refresh(req: AuthRequest, res: Response) { + try { + const { refreshToken } = req.body; + if (!refreshToken) { + return res.status(400).json({ error: 'Refresh token obrigatório' }); + } + + const jwt = require('jsonwebtoken'); + const { config } = require('../config'); + const decoded = jwt.verify(refreshToken, config.jwt.refreshSecret); + const user = await db('users').where('id', decoded.id).first(); + if (!user || user.refresh_token !== refreshToken) { + return res.status(401).json({ error: 'Refresh token inválido' }); + } + + const tokens = generateTokens(user); + await db('users').where('id', user.id).update({ refresh_token: tokens.refreshToken }); + + res.json(tokens); + } catch { + res.status(401).json({ error: 'Refresh token inválido ou expirado' }); } - const tokens = generateTokens(user); - await auditLog(user.id, 'LOGIN', 'user', user.id, 'Usuário logou no sistema', req.ip); - const redirectPath = getRedirectPath(user); - res.json({ user, redirectPath, ...tokens }); - } catch (err: any) { - res.status(500).json({ error: 'Erro no servidor' }); - } -} + }, -function getRedirectPath(user: any) { - if (user.role === 'super_admin') return '/admin/dashboard'; - if (user.role === 'partner' || user.role === 'partner_admin') { - return '/partner/dashboard'; - } - return '/user/dashboard'; -} - -export async function profile(req: Request, res: Response) { - try { - const user = await db('users') - .select('users.*', 'partners.slug as partner_slug') - .leftJoin('partners', 'users.partner_id', 'partners.id') - .where('users.id', req.user!.id) - .first(); - res.json(user); - } catch (err) { - res.status(500).json({ error: 'Erro ao buscar perfil' }); - } -} - -export async function completeProfile(req: Request, res: Response) { - try { - const userId = req.user!.id; - const { whatsapp, state, city, neighborhood } = req.body || {}; - - if (!whatsapp || !state || !city || !neighborhood) { - return res.status(400).json({ error: 'Campos obrigatórios faltando' }); + async me(req: AuthRequest, res: Response) { + try { + const user = await db('users').where('id', req.user!.id).first(); + if (!user) return res.status(404).json({ error: 'Usuário não encontrado' }); + const { password_hash: _, refresh_token: __, ...safeUser } = user; + res.json({ user: safeUser }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar perfil', details: err.message }); } + }, - await db('users').where({ id: userId }).update({ - whatsapp, - state, - city, - neighborhood, - updated_at: new Date(), - }); + async logout(req: AuthRequest, res: Response) { + try { + await db('users').where('id', req.user!.id).update({ refresh_token: null }); + await auditLog(req, { + user_id: req.user!.id, action: 'user.logout', + entity_type: 'users', entity_id: req.user!.id, + }); + res.json({ message: 'Logout realizado' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro no logout' }); + } + }, - const user = await db('users').where({ id: userId }).first(); - res.json(user); - } catch (err: any) { - res.status(500).json({ error: 'Erro ao completar cadastro' }); - } -} + async googleCallback(req: AuthRequest, res: Response) { + try { + const { token, profile } = req.body; // from frontend google SDK + if (!profile?.email) { + return res.status(400).json({ error: 'Perfil Google inválido' }); + } + + let user = await db('users').where('email', profile.email).first(); + + if (!user) { + const [userId] = await db('users').insert({ + name: profile.name || profile.email.split('@')[0], + email: profile.email, + avatar_url: profile.picture || null, + role: 'user', status: 'active', email_verified: true, + }); + user = await db('users').where('id', userId).first(); + } + + // Link OAuth account + const existingOauth = await db('oauth_accounts') + .where({ provider: 'google', provider_user_id: profile.sub || profile.id }) + .first(); + + if (!existingOauth) { + await db('oauth_accounts').insert({ + user_id: user.id, + provider: 'google', + provider_user_id: profile.sub || profile.id, + access_token: token, + profile_data: JSON.stringify(profile), + }); + } else { + await db('oauth_accounts') + .where('id', existingOauth.id) + .update({ access_token: token, profile_data: JSON.stringify(profile) }); + } + + const tokens = generateTokens(user); + await db('users').where('id', user.id).update({ + refresh_token: tokens.refreshToken, + last_login: new Date(), + }); + + await auditLog(req, { + user_id: user.id, action: 'user.google_login', + entity_type: 'users', entity_id: user.id, + description: `Login Google: ${profile.email}`, + }); + + const { password_hash: _, refresh_token: __, ...safeUser } = user; + res.json({ user: safeUser, ...tokens }); + } catch (err: any) { + res.status(500).json({ error: 'Erro na autenticação Google', details: err.message }); + } + }, +}; diff --git a/backend/src/controllers/benefit.controller.ts b/backend/src/controllers/benefit.controller.ts index 1f031b7..21dc4b0 100644 --- a/backend/src/controllers/benefit.controller.ts +++ b/backend/src/controllers/benefit.controller.ts @@ -1,160 +1,212 @@ -import { Request, Response } from 'express'; -import { db } from '../config/database'; +import { Response } from 'express'; +import { v4 as uuidv4 } from 'uuid'; +import QRCode from 'qrcode'; +import db from '../config/database'; +import { AuthRequest } from '../middleware/auth'; +import { auditLog } from '../utils/audit'; -export async function getAll(req: Request, res: Response) { - // If user is super_admin, return all. If partner, return valid ones (frontend filters but backend should optionally filter too) - // For now, let's return all and let frontend/other controllers filter. - // Ideally we should check role here or receive query param. - // Given the task is for Admin Dashboard, let's return all. - const benefits = await db('benefits').select('benefits.*', 'partners.company_name as partner_name', 'partners.icon as partner_icon', 'partners.slug as partner_slug') - .leftJoin('partners', 'benefits.partner_id', 'partners.id') - .orderBy('created_at', 'desc'); - res.json({ benefits }); -} +export const benefitController = { + async list(req: AuthRequest, res: Response) { + try { + const { page = 1, limit = 20, search, category, type, partnerId, active } = req.query; + const offset = (Number(page) - 1) * Number(limit); -export async function getCategories(req: Request, res: Response) { - const categories = await db('categories'); - res.json({ categories }); -} + let query = db('benefits') + .leftJoin('categories', 'benefits.category_id', 'categories.id') + .leftJoin('partners', 'benefits.partner_id', 'partners.id') + .select( + 'benefits.*', + 'categories.name as category_name', + 'categories.icon as category_icon', + 'partners.company_name as partner_name', + 'partners.slug as partner_slug' + ); -export async function useBenefit(req: Request, res: Response) { - const { id } = req.params; - const userId = req.user!.id; // Assumes auth middleware populates req.user + if (search) query = query.where('benefits.title', 'like', `%${search}%`); + if (category) query = query.where('benefits.category_id', category); + if (type) query = query.where('benefits.type', type); + if (partnerId) query = query.where('benefits.partner_id', partnerId); + if (active !== undefined) query = query.where('benefits.active', active === 'true'); - try { - const benefit = await db('benefits').where({ id }).first(); - if (!benefit) { - return res.status(404).json({ error: 'Benefício não encontrado' }); - } + const countQuery = db('benefits'); + if (search) countQuery.where('title', 'like', `%${search}%`); + const [{ total }] = await countQuery.count('* as total'); - // Check for existing usage today (optional rule, but good practice) - const today = new Date(); - today.setHours(0, 0, 0, 0); + const benefits = await query + .orderBy('benefits.priority', 'asc') + .orderBy('benefits.created_at', 'desc') + .limit(Number(limit)).offset(offset); - const existing = await db('transactions') - .where({ - user_id: userId, - benefit_id: id, - type: 'redemption' - }) - .where('created_at', '>=', today) - .first(); - - if (existing) { - return res.status(409).json({ error: 'Você já utilizou este benefício hoje.' }); - } - - // Create transaction - await db('transactions').insert({ - user_id: userId, - benefit_id: id, - partner_id: benefit.partner_id, - type: 'redemption', - status: 'completed', - created_at: new Date(), - updated_at: new Date() - }); - - // Create or update Lead for Partner - // We check if a lead already exists for this user/partner to avoid duplicates, - // or we just insert a new interaction. - // Admin dashboard counts by ID, so maybe unique leads? - // Let's check if lead exists first. - const existingLead = await db('leads') - .where({ user_id: userId, partner_id: benefit.partner_id }) - .first(); - - if (!existingLead) { - // Fetch user details for lead record - const user = await db('users').where({ id: userId }).first(); - - await db('leads').insert({ - user_id: userId, - partner_id: benefit.partner_id, - name: user.name, - email: user.email, - phone: user.phone, - lead_type: 'member', // As seen in admin.routes.ts - status: 'new', - created_at: new Date(), - updated_at: new Date() + res.json({ + benefits, + pagination: { page: Number(page), limit: Number(limit), total: Number(total), pages: Math.ceil(Number(total) / Number(limit)) }, }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao listar benefícios', details: err.message }); } + }, - // Increment usage count on benefit - await db('benefits').where({ id }).increment('usage_count', 1); + async getById(req: AuthRequest, res: Response) { + try { + const benefit = await db('benefits') + .leftJoin('categories', 'benefits.category_id', 'categories.id') + .leftJoin('partners', 'benefits.partner_id', 'partners.id') + .where('benefits.id', req.params.id) + .select( + 'benefits.*', + 'categories.name as category_name', + 'partners.company_name as partner_name', + 'partners.slug as partner_slug', + 'partners.phone as partner_phone' + ) + .first(); - res.json({ success: true, message: 'Benefício utilizado com sucesso' }); - } catch (error) { - console.error('Erro ao utilizar benefício:', error); - res.status(500).json({ error: 'Erro ao registrar utilização do benefício' }); - } -} - -export async function create(req: Request, res: Response) { - try { - const benefit = await db('benefits').insert(req.body).returning('*'); - res.json({ benefit: benefit[0] }); - } catch (error) { - console.error('Erro ao criar benefício:', error); - res.status(500).json({ error: 'Erro ao criar benefício' }); - } -} - -export async function update(req: Request, res: Response) { - try { - const { id } = req.params; - console.log(`[DEBUG] Updating benefit ${id} with body:`, req.body); - const benefit = await db('benefits').where({ id }).update(req.body).returning('*'); - if (!benefit.length) { - return res.status(404).json({ error: 'Benefício não encontrado' }); + if (!benefit) return res.status(404).json({ error: 'Benefício não encontrado' }); + res.json({ benefit }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar benefício' }); } - res.json({ benefit: benefit[0] }); - } catch (error) { - console.error('Erro ao atualizar benefício:', error); - res.status(500).json({ error: 'Erro ao atualizar benefício' }); - } -} + }, -export async function deleteBenefit(req: Request, res: Response) { - try { - const { id } = req.params; + async create(req: AuthRequest, res: Response) { + try { + const { + partner_id, category_id, title, description, rules, contact, + type, delivery_type, discount_percent, valid_until, + featured_image, redemption_type, priority, badge, + } = req.body; - await db.transaction(async (trx) => { - // Delete related transactions first (or you might want to Soft Delete the benefit instead?) - // Assuming Hard Delete is requested: - await trx('transactions').where({ benefit_id: id }).delete(); - - // Delete the benefit - const deleted = await trx('benefits').where({ id }).delete(); - - if (!deleted) { - throw new Error('Benefício não encontrado'); + if (!partner_id || !title || !type) { + return res.status(400).json({ error: 'parceiro, título e tipo são obrigatórios' }); } - }); - res.json({ success: true }); - } catch (error: any) { - console.error('Erro ao deletar benefício:', error); - if (error.message === 'Benefício não encontrado') { - res.status(404).json({ error: 'Benefício não encontrado' }); - } else { - res.status(500).json({ error: 'Erro ao deletar benefício: ' + error.message }); - } - } -} + const [benefitId] = await db('benefits').insert({ + partner_id, category_id, title, description, rules, contact, + type, delivery_type: delivery_type || 'local', + active: true, is_global: false, approved_by_admin: false, + global_request_status: 'none', priority: priority || 0, + discount_percent, valid_until, featured_image, + redemption_type: redemption_type || 'coupon', badge, + }); -export async function toggle(req: Request, res: Response) { - try { - const { id } = req.params; - const benefit = await db('benefits').where({ id }).first(); - if (!benefit) { - return res.status(404).json({ error: 'Benefício não encontrado' }); + await auditLog(req, { + user_id: req.user!.id, action: 'benefit.create', + entity_type: 'benefits', entity_id: benefitId, + description: `Benefício criado: ${title}`, + }); + + const benefit = await db('benefits').where('id', benefitId).first(); + res.status(201).json({ benefit }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao criar benefício', details: err.message }); } - await db('benefits').where({ id }).update({ active: !benefit.active }); - res.json({ success: true, active: !benefit.active }); - } catch (error) { - console.error('Erro ao alternar status do benefício:', error); - res.status(500).json({ error: 'Erro ao alternar status' }); - } -} + }, + + async update(req: AuthRequest, res: Response) { + try { + const { id } = req.params; + const allowed = [ + 'title', 'description', 'rules', 'contact', 'type', 'category_id', + 'delivery_type', 'active', 'priority', 'discount_percent', + 'valid_until', 'featured_image', 'redemption_type', 'badge', + ]; + const updates: any = {}; + for (const key of allowed) { + if (req.body[key] !== undefined) updates[key] = req.body[key]; + } + + await db('benefits').where('id', id).update(updates); + await auditLog(req, { + user_id: req.user!.id, action: 'benefit.update', + entity_type: 'benefits', entity_id: Number(id), + new_data: updates, + }); + + const benefit = await db('benefits').where('id', id).first(); + res.json({ benefit }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao atualizar benefício' }); + } + }, + + async delete(req: AuthRequest, res: Response) { + try { + await db('benefits').where('id', req.params.id).delete(); + await auditLog(req, { + user_id: req.user!.id, action: 'benefit.delete', + entity_type: 'benefits', entity_id: Number(req.params.id), + }); + res.json({ message: 'Benefício removido' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao remover benefício' }); + } + }, + + async approve(req: AuthRequest, res: Response) { + try { + await db('benefits').where('id', req.params.id).update({ approved_by_admin: true }); + await auditLog(req, { + user_id: req.user!.id, action: 'benefit.approve', + entity_type: 'benefits', entity_id: Number(req.params.id), + }); + res.json({ message: 'Benefício aprovado' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao aprovar benefício' }); + } + }, + + async promoteGlobal(req: AuthRequest, res: Response) { + try { + await db('benefits').where('id', req.params.id).update({ + is_global: true, approved_by_admin: true, global_request_status: 'approved', + }); + res.json({ message: 'Benefício promovido a global' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao promover benefício' }); + } + }, + + async redeem(req: AuthRequest, res: Response) { + try { + const benefitId = Number(req.params.id); + const benefit = await db('benefits').where('id', benefitId).first(); + if (!benefit) return res.status(404).json({ error: 'Benefício não encontrado' }); + + const code = `CB67-${uuidv4().substring(0, 8).toUpperCase()}`; + const qrData = JSON.stringify({ code, benefitId, userId: req.user!.id, ts: Date.now() }); + const qrCodeImage = await QRCode.toDataURL(qrData); + + const [transactionId] = await db('transactions').insert({ + user_id: req.user!.id, + benefit_id: benefitId, + partner_id: benefit.partner_id, + type: 'redemption', + status: 'pending', + redemption_code: code, + qr_code_data: qrData, + discount_value: benefit.discount_percent, + expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000), + }); + + await db('benefits').where('id', benefitId).increment('usage_count', 1); + + res.json({ + transaction: { + id: transactionId, code, qrCode: qrCodeImage, + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), + }, + }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao resgatar benefício', details: err.message }); + } + }, + + async getCategories(req: AuthRequest, res: Response) { + try { + const categories = await db('categories').where('active', true).orderBy('sort_order'); + res.json({ categories }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar categorias' }); + } + }, +}; diff --git a/backend/src/controllers/lead.controller.ts b/backend/src/controllers/lead.controller.ts deleted file mode 100644 index e51a6ea..0000000 --- a/backend/src/controllers/lead.controller.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Request, Response } from 'express'; -import { db } from '../config/database'; - -export async function getAll(req: Request, res: Response) { - try { - const isSuperAdmin = req.user?.role === 'super_admin'; - const partnerId = req.user?.partner_id; - - if (!isSuperAdmin && !partnerId) { - return res.status(403).json({ error: 'Acesso restrito a parceiros' }); - } - - const query = db('leads'); - if (!isSuperAdmin) { - query.where({ partner_id: partnerId }); - } - - const leads = await query.orderBy('created_at', 'desc'); - - // Enhance leads with details - const enhancedLeads = await Promise.all(leads.map(async (lead) => { - // Get user details if registered - let userDetails = {}; - if (lead.user_id) { - const user = await db('users').where({ id: lead.user_id }).first(); - if (user) { - userDetails = { - neighborhood: user.neighborhood, - avatar_url: user.avatar_url - }; - } - } - - // Get last benefit used - const transactionQuery = db('transactions') - .join('benefits', 'transactions.benefit_id', 'benefits.id') - .where({ 'transactions.user_id': lead.user_id }); - - if (!isSuperAdmin) { - transactionQuery.where({ 'transactions.partner_id': partnerId }); - } - - const lastTransaction = await transactionQuery - .orderBy('transactions.created_at', 'desc') - .select('benefits.title as benefit_title', 'benefits.id as benefit_id') - .first(); - - return { - ...lead, - ...userDetails, - benefit_title: lastTransaction?.benefit_title || 'N/A', - benefit_id: lastTransaction?.benefit_id - }; - })); - - res.json({ leads: enhancedLeads }); - } catch (error) { - console.error('Erro ao buscar leads:', error); - res.status(500).json({ error: 'Erro ao buscar leads' }); - } -} - -export async function updateStatus(req: Request, res: Response) { - try { - const { id } = req.params; - const { status } = req.body; - const isSuperAdmin = req.user?.role === 'super_admin'; - const partnerId = req.user?.partner_id; - - if (!isSuperAdmin && !partnerId) { - return res.status(403).json({ error: 'Acesso restrito a parceiros' }); - } - - const query = db('leads').where({ id }); - if (!isSuperAdmin) { - query.andWhere({ partner_id: partnerId }); - } - - const updated = await query.update({ status, updated_at: new Date() }); - - if (!updated) { - return res.status(404).json({ error: 'Lead não encontrado' }); - } - - res.json({ success: true }); - } catch (error) { - console.error('Erro ao atualizar lead:', error); - res.status(500).json({ error: 'Erro ao atualizar lead' }); - } -} diff --git a/backend/src/controllers/partner.controller.ts b/backend/src/controllers/partner.controller.ts index d4b1da2..6cd4311 100644 --- a/backend/src/controllers/partner.controller.ts +++ b/backend/src/controllers/partner.controller.ts @@ -1,167 +1,167 @@ -import { Request, Response } from 'express'; -import { db } from '../config/database'; +import { Response } from 'express'; +import db from '../config/database'; +import { AuthRequest } from '../middleware/auth'; +import { auditLog } from '../utils/audit'; -export async function getAll(req: Request, res: Response) { - const partners = await db('partners').where({ status: 'active' }); - res.json({ partners }); -} +export const partnerController = { + async list(req: AuthRequest, res: Response) { + try { + const { page = 1, limit = 20, search, status, type } = req.query; + const offset = (Number(page) - 1) * Number(limit); -export async function getBySlug(req: Request, res: Response) { - const { slug } = req.params; + let query = db('partners'); + if (search) query = query.where('company_name', 'like', `%${search}%`); + if (status) query = query.where('status', status as string); + if (type) query = query.where('type', type as string); - try { - const partner = await db('partners') - .where({ slug, status: 'active' }) - .first(); + const countQuery = query.clone(); + const [{ total }] = await countQuery.count('* as total'); + const partners = await query.orderBy('created_at', 'desc').limit(Number(limit)).offset(offset); - if (!partner) { - return res.status(404).json({ error: 'Parceiro não encontrado' }); + res.json({ + partners, + pagination: { page: Number(page), limit: Number(limit), total: Number(total), pages: Math.ceil(Number(total) / Number(limit)) }, + }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao listar parceiros', details: err.message }); } + }, - const benefits = await db('benefits') - .where({ partner_id: partner.id, active: true }) - .orderBy('created_at', 'desc'); + async getById(req: AuthRequest, res: Response) { + try { + const partner = await db('partners').where('id', req.params.id).first(); + if (!partner) return res.status(404).json({ error: 'Parceiro não encontrado' }); - res.json({ partner, benefits }); - } catch (error) { - console.error('Erro ao buscar parceiro por slug:', error); - res.status(500).json({ error: 'Erro ao buscar parceiro' }); - } -} - -export async function getById(req: Request, res: Response) { - const { id } = req.params; - - try { - const partner = await db('partners') - .where({ id }) - .first(); - - if (!partner) { - return res.status(404).json({ error: 'Parceiro não encontrado' }); + const benefits = await db('benefits').where('partner_id', partner.id).where('active', true); + res.json({ partner, benefits }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar parceiro', details: err.message }); } + }, - // Optional: privacy check? Public profile vs Owner? - // For now, return public info + specific owner info if needed. - // The frontend uses this for "Edit Profile", so we need all editable fields. + async getBySlug(req: AuthRequest, res: Response) { + try { + const partner = await db('partners').where('slug', req.params.slug).first(); + if (!partner) return res.status(404).json({ error: 'Parceiro não encontrado' }); - res.json({ partner }); - } catch (error) { - console.error('Erro ao buscar parceiro por ID:', error); - res.status(500).json({ error: 'Erro ao buscar parceiro' }); - } -} - -export async function updatePartner(req: Request, res: Response) { - const { id } = req.params; - const { - company_name, - description, - logo_url, - banner_url, - instagram, - website, - address_street, - address_number, - address_neighborhood, - address_city, - address_state, - phone, // WhatsApp - logo_bg_color - } = req.body; - - try { - const userId = req.user!.id; // Authenticated user - const userRole = req.user!.role; - - const partner = await db('partners').where({ id }).first(); - - if (!partner) { - return res.status(404).json({ error: 'Parceiro não encontrado' }); + const benefits = await db('benefits').where('partner_id', partner.id).where('active', true); + res.json({ partner, benefits }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar parceiro' }); } + }, - // Authorization: Only Owner or Super Admin - if (userRole !== 'super_admin' && partner.owner_user_id !== userId) { - return res.status(403).json({ error: 'Você não tem permissão para editar este parceiro.' }); - } + async create(req: AuthRequest, res: Response) { + try { + const { + company_name, slug, cnpj, email, phone, + address_street, address_number, address_neighborhood, + address_city, address_state, address_zip, + type, monthly_goal, description, + } = req.body; - // Generate Slug if company name changes? - // Better not to change slug automatically to avoid breaking links. - // If needed, we can add a check. For now, let's keep slug stable. - - const updateData: any = {}; - if (company_name !== undefined) updateData.company_name = company_name; - if (description !== undefined) updateData.description = description; - if (logo_url !== undefined) updateData.logo_url = logo_url; - if (logo_bg_color !== undefined) updateData.logo_bg_color = logo_bg_color; - if (banner_url !== undefined) updateData.banner_url = banner_url; - if (instagram !== undefined) updateData.instagram = instagram; - if (website !== undefined) updateData.website = website; - if (address_street !== undefined) updateData.address_street = address_street; - if (address_number !== undefined) updateData.address_number = address_number; - if (address_neighborhood !== undefined) updateData.address_neighborhood = address_neighborhood; - if (address_city !== undefined) updateData.address_city = address_city; - if (address_state !== undefined) updateData.address_state = address_state; - if (phone !== undefined) updateData.phone = phone; - - updateData.updated_at = new Date(); - - if (Object.keys(updateData).length > 0) { - await db('partners').where({ id }).update(updateData); - } - - res.json({ message: 'Parceiro atualizado com sucesso' }); - - } catch (error) { - console.error('Erro ao atualizar parceiro:', error); - res.status(500).json({ error: 'Erro ao atualizar parceiro' }); - } -} - -export async function deletePartner(req: Request, res: Response) { - const { id } = req.params; - - try { - await db.transaction(async (trx) => { - // Delete related benefits (which deletes their dependent records like uses, clicks, etc.) - // We need to fetch benefit IDs first to delete their dependencies properly if not using CASCADE in DB - // However, our benefit delete logic is in another controller/plugin. - // For a direct DB delete here, we must clean up manually or trust DB constraints. - // Given the issues with benefits, let's delete benefit dependencies first. - - const benefits = await trx('benefits').where({ partner_id: id }).select('id'); - const benefitIds = benefits.map(b => b.id); - - if (benefitIds.length > 0) { - await trx('benefit_matches').whereIn('benefit_id', benefitIds).delete(); - await trx('benefit_clicks').whereIn('benefit_id', benefitIds).delete(); - await trx('benefit_uses').whereIn('benefit_id', benefitIds).delete(); - try { - await trx('transactions').whereIn('benefit_id', benefitIds).delete(); - } catch (e) { /* ignore */ } - await trx('benefits').whereIn('id', benefitIds).delete(); + if (!company_name || !slug || !type) { + return res.status(400).json({ error: 'Nome, slug e tipo são obrigatórios' }); } - // Delete Leads - await trx('leads').where({ partner_id: id }).delete(); + const existingSlug = await db('partners').where('slug', slug).first(); + if (existingSlug) return res.status(409).json({ error: 'Slug já existe' }); - // Finally delete Partner - // Note: Does not delete the User account, just the Partner profile. - // If the user should revert to 'user' role, we should update that. - // Let's keep the user but unlink/delete partner profile. + const [partnerId] = await db('partners').insert({ + company_name, slug, cnpj, email, phone, + address_street, address_number, address_neighborhood, + address_city, address_state, address_zip, + type, monthly_goal: monthly_goal || 0, + description, status: 'active', + owner_user_id: req.user!.id, + }); - const partner = await trx('partners').where({ id }).first(); - if (partner && partner.owner_user_id) { - await trx('users').where({ id: partner.owner_user_id }).update({ role: 'user' }); + await auditLog(req, { + user_id: req.user!.id, action: 'partner.create', + entity_type: 'partners', entity_id: partnerId, + description: `Parceiro criado: ${company_name}`, + }); + + const partner = await db('partners').where('id', partnerId).first(); + res.status(201).json({ partner }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao criar parceiro', details: err.message }); + } + }, + + async update(req: AuthRequest, res: Response) { + try { + const { id } = req.params; + const allowed = [ + 'company_name', 'cnpj', 'email', 'phone', 'description', 'type', + 'address_street', 'address_number', 'address_neighborhood', + 'address_city', 'address_state', 'address_zip', + 'monthly_goal', 'status', 'logo_url', 'images', + 'gradient_from', 'gradient_to', 'icon', 'subscription_plan', + 'domain', 'subdomain', + ]; + const updates: any = {}; + for (const key of allowed) { + if (req.body[key] !== undefined) updates[key] = req.body[key]; } - const deleted = await trx('partners').where({ id }).delete(); - if (!deleted) throw new Error('Parceiro não encontrado'); - }); + await db('partners').where('id', id).update(updates); + await auditLog(req, { + user_id: req.user!.id, action: 'partner.update', + entity_type: 'partners', entity_id: Number(id), + new_data: updates, + }); - res.json({ message: 'Parceiro excluído com sucesso' }); - } catch (error: any) { - console.error('Erro ao excluir parceiro:', error); - res.status(500).json({ error: error.message || 'Erro ao excluir parceiro' }); - } -} + const partner = await db('partners').where('id', id).first(); + res.json({ partner }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao atualizar parceiro' }); + } + }, + + async delete(req: AuthRequest, res: Response) { + try { + const { id } = req.params; + await db('partners').where('id', id).delete(); + await auditLog(req, { + user_id: req.user!.id, action: 'partner.delete', + entity_type: 'partners', entity_id: Number(id), + }); + res.json({ message: 'Parceiro removido' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao remover parceiro' }); + } + }, + + async uploadLogo(req: AuthRequest, res: Response) { + try { + if (!req.file) return res.status(400).json({ error: 'Arquivo não fornecido' }); + const logoUrl = `/uploads/${req.file.filename}`; + await db('partners').where('id', req.params.id).update({ logo_url: logoUrl }); + res.json({ logo_url: logoUrl }); + } catch (err: any) { + res.status(500).json({ error: 'Erro no upload' }); + } + }, + + async getDashboardStats(req: AuthRequest, res: Response) { + try { + const partnerId = Number(req.params.id); + const [benefits] = await db('benefits').where('partner_id', partnerId).count('* as count'); + const [activeB] = await db('benefits').where({ partner_id: partnerId, active: true }).count('* as count'); + const [transactions] = await db('transactions').where('partner_id', partnerId).count('* as count'); + const [totalUsage] = await db('benefits').where('partner_id', partnerId).sum('usage_count as total'); + + res.json({ + stats: { + totalBenefits: Number(benefits.count), + activeBenefits: Number(activeB.count), + totalTransactions: Number(transactions.count), + totalUsage: Number(totalUsage.total) || 0, + }, + }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar estatísticas do parceiro' }); + } + }, +}; diff --git a/backend/src/controllers/referral.controller.ts b/backend/src/controllers/referral.controller.ts deleted file mode 100644 index 63bf4c1..0000000 --- a/backend/src/controllers/referral.controller.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Request, Response } from 'express'; -import { db } from '../config/database'; - -// Registra um clique em link de indicacao -export async function trackClick(req: Request, res: Response) { - try { - const { ref, benefit_id } = req.body; - if (!ref || !benefit_id) { - return res.status(400).json({ error: 'ref e benefit_id sao obrigatorios' }); - } - - // Busca ou cria o registro de referral - const existing = await db('referrals') - .where({ referral_code: ref, benefit_id }) - .first(); - - if (existing) { - await db('referrals') - .where({ id: existing.id }) - .update({ status: 'clicked', clicked_at: new Date(), updated_at: new Date() }); - } else { - // Decodifica o user id do codigo REF - const userIdStr = ref.replace('REF', ''); - const referrerUserId = parseInt(userIdStr, 36); - - if (isNaN(referrerUserId)) { - return res.status(400).json({ error: 'Codigo de referral invalido' }); - } - - await db('referrals').insert({ - referrer_user_id: referrerUserId, - benefit_id, - referral_code: ref, - status: 'clicked', - clicked_at: new Date(), - created_at: new Date(), - updated_at: new Date(), - }); - } - - res.json({ success: true }); - } catch (error) { - console.error('Erro ao registrar clique de referral:', error); - res.status(500).json({ error: 'Erro ao registrar clique' }); - } -} - -// Estatisticas de indicacoes do usuario logado -export async function getMyStats(req: Request, res: Response) { - try { - const userId = req.user!.id; - - // Total de indicacoes - const [totalRow] = await db('referrals') - .where({ referrer_user_id: userId }) - .count('id as total'); - - // Por status - const byStatus = await db('referrals') - .where({ referrer_user_id: userId }) - .select('status') - .count('id as count') - .groupBy('status'); - - // Top beneficios indicados - const topBenefits = await db('referrals') - .where({ 'referrals.referrer_user_id': userId }) - .join('benefits', 'referrals.benefit_id', 'benefits.id') - .leftJoin('partners', 'benefits.partner_id', 'partners.id') - .select( - 'benefits.id as benefit_id', - 'benefits.title', - 'partners.company_name as partner_name', - 'partners.icon as partner_icon' - ) - .count('referrals.id as total_referrals') - .sum({ conversions: db.raw("CASE WHEN referrals.status = 'converted' THEN 1 ELSE 0 END") }) - .groupBy('benefits.id', 'benefits.title', 'partners.company_name', 'partners.icon') - .orderBy('total_referrals', 'desc') - .limit(10); - - // Historico recente - const recent = await db('referrals') - .where({ 'referrals.referrer_user_id': userId }) - .join('benefits', 'referrals.benefit_id', 'benefits.id') - .leftJoin('partners', 'benefits.partner_id', 'partners.id') - .select( - 'referrals.id', - 'referrals.status', - 'referrals.referral_code', - 'referrals.clicked_at', - 'referrals.converted_at', - 'referrals.created_at', - 'benefits.title as benefit_title', - 'partners.company_name as partner_name' - ) - .orderBy('referrals.created_at', 'desc') - .limit(50); - - const statusMap: Record = {}; - byStatus.forEach((row: any) => { - statusMap[row.status] = Number(row.count); - }); - - res.json({ - total: Number(totalRow.total), - clicked: statusMap.clicked || 0, - converted: statusMap.converted || 0, - pending: statusMap.pending || 0, - topBenefits, - recent, - }); - } catch (error) { - console.error('Erro ao buscar stats de referral:', error); - res.status(500).json({ error: 'Erro ao buscar estatisticas' }); - } -} - -// Converte uma indicacao (chamado quando o indicado realiza acao) -export async function convert(req: Request, res: Response) { - try { - const { ref, benefit_id, referred_user_id } = req.body; - if (!ref || !benefit_id) { - return res.status(400).json({ error: 'ref e benefit_id sao obrigatorios' }); - } - - const updated = await db('referrals') - .where({ referral_code: ref, benefit_id }) - .whereNot({ status: 'converted' }) - .update({ - status: 'converted', - referred_user_id: referred_user_id || null, - converted_at: new Date(), - updated_at: new Date(), - }); - - if (!updated) { - return res.status(404).json({ error: 'Referral nao encontrado ou ja convertido' }); - } - - res.json({ success: true }); - } catch (error) { - console.error('Erro ao converter referral:', error); - res.status(500).json({ error: 'Erro ao converter indicacao' }); - } -} diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts index 068828b..bba31a0 100644 --- a/backend/src/controllers/user.controller.ts +++ b/backend/src/controllers/user.controller.ts @@ -1,120 +1,180 @@ -import { Request, Response } from 'express'; -import { db } from '../config/database'; +import { Response } from 'express'; +import db from '../config/database'; +import { AuthRequest } from '../middleware/auth'; +import { auditLog } from '../utils/audit'; -export async function getAll(req: Request, res: Response) { - const users = await db('users') - .where('is_anonymized', false) - .select('id', 'name', 'email', 'role', 'status', 'created_at'); - res.json({ users }); -} +export const userController = { + async list(req: AuthRequest, res: Response) { + try { + const { page = 1, limit = 20, search, role, status } = req.query; + const offset = (Number(page) - 1) * Number(limit); -export async function getStats(req: Request, res: Response) { - const [totalBenefits] = await db('benefits').count('id as count'); - const [totalFavorites] = await db('favorites').where({ user_id: req.user!.id }).count('id as count'); - res.json({ totalBenefits: totalBenefits.count, totalFavorites: totalFavorites.count }); -} + let query = db('users').select( + 'id', 'name', 'email', 'phone', 'role', 'status', 'avatar_url', + 'city', 'neighborhood', 'gender', 'last_login', 'created_at' + ); -export async function getMyBenefits(req: Request, res: Response) { - try { - const history = await db('transactions') - .join('benefits', 'transactions.benefit_id', 'benefits.id') - .join('partners', 'benefits.partner_id', 'partners.id') - .leftJoin('leads', function () { - this.on('leads.user_id', '=', 'transactions.user_id') - .andOn('leads.partner_id', '=', 'partners.id'); - }) - .where({ 'transactions.user_id': req.user!.id }) - .select( - 'transactions.id as transaction_id', - 'transactions.status as transaction_status', - 'transactions.created_at as used_at', - 'benefits.id as benefit_id', - 'benefits.title', - 'benefits.description', - 'benefits.partner_id', - 'partners.company_name as partner_name', - 'partners.icon as partner_icon', - 'leads.status as lead_status' - ) - .orderBy('transactions.created_at', 'desc'); + if (search) query = query.where(function () { + this.where('name', 'like', `%${search}%`) + .orWhere('email', 'like', `%${search}%`); + }); + if (role) query = query.where('role', role as string); + if (status) query = query.where('status', status as string); - res.json({ history }); - } catch (error) { - console.error('Erro ao buscar histórico:', error); - res.status(500).json({ error: 'Erro ao buscar histórico' }); - } -} + const [{ total }] = await db('users').count('* as total'); + const users = await query.orderBy('created_at', 'desc').limit(Number(limit)).offset(offset); -// --- Admin Actions --- + res.json({ + users, + pagination: { page: Number(page), limit: Number(limit), total: Number(total), pages: Math.ceil(Number(total) / Number(limit)) }, + }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao listar usuários', details: err.message }); + } + }, -export async function updateUser(req: Request, res: Response) { - const { id } = req.params; - const { name, email, role } = req.body; + async getById(req: AuthRequest, res: Response) { + try { + const user = await db('users') + .where('id', req.params.id) + .select('id', 'name', 'email', 'phone', 'cpf', 'role', 'status', 'avatar_url', + 'city', 'neighborhood', 'gender', 'birth_date', 'last_login', 'created_at') + .first(); + if (!user) return res.status(404).json({ error: 'Usuário não encontrado' }); + res.json({ user }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar usuário', details: err.message }); + } + }, - try { - const targetUser = await db('users').where({ id }).first(); - - // 🛡️ SECURITY: Prevent Super Admin Demotion - if (targetUser.role === 'super_admin') { - // Cannot change role of a super_admin - if (role && role !== 'super_admin') { - return res.status(403).json({ error: 'NÃO É PERMITIDO REBAIXAR UM SUPER ADMIN.' }); + async update(req: AuthRequest, res: Response) { + try { + const { id } = req.params; + const allowed = ['name', 'phone', 'city', 'neighborhood', 'gender', 'birth_date', 'avatar_url']; + const updates: any = {}; + for (const key of allowed) { + if (req.body[key] !== undefined) updates[key] = req.body[key]; } - } - - // 🛡️ SECURITY: Protect specific email - if (targetUser.email === 'ruibto@gmail.com') { - if (role && role !== 'super_admin') { - return res.status(403).json({ error: 'Este usuário é o Dono do Sistema e não pode ter funções alteradas.' }); + if (Object.keys(updates).length === 0) { + return res.status(400).json({ error: 'Nenhum campo para atualizar' }); } + + // Only allow users to update themselves, or admins to update anyone + if (req.user!.role !== 'super_admin' && req.user!.id !== Number(id)) { + return res.status(403).json({ error: 'Sem permissão' }); + } + + await db('users').where('id', id).update(updates); + await auditLog(req, { + user_id: req.user!.id, action: 'user.update', + entity_type: 'users', entity_id: Number(id), + new_data: updates, + }); + + const user = await db('users').where('id', id).select('id', 'name', 'email', 'phone', 'role', 'status', 'avatar_url', 'city').first(); + res.json({ user }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao atualizar', details: err.message }); } + }, - // Filter undefined fields to avoid SQL errors or accidental nulls - const updateData: any = {}; - if (name !== undefined) updateData.name = name; - if (email !== undefined) updateData.email = email; - if (role !== undefined) updateData.role = role; - - if (Object.keys(updateData).length > 0) { - await db('users').where({ id }).update(updateData); + async updateRole(req: AuthRequest, res: Response) { + try { + const { id } = req.params; + const { role } = req.body; + const validRoles = ['user', 'partner_admin', 'secretary', 'clinical_manager', 'finance', 'super_admin']; + if (!validRoles.includes(role)) { + return res.status(400).json({ error: 'Role inválida' }); + } + await db('users').where('id', id).update({ role }); + await auditLog(req, { + user_id: req.user!.id, action: 'user.role_change', + entity_type: 'users', entity_id: Number(id), + new_data: { role }, + }); + res.json({ message: 'Role atualizada' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao atualizar role' }); } + }, - res.json({ message: 'Usuário atualizado com sucesso' }); - } catch (error) { - console.error('Erro ao atualizar usuário:', error); - res.status(500).json({ error: 'Erro ao atualizar usuário' }); - } -} + async delete(req: AuthRequest, res: Response) { + try { + const { id } = req.params; + await db('users').where('id', id).delete(); + await auditLog(req, { + user_id: req.user!.id, action: 'user.delete', + entity_type: 'users', entity_id: Number(id), + }); + res.json({ message: 'Usuário removido' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao remover usuário' }); + } + }, -export async function updateStatus(req: Request, res: Response) { - const { id } = req.params; - const { status } = req.body; // 'active', 'blocked', 'banned' + async getFavorites(req: AuthRequest, res: Response) { + try { + const favorites = await db('favorites') + .join('benefits', 'favorites.benefit_id', 'benefits.id') + .where('favorites.user_id', req.user!.id) + .select('benefits.*', 'favorites.id as favorite_id', 'favorites.created_at as favorited_at'); + res.json({ favorites }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar favoritos' }); + } + }, - if (!['active', 'blocked', 'banned'].includes(status)) { - return res.status(400).json({ error: 'Status inválido' }); - } + async addFavorite(req: AuthRequest, res: Response) { + try { + const { benefitId } = req.body; + const exists = await db('favorites').where({ user_id: req.user!.id, benefit_id: benefitId }).first(); + if (exists) return res.status(409).json({ error: 'Já favoritado' }); + await db('favorites').insert({ user_id: req.user!.id, benefit_id: benefitId }); + res.status(201).json({ message: 'Favorito adicionado' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao adicionar favorito' }); + } + }, - try { - await db('users').where({ id }).update({ status }); - res.json({ message: `Status alterado para ${status}` }); - } catch (error) { - console.error('Erro ao atualizar status:', error); - res.status(500).json({ error: 'Erro ao atualizar status' }); - } -} + async removeFavorite(req: AuthRequest, res: Response) { + try { + await db('favorites').where({ user_id: req.user!.id, benefit_id: req.params.benefitId }).delete(); + res.json({ message: 'Favorito removido' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao remover favorito' }); + } + }, -export async function deleteUser(req: Request, res: Response) { - const { id } = req.params; - const adminId = req.user?.id; // Assuming auth middleware provides logged in admin ID + async getHistory(req: AuthRequest, res: Response) { + try { + const transactions = await db('transactions') + .join('benefits', 'transactions.benefit_id', 'benefits.id') + .where('transactions.user_id', req.user!.id) + .select('transactions.*', 'benefits.title as benefit_title') + .orderBy('transactions.created_at', 'desc') + .limit(50); + res.json({ transactions }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar histórico' }); + } + }, - try { - const { UserLifecycleService } = await import('../services/UserLifecycleService'); + async getDashboardStats(req: AuthRequest, res: Response) { + try { + const userId = req.user!.id; + const [favorites] = await db('favorites').where('user_id', userId).count('* as count'); + const [transactions] = await db('transactions').where('user_id', userId).count('* as count'); + const [savings] = await db('transactions').where('user_id', userId).sum('discount_value as total'); - await UserLifecycleService.anonymizeUser(Number(id), Number(adminId)); - - res.json({ message: 'Usuário excluído e anonimizado com sucesso conforme LGPD.' }); - } catch (error: any) { - console.error('Erro ao excluir usuário:', error); - res.status(500).json({ error: error.message || 'Erro ao excluir usuário' }); - } -} + res.json({ + stats: { + totalFavorites: Number(favorites.count), + totalTransactions: Number(transactions.count), + totalSavings: Number(savings.total) || 0, + }, + }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao buscar estatísticas' }); + } + }, +}; diff --git a/backend/src/controllers/webhook.controller.ts b/backend/src/controllers/webhook.controller.ts new file mode 100644 index 0000000..23b7e0d --- /dev/null +++ b/backend/src/controllers/webhook.controller.ts @@ -0,0 +1,112 @@ +import { Response } from 'express'; +import db from '../config/database'; +import { AuthRequest } from '../middleware/auth'; +import { auditLog } from '../utils/audit'; +import { v4 as uuidv4 } from 'uuid'; +import crypto from 'crypto'; + +export const webhookController = { + async list(req: AuthRequest, res: Response) { + try { + const webhooks = await db('webhooks').orderBy('created_at', 'desc'); + res.json({ webhooks }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao listar webhooks' }); + } + }, + + async create(req: AuthRequest, res: Response) { + try { + const { url, events, partner_id } = req.body; + if (!url || !events) { + return res.status(400).json({ error: 'URL e eventos são obrigatórios' }); + } + + const secret = crypto.randomBytes(32).toString('hex'); + const [id] = await db('webhooks').insert({ + url, events: JSON.stringify(events), secret, + partner_id: partner_id || null, active: true, + }); + + await auditLog(req, { + user_id: req.user!.id, action: 'webhook.create', + entity_type: 'webhooks', entity_id: id, + }); + + res.status(201).json({ webhook: { id, url, events, secret } }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao criar webhook' }); + } + }, + + async update(req: AuthRequest, res: Response) { + try { + const { id } = req.params; + const { url, events, active } = req.body; + const updates: any = {}; + if (url) updates.url = url; + if (events) updates.events = JSON.stringify(events); + if (active !== undefined) updates.active = active; + + await db('webhooks').where('id', id).update(updates); + res.json({ message: 'Webhook atualizado' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao atualizar webhook' }); + } + }, + + async delete(req: AuthRequest, res: Response) { + try { + await db('webhooks').where('id', req.params.id).delete(); + res.json({ message: 'Webhook removido' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao remover webhook' }); + } + }, + + async test(req: AuthRequest, res: Response) { + try { + const webhook = await db('webhooks').where('id', req.params.id).first(); + if (!webhook) return res.status(404).json({ error: 'Webhook não encontrado' }); + + // Send test event + const payload = { + event: 'test', + timestamp: new Date().toISOString(), + data: { message: 'Webhook test from Clube de Benefícios' }, + }; + + const signature = crypto + .createHmac('sha256', webhook.secret) + .update(JSON.stringify(payload)) + .digest('hex'); + + try { + const response = await fetch(webhook.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CB67-Signature': signature, + }, + body: JSON.stringify(payload), + }); + + await db('webhooks').where('id', webhook.id).update({ + last_triggered: new Date(), + last_status: response.ok ? 'success' : 'failed', + }); + + res.json({ success: response.ok, status: response.status }); + } catch (error) { + await db('webhooks').where('id', webhook.id).update({ + last_triggered: new Date(), + last_status: 'failed', + retry_count: webhook.retry_count + 1, + }); + res.json({ success: false, error: 'Falha ao enviar webhook' }); + } + } catch (err: any) { + res.status(500).json({ error: 'Erro ao testar webhook' }); + } + }, +}; diff --git a/backend/src/core/StorageProvider.ts b/backend/src/core/StorageProvider.ts deleted file mode 100644 index b34c207..0000000 --- a/backend/src/core/StorageProvider.ts +++ /dev/null @@ -1,82 +0,0 @@ -// ============================================================ -// Clube67 — Central Storage Provider Interface -// ============================================================ -// This service acts as a bridge between the system and the -// active storage plugin (Wasabi, Local, etc). - -import { logger } from '../utils/logger'; - -export interface StorageUploadPayload { - clinicId?: string; - patientId?: string; - partnerId?: string; - benefitId?: string; - postId?: string; - whatsappId?: string; - userId?: string; - category: 'cdi' | 'xrays' | 'documents' | 'exports' | 'posts' | 'cards' | 'partners' | 'whatsapp' | 'media'; - file: { - originalname: string; - buffer: Buffer; - mimetype: string; - }; -} - -export interface StorageProviderInterface { - upload(payload: StorageUploadPayload): Promise<{ provider: string; path: string }>; - checkHealth(): Promise; -} - -class StorageProvider { - private provider: StorageProviderInterface | null = null; - private healthy: boolean = true; - - /** Register a storage provider plugin */ - register(provider: StorageProviderInterface): void { - this.provider = provider; - logger.info(`[StorageProvider] Central provider registered: ${provider.constructor.name}`); - // Initial health check - this.updateHealth(); - } - - /** Periodically update health status */ - async updateHealth(): Promise { - if (!this.provider) { - this.healthy = false; - return false; - } - try { - this.healthy = await this.provider.checkHealth(); - if (!this.healthy) logger.warn('[StorageProvider] Provider reported UNHEALTHY status'); - return this.healthy; - } catch (err) { - this.healthy = false; - logger.error('[StorageProvider] Health check failed with error'); - return false; - } - } - - /** Is the storage system available? */ - isAvailable(): boolean { - return this.healthy && this.provider !== null; - } - - /** Upload a file using the registered provider */ - async uploadFile(payload: StorageUploadPayload) { - if (!this.provider) { - throw new Error('[StorageProvider] No storage provider registered. Did you enable the UnifiedStorageProvider plugin?'); - } - if (!this.healthy) { - throw new Error('[StorageProvider] Storage system is currently UNAVAILABLE'); - } - return this.provider.upload(payload); - } -} - -// Singleton -export const storageProvider = new StorageProvider(); - -/** Helper function for easier imports */ -export async function uploadFile(payload: StorageUploadPayload) { - return storageProvider.uploadFile(payload); -} diff --git a/backend/src/core/hooks.ts b/backend/src/core/hooks.ts deleted file mode 100644 index 73984c8..0000000 --- a/backend/src/core/hooks.ts +++ /dev/null @@ -1,69 +0,0 @@ -// ============================================================ -// Clube67 — Hook System -// ============================================================ -// Event-driven communication between plugins. -// Plugins register handlers for events and emit events. -// This enables decoupled, modular architecture. - -import { HookSystem } from './types'; -import { logger } from '../utils/logger'; - -type HookHandler = (...args: any[]) => Promise; - -class HookSystemImpl implements HookSystem { - private handlers: Map> = new Map(); - - register(event: string, handler: HookHandler): void { - if (!this.handlers.has(event)) { - this.handlers.set(event, new Set()); - } - this.handlers.get(event)!.add(handler); - logger.info(`[Hooks] Registered handler for "${event}"`); - } - - async emit(event: string, ...args: any[]): Promise { - const eventHandlers = this.handlers.get(event); - if (!eventHandlers || eventHandlers.size === 0) { - return []; - } - - const results: any[] = []; - for (const handler of eventHandlers) { - try { - const result = await handler(...args); - results.push(result); - } catch (err: any) { - logger.error(`[Hooks] Error in handler for "${event}": ${err.message}`); - } - } - return results; - } - - remove(event: string, handler: HookHandler): void { - const eventHandlers = this.handlers.get(event); - if (eventHandlers) { - eventHandlers.delete(handler); - if (eventHandlers.size === 0) { - this.handlers.delete(event); - } - } - } - - /** List all registered events (for admin dashboard) */ - listEvents(): string[] { - return Array.from(this.handlers.keys()); - } - - /** Count handlers for a specific event */ - handlerCount(event: string): number { - return this.handlers.get(event)?.size ?? 0; - } - - /** Clear all handlers (for testing / shutdown) */ - clear(): void { - this.handlers.clear(); - } -} - -// Singleton — shared across all plugins -export const hooks = new HookSystemImpl(); diff --git a/backend/src/core/plugin-config.ts b/backend/src/core/plugin-config.ts deleted file mode 100644 index 34671ca..0000000 --- a/backend/src/core/plugin-config.ts +++ /dev/null @@ -1,44 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -const STORAGE_DIR = path.resolve(process.cwd(), 'storage'); -const CONFIG_FILE = path.join(STORAGE_DIR, 'plugin-configs.json'); - -// Ensure storage dir exists -if (!fs.existsSync(STORAGE_DIR)) { - fs.mkdirSync(STORAGE_DIR, { recursive: true }); -} - -// Ensure config file exists -if (!fs.existsSync(CONFIG_FILE)) { - fs.writeFileSync(CONFIG_FILE, '{}', 'utf-8'); -} - -export const pluginConfig = { - getAll(): Record { - try { - const raw = fs.readFileSync(CONFIG_FILE, 'utf-8'); - return JSON.parse(raw); - } catch (error) { - console.error('Failed to read plugin config:', error); - return {}; - } - }, - - get(pluginName: string): any { - const all = this.getAll(); - return all[pluginName] || {}; - }, - - set(pluginName: string, config: any): void { - const all = this.getAll(); - all[pluginName] = { ...all[pluginName], ...config }; - - try { - fs.writeFileSync(CONFIG_FILE, JSON.stringify(all, null, 2), 'utf-8'); - } catch (error) { - console.error('Failed to save plugin config:', error); - throw new Error('Failed to save configuration'); - } - } -}; diff --git a/backend/src/core/plugin-loader.ts b/backend/src/core/plugin-loader.ts deleted file mode 100644 index 63e4518..0000000 --- a/backend/src/core/plugin-loader.ts +++ /dev/null @@ -1,719 +0,0 @@ -// ============================================================ -// Clube67 — Plugin Loader -// ============================================================ -// Scans /plugins directory, validates manifests, resolves -// dependency order, and activates each plugin. - -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import { Express } from 'express'; -import multer from 'multer'; -import { spawnSync } from 'child_process'; -import { PluginManifest, PluginInstance, PluginContext } from './types'; -import { pluginRegistry } from './plugin-registry'; -import { hooks } from './hooks'; -import { logger } from '../utils/logger'; -import { db } from '../config/database'; -import { redis } from '../config/redis'; -import { authenticate } from '../middleware/auth'; -import { authorize } from '../middleware/rbac'; - -// Source plugins dir (manifests + SQL migrations) -const PROJECT_ROOT = path.resolve(__dirname, '../../../../'); -const PLUGINS_SOURCE_DIR = path.join(PROJECT_ROOT, 'plugins'); -// Compiled plugins dir (JS modules) -const PLUGINS_COMPILED_DIR = path.resolve(__dirname, '../../../plugins'); - -// ── Manifest Loading ──────────────────────────────────────── - -function loadManifest(pluginDir: string): PluginManifest | null { - const manifestPath = path.join(pluginDir, 'manifest.json'); - if (!fs.existsSync(manifestPath)) { - logger.warn(`[PluginLoader] No manifest.json in ${path.basename(pluginDir)}`); - return null; - } - try { - const raw = fs.readFileSync(manifestPath, 'utf-8'); - return JSON.parse(raw) as PluginManifest; - } catch (err: any) { - logger.error(`[PluginLoader] Invalid manifest in ${path.basename(pluginDir)}: ${err.message}`); - return null; - } -} - -// ── Dependency Resolution (Topological Sort) ──────────────── - -function resolveDependencyOrder(manifests: PluginManifest[]): PluginManifest[] { - const byName = new Map(manifests.map(m => [m.name, m])); - const visited = new Set(); - const sorted: PluginManifest[] = []; - - function visit(name: string, stack: Set) { - if (visited.has(name)) return; - if (stack.has(name)) { - throw new Error(`[PluginLoader] Circular dependency detected involving "${name}"`); - } - stack.add(name); - const manifest = byName.get(name); - if (!manifest) return; - - for (const dep of manifest.dependencies) { - visit(dep, stack); - } - - stack.delete(name); - visited.add(name); - sorted.push(manifest); - } - - for (const m of manifests) { - visit(m.name, new Set()); - } - return sorted; -} - -// ── Plugin Instance Loading ───────────────────────────────── - -function loadPluginInstance(pluginName: string, manifest: PluginManifest): PluginInstance | null { - // Try compiled JS first, then source TS - const compiledDir = path.join(PLUGINS_COMPILED_DIR, pluginName); - const sourceDir = path.join(PLUGINS_SOURCE_DIR, pluginName); - - const candidates = [ - path.join(compiledDir, 'index.js'), - path.join(sourceDir, 'index.js'), - path.join(sourceDir, 'index.ts'), - ]; - - let modulePath: string | null = null; - for (const candidate of candidates) { - if (fs.existsSync(candidate)) { - modulePath = candidate; - break; - } - } - - if (!modulePath) { - logger.warn(`[PluginLoader] No index file in plugin "${manifest.name}"`); - return null; - } - - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const mod = require(modulePath); - const instance: PluginInstance = mod.default || mod; - instance.manifest = manifest; - return instance; - } catch (err: any) { - logger.error(`[PluginLoader] Failed to load "${manifest.name}": ${err.message}`); - return null; - } -} - -function getPluginDirByName(name: string): { dirName: string; fullPath: string; manifest: PluginManifest } | null { - if (!fs.existsSync(PLUGINS_SOURCE_DIR)) return null; - const pluginDirs = fs.readdirSync(PLUGINS_SOURCE_DIR) - .map(dirName => ({ dirName, fullPath: path.join(PLUGINS_SOURCE_DIR, dirName) })) - .filter(p => fs.existsSync(path.join(p.fullPath, 'manifest.json'))); - - for (const p of pluginDirs) { - const manifest = loadManifest(p.fullPath); - if (manifest && manifest.name === name) { - return { dirName: p.dirName, fullPath: p.fullPath, manifest }; - } - } - return null; -} - -// ── Migration Runner ──────────────────────────────────────── - -async function runMigrations(pluginName: string, manifest: PluginManifest): Promise { - if (!manifest.backend?.hasMigrations) return; - - // Always read migrations from SOURCE directory (SQL files aren't compiled) - const migrationsDir = path.join(PLUGINS_SOURCE_DIR, pluginName, 'backend', 'migrations'); - if (!fs.existsSync(migrationsDir)) return; - - const migrationFiles = fs.readdirSync(migrationsDir) - .filter(f => f.endsWith('.sql')) - .sort(); - - for (const file of migrationFiles) { - const filePath = path.join(migrationsDir, file); - const sql = fs.readFileSync(filePath, 'utf-8'); - - // Check if migration was already applied - const migrationKey = `${manifest.name}:${file}`; - try { - if (db.client.config.client === 'pg') { - await db.raw(` - CREATE TABLE IF NOT EXISTS plugin_migrations ( - id SERIAL PRIMARY KEY, - plugin_name VARCHAR(255) NOT NULL, - migration_file VARCHAR(255) NOT NULL, - applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - CONSTRAINT unique_migration UNIQUE (plugin_name, migration_file) - ) - `); - } else { - await db.raw(` - CREATE TABLE IF NOT EXISTS plugin_migrations ( - id INT AUTO_INCREMENT PRIMARY KEY, - plugin_name VARCHAR(255) NOT NULL, - migration_file VARCHAR(255) NOT NULL, - applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY unique_migration (plugin_name, migration_file) - ) - `); - } - - const existing = await db('plugin_migrations') - .select('id') - .where({ plugin_name: manifest.name, migration_file: file }) - .first(); - - if (existing) { - logger.info(`[PluginLoader] Migration ${migrationKey} already applied, skipping.`); - continue; - } - - // Run migration — each statement is fault-tolerant for idempotent patterns - const statements = sql.split(';').filter(s => s.trim().length > 0); - for (const stmt of statements) { - try { - let finalStmt = stmt; - if (db.client.config.client === 'pg') { - // If it's a MySQL session variable script or procedural blocks, skip it - if (stmt.toLowerCase().includes('set @') || - stmt.toLowerCase().includes('prepare ') || - stmt.toLowerCase().includes('execute ') || - stmt.toLowerCase().includes('deallocate ')) { - continue; - } - - // Replace common MySQL types/keywords - finalStmt = finalStmt - .replace(/int\s+auto_increment/gi, 'SERIAL') - .replace(/auto_increment/gi, '') - .replace(/int\(\d+\)/gi, 'integer') - .replace(/varchar\(\d+\)/gi, 'varchar') - .replace(/text/gi, 'text') - .replace(/datetime/gi, 'timestamp') - .replace(/timestamp\s+default\s+current_timestamp\s+on\s+update\s+current_timestamp/gi, 'timestamp default current_timestamp') - .replace(/on\s+update\s+current_timestamp/gi, '') - .replace(/unique\s+key\s+\w+\s+\(([^)]+)\)/gi, 'CONSTRAINT unique_$1 UNIQUE ($1)') - .replace(/index\s+\w+\s+\(([^)]+)\)/gi, '') - .replace(/foreign\s+key\s+\(([^)]+)\)\s+references\s+(\w+)\(([^)]+)\)/gi, 'FOREIGN KEY ($1) REFERENCES $2($3)') - .replace(/engine\s*=\s*\w+/gi, '') - .replace(/default\s+charset\s*=\s*\w+/gi, '') - .replace(/collate\s*=\s*\w+/gi, '') - .replace(/modify\s+column/gi, 'ALTER COLUMN') - .replace(/add\s+column\s+if\s+not\s+exists/gi, 'ADD COLUMN') - .replace(/add\s+column/gi, 'ADD COLUMN') - .replace(/if\s+not\s+exists/gi, '') - .replace(/enum\([^)]+\)/gi, 'varchar(255)'); - } - - await db.raw(finalStmt); - } catch (stmtErr: any) { - if (db.client.config.client === 'pg') { - const pgErrorMsgs = [ - 'already exists', - 'duplicate key', - 'does not exist', - 'already is a member' - ]; - if (pgErrorMsgs.some(msg => stmtErr.message.toLowerCase().includes(msg))) { - logger.info(`[PluginLoader] ⏭️ PostgreSQL skipped: ${stmtErr.message.substring(0, 80)}`); - } else { - logger.warn(`[PluginLoader] ⚠️ PostgreSQL migration statement failed (continuing): ${stmtErr.message}`); - } - } else { - const toleratedCodes = [1060, 1061, 1050, 1091]; - if (toleratedCodes.includes(stmtErr.errno)) { - logger.info(`[PluginLoader] ⏭️ Skipped (already exists): ${stmtErr.message.substring(0, 80)}`); - } else { - throw stmtErr; - } - } - } - } - - // Record migration - await db('plugin_migrations').insert({ - plugin_name: manifest.name, - migration_file: file - }); - - logger.info(`[PluginLoader] ✅ Applied migration ${migrationKey}`); - } catch (err: any) { - logger.error(`[PluginLoader] ❌ Migration ${migrationKey} failed: ${err.message}`); - throw err; - } - } -} - -function buildPluginContext(app: Express, io: any, manifest: PluginManifest): PluginContext { - return { - app, - io, - db, - hooks, - config: {}, - logger: { - info: (msg, meta) => logger.info(`[Plugin:${manifest.name}] ${msg}`, meta), - warn: (msg, meta) => logger.warn(`[Plugin:${manifest.name}] ${msg}`, meta), - error: (msg, meta) => logger.error(`[Plugin:${manifest.name}] ${msg}`, meta), - }, - }; -} - -function clearRequireCache(targetDir: string): void { - const normalized = path.resolve(targetDir); - Object.keys(require.cache).forEach(key => { - if (key.startsWith(normalized)) { - delete require.cache[key]; - } - }); -} - -async function unloadPlugin(app: Express, io: any, name: string): Promise { - const entry = pluginRegistry.get(name); - if (!entry) return; - const ctx = buildPluginContext(app, io, entry.manifest); - try { - await entry.instance.deactivate(ctx); - } catch (err: any) { - logger.warn(`[PluginLoader] Deactivate "${name}" failed: ${err.message}`); - } - - const layers = entry.layers || []; - const routerStack = (app as any)?._router?.stack; - if (Array.isArray(routerStack) && layers.length > 0) { - (app as any)._router.stack = routerStack.filter((l: any) => !layers.includes(l)); - } - - pluginRegistry.remove(name); -} - -async function loadPluginFromDir(app: Express, io: any, dirName: string, manifest: PluginManifest): Promise { - const instance = loadPluginInstance(dirName, manifest); - if (!instance) { - pluginRegistry.register(manifest, { manifest, activate: async () => { }, deactivate: async () => { } }); - pluginRegistry.markError(manifest.name, 'Failed to load plugin module'); - return; - } - - pluginRegistry.register(manifest, instance); - - try { - await runMigrations(dirName, manifest); - } catch (err: any) { - pluginRegistry.markError(manifest.name, `Migration failed: ${err.message}`); - return; - } - - const ctx = buildPluginContext(app, io, manifest); - const beforeStack = ((app as any)?._router?.stack || []).slice(); - - try { - await instance.activate(ctx); - pluginRegistry.markActive(manifest.name); - const afterStack = (app as any)?._router?.stack || []; - const newLayers = afterStack.filter((l: any) => !beforeStack.includes(l)); - pluginRegistry.setLayers(manifest.name, newLayers); - logger.info(`[PluginLoader] ✅ Activated: ${manifest.displayName} v${manifest.version}`); - } catch (err: any) { - pluginRegistry.markError(manifest.name, err.message); - logger.error(`[PluginLoader] ❌ Failed to activate "${manifest.name}": ${err.message}`); - } -} - -// ── Main Loader ───────────────────────────────────────────── - -export async function loadPlugins(app: Express, io: any): Promise { - logger.info(`[PluginLoader] Scanning plugins source: ${PLUGINS_SOURCE_DIR}`); - logger.info(`[PluginLoader] Compiled plugins at: ${PLUGINS_COMPILED_DIR}`); - - if (!fs.existsSync(PLUGINS_SOURCE_DIR)) { - logger.info('[PluginLoader] No plugins directory found, creating...'); - fs.mkdirSync(PLUGINS_SOURCE_DIR, { recursive: true }); - return; - } - - // 1. Scan and load manifests from source directory - const pluginDirs = fs.readdirSync(PLUGINS_SOURCE_DIR) - .map(name => ({ name, fullPath: path.join(PLUGINS_SOURCE_DIR, name) })) - .filter(p => fs.statSync(p.fullPath).isDirectory() && p.name !== 'node_modules'); - - const manifests: PluginManifest[] = []; - const pluginNames: Map = new Map(); - - for (const { name: dirName, fullPath } of pluginDirs) { - const manifest = loadManifest(fullPath); - if (manifest && manifest.enabled) { - manifests.push(manifest); - pluginNames.set(manifest.name, dirName); - } - } - - logger.info(`[PluginLoader] Found ${manifests.length} enabled plugins`); - - // 2. Resolve dependency order - let sorted: PluginManifest[]; - try { - sorted = resolveDependencyOrder(manifests); - } catch (err: any) { - logger.error(err.message); - return; - } - - // 3. Activate plugins in order - for (const manifest of sorted) { - const dirName = pluginNames.get(manifest.name)!; - - // Check dependencies - const depCheck = pluginRegistry.checkDependencies(manifest.name); - // Register first so dependency check works for later plugins - const instance = loadPluginInstance(dirName, manifest); - if (!instance) { - pluginRegistry.register(manifest, { manifest, activate: async () => { }, deactivate: async () => { } }); - pluginRegistry.markError(manifest.name, 'Failed to load plugin module'); - continue; - } - - pluginRegistry.register(manifest, instance); - - // Run migrations before activation - try { - await runMigrations(dirName, manifest); - } catch (err: any) { - pluginRegistry.markError(manifest.name, `Migration failed: ${err.message}`); - continue; - } - - // Build plugin context - const ctx: PluginContext = buildPluginContext(app, io, manifest); - - // Activate - try { - const beforeStack = ((app as any)?._router?.stack || []).slice(); - await instance.activate(ctx); - pluginRegistry.markActive(manifest.name); - const afterStack = (app as any)?._router?.stack || []; - const newLayers = afterStack.filter((l: any) => !beforeStack.includes(l)); - pluginRegistry.setLayers(manifest.name, newLayers); - logger.info(`[PluginLoader] ✅ Activated: ${manifest.displayName} v${manifest.version}`); - } catch (err: any) { - pluginRegistry.markError(manifest.name, err.message); - logger.error(`[PluginLoader] ❌ Failed to activate "${manifest.name}": ${err.message}`); - } - } - - // 4. Summary - const summary = pluginRegistry.getSummary(); - logger.info(`[PluginLoader] ═══════════════════════════════════`); - logger.info(`[PluginLoader] Plugins: ${summary.active} active, ${summary.error} errors, ${summary.inactive} inactive`); - logger.info(`[PluginLoader] ═══════════════════════════════════`); - - // 5. Register admin API for plugin management - registerPluginAdminRoutes(app, io); -} - -// ── Admin API Routes ──────────────────────────────────────── - -function registerPluginAdminRoutes(app: Express, io: any): void { - const adminOnly = [authenticate, authorize(['super_admin'])]; - const uploadZip = multer({ - storage: multer.memoryStorage(), - limits: { fileSize: 50 * 1024 * 1024 }, - fileFilter: (_req, file, cb) => { - if (file.originalname.toLowerCase().endsWith('.zip')) return cb(null, true); - cb(new Error('Apenas arquivos .zip são permitidos')); - }, - }); - - const PLUGIN_LIST_CACHE_KEY = 'admin:plugins:list'; - const PLUGIN_MENU_CACHE_PREFIX = 'admin:plugins:menu:'; - const PLUGIN_CACHE_TTL_SECONDS = 30; - - const clearPluginCaches = async () => { - try { - await redis.del(PLUGIN_LIST_CACHE_KEY); - const menuKeys = await redis.keys(`${PLUGIN_MENU_CACHE_PREFIX}*`); - if (menuKeys.length) await redis.del(...menuKeys); - } catch { - // ignore cache errors - } - }; - - function copyDir(srcDir: string, destDir: string) { - if (!fs.existsSync(srcDir)) return; - fs.mkdirSync(destDir, { recursive: true }); - const entries = fs.readdirSync(srcDir, { withFileTypes: true }); - for (const entry of entries) { - const src = path.join(srcDir, entry.name); - const dest = path.join(destDir, entry.name); - if (entry.isDirectory()) copyDir(src, dest); - else fs.copyFileSync(src, dest); - } - } - - function buildExportZip(pluginDir: string, lite: boolean): Buffer { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'plugin-export-')); - const stageDir = path.join(tmpDir, 'plugin'); - fs.mkdirSync(stageDir, { recursive: true }); - - const manifestPath = path.join(pluginDir, 'manifest.json'); - if (fs.existsSync(manifestPath)) fs.copyFileSync(manifestPath, path.join(stageDir, 'manifest.json')); - - const backendDir = path.join(pluginDir, 'backend'); - const routesTs = path.join(backendDir, 'routes.ts'); - const routesJs = path.join(backendDir, 'routes.js'); - const serviceTs = path.join(backendDir, 'service.ts'); - const serviceJs = path.join(backendDir, 'service.js'); - - const stageBackend = path.join(stageDir, 'backend'); - fs.mkdirSync(stageBackend, { recursive: true }); - if (fs.existsSync(routesTs)) fs.copyFileSync(routesTs, path.join(stageBackend, 'routes.ts')); - if (fs.existsSync(routesJs)) fs.copyFileSync(routesJs, path.join(stageBackend, 'routes.js')); - if (fs.existsSync(serviceTs)) fs.copyFileSync(serviceTs, path.join(stageBackend, 'service.ts')); - if (fs.existsSync(serviceJs)) fs.copyFileSync(serviceJs, path.join(stageBackend, 'service.js')); - - copyDir(path.join(backendDir, 'models'), path.join(stageBackend, 'models')); - if (!lite) { - copyDir(path.join(backendDir, 'migrations'), path.join(stageBackend, 'migrations')); - const readmePath = path.join(pluginDir, 'README.md'); - if (fs.existsSync(readmePath)) fs.copyFileSync(readmePath, path.join(stageDir, 'README.md')); - } - - const zipPath = path.join(tmpDir, 'plugin.zip'); - const zipProc = spawnSync('zip', ['-r', zipPath, '.'], { cwd: stageDir }); - if (zipProc.status !== 0) { - throw new Error('Falha ao gerar ZIP'); - } - const buffer = fs.readFileSync(zipPath); - fs.rmSync(tmpDir, { recursive: true, force: true }); - return buffer; - } - - async function importPluginFromZip(app: Express, io: any, buffer: Buffer, reloadAfter: boolean): Promise<{ name: string }> { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'plugin-import-')); - const zipPath = path.join(tmpDir, 'plugin.zip'); - fs.writeFileSync(zipPath, buffer); - - const listProc = spawnSync('unzip', ['-Z1', zipPath]); - if (listProc.status !== 0) throw new Error('Arquivo ZIP inválido'); - const entries = listProc.stdout.toString('utf-8').split('\n').filter(Boolean); - for (const name of entries) { - const clean = name.replace(/\\/g, '/'); - if (clean.startsWith('/') || clean.includes('..') || clean.includes(':')) { - throw new Error('Arquivo ZIP inválido'); - } - } - - const extractDir = path.join(tmpDir, 'extract'); - fs.mkdirSync(extractDir, { recursive: true }); - const unzipProc = spawnSync('unzip', ['-o', zipPath, '-d', extractDir]); - if (unzipProc.status !== 0) throw new Error('Falha ao extrair ZIP'); - - const findManifest = (dir: string): string | null => { - const items = fs.readdirSync(dir, { withFileTypes: true }); - for (const item of items) { - const full = path.join(dir, item.name); - if (item.isDirectory()) { - const found = findManifest(full); - if (found) return found; - } else if (item.name === 'manifest.json') { - return full; - } - } - return null; - }; - - const manifestPath = findManifest(extractDir); - if (!manifestPath) throw new Error('manifest.json não encontrado no ZIP'); - const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as PluginManifest; - if (!manifest?.name) throw new Error('manifest.json inválido'); - - const rootDir = path.dirname(manifestPath); - const targetDir = path.join(PLUGINS_SOURCE_DIR, manifest.name); - if (fs.existsSync(targetDir)) { - await unloadPlugin(app, io, manifest.name); - fs.rmSync(targetDir, { recursive: true, force: true }); - } - - fs.mkdirSync(PLUGINS_SOURCE_DIR, { recursive: true }); - fs.cpSync(rootDir, targetDir, { recursive: true }); - - clearRequireCache(path.join(PLUGINS_SOURCE_DIR, manifest.name)); - clearRequireCache(path.join(PLUGINS_COMPILED_DIR, manifest.name)); - - const loaded = loadManifest(targetDir); - if (!loaded) throw new Error('manifest.json inválido no destino'); - - await loadPluginFromDir(app, io, manifest.name, loaded); - - if (reloadAfter) { - await unloadPlugin(app, io, manifest.name); - clearRequireCache(path.join(PLUGINS_SOURCE_DIR, manifest.name)); - clearRequireCache(path.join(PLUGINS_COMPILED_DIR, manifest.name)); - await loadPluginFromDir(app, io, manifest.name, loaded); - } - - fs.rmSync(tmpDir, { recursive: true, force: true }); - await clearPluginCaches(); - return { name: manifest.name }; - } - - // GET /api/admin/plugins — list all plugins - app.get('/api/admin/plugins', ...adminOnly, (req, res) => { - (async () => { - try { - const cached = await redis.get(PLUGIN_LIST_CACHE_KEY); - if (cached) { - return res.json(JSON.parse(cached)); - } - } catch { - // ignore cache errors - } - - const plugins = pluginRegistry.getAll().map(entry => ({ - name: entry.manifest.name, - displayName: entry.manifest.displayName, - version: entry.manifest.version, - description: entry.manifest.description, - category: entry.manifest.category, - status: entry.status, - error: entry.error, - canDisable: entry.manifest.canDisable, - dependencies: entry.manifest.dependencies, - activatedAt: entry.activatedAt, - menuItems: entry.manifest.frontend?.menuItems || [], - })); - - const payload = { - summary: pluginRegistry.getSummary(), - plugins, - }; - - try { - await redis.set(PLUGIN_LIST_CACHE_KEY, JSON.stringify(payload), 'EX', PLUGIN_CACHE_TTL_SECONDS); - } catch { - // ignore cache errors - } - - res.json(payload); - })(); - }); - - // GET /api/admin/plugins/menu — get sidebar menu items for current user - app.get('/api/admin/plugins/menu', ...adminOnly, (req, res) => { - (async () => { - const role = (req as any).user?.role || 'user'; - const cacheKey = `${PLUGIN_MENU_CACHE_PREFIX}${role}`; - try { - const cached = await redis.get(cacheKey); - if (cached) { - return res.json(JSON.parse(cached)); - } - } catch { - // ignore cache errors - } - - const activeManifests = pluginRegistry.getActiveManifests(); - const menuItems = activeManifests - .flatMap(m => (m.frontend?.menuItems || []).map(item => ({ - ...item, - pluginName: m.name, - }))) - .filter(item => item.roles.includes(role) || item.roles.includes('*')) - .sort((a, b) => a.order - b.order); - - const payload = { menuItems }; - try { - await redis.set(cacheKey, JSON.stringify(payload), 'EX', PLUGIN_CACHE_TTL_SECONDS); - } catch { - // ignore cache errors - } - - res.json(payload); - })(); - }); - - // GET /api/admin/plugins/hooks — list registered hooks - app.get('/api/admin/plugins/hooks', ...adminOnly, (_req, res) => { - const hooksList = hooks.listEvents().map(event => ({ - event, - handlerCount: hooks.handlerCount(event), - })); - res.json({ hooks: hooksList }); - }); - - // GET /api/admin/plugins/:name/export — download plugin package - app.get('/api/admin/plugins/:name/export', ...adminOnly, (req, res) => { - const name = req.params.name; - const info = getPluginDirByName(name); - if (!info) return res.status(404).json({ error: 'Plugin não encontrado' }); - - const buffer = buildExportZip(info.fullPath, false); - res.setHeader('Content-Type', 'application/zip'); - res.setHeader('Content-Disposition', `attachment; filename="${name}.zip"`); - res.send(buffer); - }); - - // GET /api/admin/plugins/:name/export-lite — lightweight package - app.get('/api/admin/plugins/:name/export-lite', ...adminOnly, (req, res) => { - const name = req.params.name; - const info = getPluginDirByName(name); - if (!info) return res.status(404).json({ error: 'Plugin não encontrado' }); - - const buffer = buildExportZip(info.fullPath, true); - res.setHeader('Content-Type', 'application/zip'); - res.setHeader('Content-Disposition', `attachment; filename="${name}-lite.zip"`); - res.send(buffer); - }); - - // POST /api/admin/plugins/import — upload plugin ZIP - app.post('/api/admin/plugins/import', ...adminOnly, uploadZip.single('file'), async (req, res) => { - try { - if (!req.file) return res.status(400).json({ error: 'Arquivo ZIP obrigatório' }); - const result = await importPluginFromZip(app, io, req.file.buffer, false); - await clearPluginCaches(); - res.json({ message: 'Plugin importado', name: result.name }); - } catch (err: any) { - res.status(400).json({ error: err.message || 'Erro ao importar plugin' }); - } - }); - - // POST /api/admin/plugins/:name/reload — reload plugin - app.post('/api/admin/plugins/:name/reload', ...adminOnly, async (req, res) => { - const name = req.params.name; - const info = getPluginDirByName(name); - if (!info) return res.status(404).json({ error: 'Plugin não encontrado' }); - - try { - await unloadPlugin(app, io, name); - clearRequireCache(path.join(PLUGINS_SOURCE_DIR, info.dirName)); - clearRequireCache(path.join(PLUGINS_COMPILED_DIR, info.dirName)); - await loadPluginFromDir(app, io, info.dirName, info.manifest); - await clearPluginCaches(); - res.json({ message: 'Plugin recarregado', name }); - } catch (err: any) { - res.status(500).json({ error: err.message || 'Erro ao recarregar plugin' }); - } - }); - - // POST /api/admin/plugins/:name/update — update plugin via ZIP - app.post('/api/admin/plugins/:name/update', ...adminOnly, uploadZip.single('file'), async (req, res) => { - try { - if (!req.file) return res.status(400).json({ error: 'Arquivo ZIP obrigatório' }); - const result = await importPluginFromZip(app, io, req.file.buffer, true); - await clearPluginCaches(); - res.json({ message: 'Plugin atualizado', name: result.name }); - } catch (err: any) { - res.status(400).json({ error: err.message || 'Erro ao atualizar plugin' }); - } - }); -} diff --git a/backend/src/core/plugin-registry.ts b/backend/src/core/plugin-registry.ts deleted file mode 100644 index 09fefc8..0000000 --- a/backend/src/core/plugin-registry.ts +++ /dev/null @@ -1,120 +0,0 @@ -// ============================================================ -// Clube67 — Plugin Registry -// ============================================================ -// Central registry of all loaded plugins. -// Used by admin dashboard and frontend to query active plugins. - -import { PluginManifest, PluginInstance } from './types'; -import { logger } from '../utils/logger'; - -interface RegistryEntry { - manifest: PluginManifest; - instance: PluginInstance; - status: 'active' | 'inactive' | 'error'; - error?: string; - activatedAt?: Date; - layers?: any[]; -} - -class PluginRegistry { - private plugins: Map = new Map(); - - register(manifest: PluginManifest, instance: PluginInstance): void { - this.plugins.set(manifest.name, { - manifest, - instance, - status: 'inactive', - }); - } - - markActive(name: string): void { - const entry = this.plugins.get(name); - if (entry) { - entry.status = 'active'; - entry.activatedAt = new Date(); - } - } - - markError(name: string, error: string): void { - const entry = this.plugins.get(name); - if (entry) { - entry.status = 'error'; - entry.error = error; - } - } - - markInactive(name: string): void { - const entry = this.plugins.get(name); - if (entry) { - entry.status = 'inactive'; - entry.activatedAt = undefined; - } - } - - setLayers(name: string, layers: any[]): void { - const entry = this.plugins.get(name); - if (entry) entry.layers = layers; - } - - remove(name: string): void { - this.plugins.delete(name); - } - - get(name: string): RegistryEntry | undefined { - return this.plugins.get(name); - } - - getAll(): RegistryEntry[] { - return Array.from(this.plugins.values()); - } - - getActive(): RegistryEntry[] { - return this.getAll().filter(e => e.status === 'active'); - } - - getManifests(): PluginManifest[] { - return this.getAll().map(e => e.manifest); - } - - getActiveManifests(): PluginManifest[] { - return this.getActive().map(e => e.manifest); - } - - /** Get all menu items from active plugins (for frontend sidebar) */ - getMenuItems(userRole: string): Array<{ pluginName: string; items: PluginManifest['frontend'] }> { - return this.getActive() - .filter(e => e.manifest.frontend?.menuItems?.length) - .map(e => ({ - pluginName: e.manifest.name, - items: e.manifest.frontend!, - })); - } - - /** Check if a plugin is active */ - isActive(name: string): boolean { - return this.plugins.get(name)?.status === 'active'; - } - - /** Get plugin count summary */ - getSummary(): { total: number; active: number; inactive: number; error: number } { - const all = this.getAll(); - return { - total: all.length, - active: all.filter(e => e.status === 'active').length, - inactive: all.filter(e => e.status === 'inactive').length, - error: all.filter(e => e.status === 'error').length, - }; - } - - /** Check if all dependencies for a plugin are satisfied */ - checkDependencies(name: string): { satisfied: boolean; missing: string[] } { - const entry = this.plugins.get(name); - if (!entry) return { satisfied: false, missing: [name] }; - - const missing = entry.manifest.dependencies.filter(dep => !this.isActive(dep)); - return { satisfied: missing.length === 0, missing }; - } -} - -// Singleton -export const pluginRegistry = new PluginRegistry(); diff --git a/backend/src/core/types.js b/backend/src/core/types.js deleted file mode 100644 index f033188..0000000 --- a/backend/src/core/types.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -// ============================================================ -// Clube67 — Plugin Manifest Schema -// ============================================================ -// Each plugin MUST have a manifest.json following this interface. -Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/backend/src/core/types.ts b/backend/src/core/types.ts deleted file mode 100644 index af78755..0000000 --- a/backend/src/core/types.ts +++ /dev/null @@ -1,120 +0,0 @@ -// ============================================================ -// Clube67 — Plugin Manifest Schema -// ============================================================ -// Each plugin MUST have a manifest.json following this interface. - -export interface PluginManifest { - /** Unique plugin identifier (matches folder name) */ - name: string; - - /** Human-readable display name */ - displayName: string; - - /** Semantic version */ - version: string; - - /** Brief description */ - description: string; - - /** Author or team */ - author: string; - - /** Plugin category for admin grouping */ - category: 'core' | 'business' | 'integration' | 'ui' | 'analytics'; - - /** Is this plugin enabled by default? */ - enabled: boolean; - - /** Can this plugin be disabled by admin? (core plugins = false) */ - canDisable: boolean; - - /** Dependencies on other plugins (by name) */ - dependencies: string[]; - - /** Backend configuration */ - backend?: { - /** Route prefix (e.g. /api/auth, /api/users) */ - routePrefix: string; - /** Has database migrations? */ - hasMigrations: boolean; - /** Middleware to inject globally */ - globalMiddleware?: string[]; - }; - - /** Frontend configuration */ - frontend?: { - /** Menu items to inject into sidebar */ - menuItems: PluginMenuItem[]; - /** Dashboard widgets */ - widgets?: PluginWidget[]; - /** Pages to register */ - pages?: PluginPage[]; - }; - - /** Hook subscriptions */ - hooks?: { - /** Events this plugin listens to */ - subscribes: string[]; - /** Events this plugin emits */ - emits: string[]; - }; -} - -export interface PluginMenuItem { - id: string; - label: string; - icon: string; - href: string; - /** Which roles can see this menu item */ - roles: string[]; - /** Order in sidebar (lower = higher) */ - order: number; -} - -export interface PluginWidget { - id: string; - component: string; - /** Grid columns span (1-4) */ - span: number; - roles: string[]; - order: number; -} - -export interface PluginPage { - path: string; - component: string; - roles: string[]; -} - -// ============================================================ -// Plugin Lifecycle Interface -// ============================================================ - -import { Express } from 'express'; - -export interface PluginContext { - app: Express; - db: any; - hooks: HookSystem; - config: Record; - logger: PluginLogger; - io: any; // Socket.IO Server -} - -export interface PluginLogger { - info(message: string, meta?: Record): void; - warn(message: string, meta?: Record): void; - error(message: string, meta?: Record): void; -} - -export interface PluginInstance { - manifest: PluginManifest; - activate(ctx: PluginContext): Promise; - deactivate(ctx: PluginContext): Promise; -} - -export interface HookSystem { - register(event: string, handler: (...args: any[]) => Promise): void; - emit(event: string, ...args: any[]): Promise; - remove(event: string, handler: (...args: any[]) => Promise): void; -} diff --git a/backend/src/database/migrations/20260215000000_init_schema.ts b/backend/src/database/migrations/20260215000000_init_schema.ts deleted file mode 100644 index 093b837..0000000 --- a/backend/src/database/migrations/20260215000000_init_schema.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - // 1. users - await knex.schema.createTable('users', (table) => { - table.increments('id').primary(); - table.string('name').notNullable(); - table.string('email').unique().notNullable(); - table.string('password_hash').notNullable(); - table.string('role').notNullable().defaultTo('user'); - table.string('status').notNullable().defaultTo('active'); - table.string('whatsapp').nullable(); - table.string('state').nullable(); - table.string('city').nullable(); - table.string('neighborhood').nullable(); - table.integer('partner_id').nullable(); - table.timestamps(true, true); - }); - - // 2. partners - await knex.schema.createTable('partners', (table) => { - table.increments('id').primary(); - table.string('company_name').notNullable(); - table.string('slug').unique().nullable(); - table.string('cnpj').unique().nullable(); - table.string('email').nullable(); - table.string('phone').nullable(); - table.text('description').nullable(); - table.string('logo_url').nullable(); - table.string('address_street').nullable(); - table.string('address_number').nullable(); - table.string('address_neighborhood').nullable(); - table.string('address_city').nullable(); - table.string('address_state').nullable(); - table.string('type').nullable(); - table.integer('owner_user_id').nullable(); - table.string('status').notNullable().defaultTo('active'); - table.integer('data_consent').notNullable().defaultTo(0); - table.timestamps(true, true); - }); - - // 3. categories - await knex.schema.createTable('categories', (table) => { - table.increments('id').primary(); - table.string('name').notNullable(); - table.string('slug').unique().notNullable(); - table.string('icon').nullable(); - table.timestamps(true, true); - }); - - // 4. benefits - await knex.schema.createTable('benefits', (table) => { - table.increments('id').primary(); - table.integer('partner_id').notNullable(); - table.integer('category_id').notNullable(); - table.string('title').notNullable(); - table.text('description').nullable(); - table.string('discount_value').nullable(); - table.text('rules').nullable(); - table.boolean('active').notNullable().defaultTo(true); - table.integer('usage_count').notNullable().defaultTo(0); - table.boolean('is_global').notNullable().defaultTo(false); - table.timestamps(true, true); - }); - - // 5. transactions - await knex.schema.createTable('transactions', (table) => { - table.increments('id').primary(); - table.integer('user_id').notNullable(); - table.integer('benefit_id').notNullable(); - table.integer('partner_id').notNullable(); - table.string('type').notNullable().defaultTo('redemption'); - table.string('status').notNullable().defaultTo('completed'); - table.timestamps(true, true); - }); - - // 6. leads - await knex.schema.createTable('leads', (table) => { - table.increments('id').primary(); - table.integer('user_id').nullable(); - table.integer('partner_id').notNullable(); - table.string('name').notNullable(); - table.string('email').notNullable(); - table.string('phone').nullable(); - table.string('lead_type').notNullable().defaultTo('member'); - table.string('status').notNullable().defaultTo('new'); - table.timestamps(true, true); - }); - - // 7. favorites - await knex.schema.createTable('favorites', (table) => { - table.increments('id').primary(); - table.integer('user_id').notNullable(); - table.integer('benefit_id').notNullable(); - table.timestamps(true, true); - }); - - // 8. logs - await knex.schema.createTable('logs', (table) => { - table.increments('id').primary(); - table.integer('user_id').nullable(); - table.string('action').notNullable(); - table.string('entity_type').nullable(); - table.integer('entity_id').nullable(); - table.text('details').nullable(); - table.string('ip_address').nullable(); - table.timestamp('created_at').defaultTo(knex.fn.now()); - }); -} - -export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists('logs'); - await knex.schema.dropTableIfExists('favorites'); - await knex.schema.dropTableIfExists('leads'); - await knex.schema.dropTableIfExists('transactions'); - await knex.schema.dropTableIfExists('benefits'); - await knex.schema.dropTableIfExists('categories'); - await knex.schema.dropTableIfExists('partners'); - await knex.schema.dropTableIfExists('users'); -} diff --git a/backend/src/database/migrations/20260216151047_add_deleted_at_to_users.ts b/backend/src/database/migrations/20260216151047_add_deleted_at_to_users.ts deleted file mode 100644 index c1d7fbc..0000000 --- a/backend/src/database/migrations/20260216151047_add_deleted_at_to_users.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.schema.table('users', (table) => { - table.timestamp('deleted_at').nullable().defaultTo(null); - table.boolean('is_anonymized').defaultTo(false); - }); -} - -export async function down(knex: Knex): Promise { - await knex.schema.table('users', (table) => { - table.dropColumn('is_anonymized'); - table.dropColumn('deleted_at'); - }); -} diff --git a/backend/src/database/migrations/20260216163300_update_user_status_enum.ts b/backend/src/database/migrations/20260216163300_update_user_status_enum.ts deleted file mode 100644 index 85228b0..0000000 --- a/backend/src/database/migrations/20260216163300_update_user_status_enum.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - if (knex.client.config.client === 'mysql' || knex.client.config.client === 'mysql2') { - await knex.raw("ALTER TABLE users MODIFY COLUMN status ENUM('lead', 'pending', 'active', 'inactive', 'blocked', 'banned') NOT NULL DEFAULT 'active'"); - } else { - // PostgreSQL: status is a varchar column, so no type modification is required. - } -} - -export async function down(knex: Knex): Promise { - if (knex.client.config.client === 'mysql' || knex.client.config.client === 'mysql2') { - await knex.raw("ALTER TABLE users MODIFY COLUMN status ENUM('lead', 'pending', 'active', 'inactive') NOT NULL DEFAULT 'active'"); - } else { - // PostgreSQL: status is a varchar column. - } -} diff --git a/backend/src/database/migrations/20260217004800_add_hide_partner_name_to_benefits.ts b/backend/src/database/migrations/20260217004800_add_hide_partner_name_to_benefits.ts deleted file mode 100644 index 93d3c7c..0000000 --- a/backend/src/database/migrations/20260217004800_add_hide_partner_name_to_benefits.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.schema.alterTable('benefits', (table) => { - table.boolean('hide_partner_name').defaultTo(false); - table.string('partner_label').nullable(); - }); -} - -export async function down(knex: Knex): Promise { - await knex.schema.alterTable('benefits', (table) => { - table.dropColumn('hide_partner_name'); - table.dropColumn('partner_label'); - }); -} diff --git a/backend/src/database/migrations/20260217145800_add_partners_columns.ts b/backend/src/database/migrations/20260217145800_add_partners_columns.ts deleted file mode 100644 index 02b5c2a..0000000 --- a/backend/src/database/migrations/20260217145800_add_partners_columns.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - return knex.schema.table('partners', (table) => { - table.string('banner_url').nullable(); - table.string('instagram').nullable(); - table.string('website').nullable(); - }); -} - -export async function down(knex: Knex): Promise { - return knex.schema.table('partners', (table) => { - table.dropColumn('banner_url'); - table.dropColumn('instagram'); - table.dropColumn('website'); - }); -} diff --git a/backend/src/database/migrations/20260217210000_add_partner_logo_bg_color.ts b/backend/src/database/migrations/20260217210000_add_partner_logo_bg_color.ts deleted file mode 100644 index 2cb37e4..0000000 --- a/backend/src/database/migrations/20260217210000_add_partner_logo_bg_color.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - return knex.schema.table('partners', (table) => { - table.string('logo_bg_color').nullable().defaultTo('#FFFFFF'); - }); -} - -export async function down(knex: Knex): Promise { - return knex.schema.table('partners', (table) => { - table.dropColumn('logo_bg_color'); - }); -} diff --git a/backend/src/database/migrations/20260217220000_add_benefit_public_fields.ts b/backend/src/database/migrations/20260217220000_add_benefit_public_fields.ts deleted file mode 100644 index 6d62ed6..0000000 --- a/backend/src/database/migrations/20260217220000_add_benefit_public_fields.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.schema.table("benefits", (table) => { - table.string("public_title").nullable(); - table.text("public_description").nullable(); - table.boolean("public_hide_values").defaultTo(false); - }); -} - -export async function down(knex: Knex): Promise { - await knex.schema.table("benefits", (table) => { - table.dropColumn("public_title"); - table.dropColumn("public_description"); - table.dropColumn("public_hide_values"); - }); -} diff --git a/backend/src/database/migrations/20260218120000_create_referrals.ts b/backend/src/database/migrations/20260218120000_create_referrals.ts deleted file mode 100644 index ee2424e..0000000 --- a/backend/src/database/migrations/20260218120000_create_referrals.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Knex } from "knex"; - -export async function up(knex: Knex): Promise { - await knex.schema.createTable('referrals', (table) => { - table.increments('id').primary(); - table.integer('referrer_user_id').notNullable(); - table.integer('benefit_id').notNullable(); - table.string('referral_code', 50).notNullable(); - table.string('referred_email').nullable(); - table.integer('referred_user_id').nullable(); - table.enum('status', ['pending', 'clicked', 'converted', 'expired']).defaultTo('pending'); - table.timestamp('clicked_at').nullable(); - table.timestamp('converted_at').nullable(); - table.timestamp('created_at').defaultTo(knex.fn.now()); - table.timestamp('updated_at').defaultTo(knex.fn.now()); - - table.index(['referrer_user_id']); - table.index(['referral_code']); - table.index(['benefit_id']); - }); -} - -export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists('referrals'); -} diff --git a/backend/src/index.ts b/backend/src/index.ts index 32d57cc..379799f 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,13 +1,13 @@ import express from 'express'; import http from 'http'; -import { Server } from 'socket.io'; +import { Server as SocketServer } from 'socket.io'; import { config } from './config'; import { setupSecurity } from './middleware/security'; -import { logger } from './utils/logger'; +import logger from './utils/logger'; +import db from './config/database'; +import { pluginService } from './services/plugin.service'; -import path from 'path'; - -// ── Legacy monolithic routes (kept for backward compatibility) ── +// Route imports import authRoutes from './routes/auth.routes'; import userRoutes from './routes/user.routes'; import partnerRoutes from './routes/partner.routes'; @@ -15,86 +15,105 @@ import benefitRoutes from './routes/benefit.routes'; import adminRoutes from './routes/admin.routes'; import webhookRoutes from './routes/webhook.routes'; import pluginRoutes from './routes/plugin.routes'; -import leadRoutes from './routes/lead.routes'; -import uploadRoutes from './routes/upload.routes'; -import referralRoutes from './routes/referral.routes'; -// ── New Plugin System ────────────────────────────────────────── -import { loadPlugins } from './core/plugin-loader'; -import { pluginRegistry } from './core/plugin-registry'; -import { storageProvider } from './core/StorageProvider'; -import { checkStorageHealth } from './middleware/storage-health'; +const app = express(); +const server = http.createServer(app); -async function start() { - const app = express(); - const server = http.createServer(app); - const io = new Server(server, { cors: { origin: '*' } }); +// WebSocket +const io = new SocketServer(server, { + cors: { + origin: [config.app.frontendUrl, 'http://localhost:3000'], + credentials: true, + }, +}); - // Trust Nginx proxy (required for express-rate-limit behind reverse proxy) - app.set('trust proxy', 1); +// Body parsing +app.use(express.json({ limit: '10mb' })); +app.use(express.urlencoded({ extended: true, limit: '10mb' })); - setupSecurity(app); +// Security middleware +setupSecurity(app); - // ── Fail-Safe Storage Monitoring ──────────────────────── - app.use(checkStorageHealth); +// Static files (uploads) +app.use('/uploads', express.static(config.upload.dir)); - // WebSocket - io.on('connection', (socket) => { - const { partnerId, userId } = socket.handshake.query; - if (partnerId) socket.join(`partner:${partnerId}`); - if (userId) socket.join(`user:${userId}`); - }); - - // Health (enhanced with plugin info) - app.get('/api/health', (_req, res) => res.json({ +// Health check +app.get('/api/health', (_req, res) => { + res.json({ status: 'ok', - service: 'Clube de Benefícios', - architecture: 'Core + Plugins', - timestamp: new Date(), + service: config.app.name, + timestamp: new Date().toISOString(), uptime: process.uptime(), - plugins: pluginRegistry.getSummary(), - storage: { - available: storageProvider.isAvailable() - } - })); + }); +}); - // ── Legacy Routes (will be gradually replaced by plugins) ── - app.use('/api/auth', authRoutes); - app.use('/api/users', userRoutes); - app.use('/api/partners', partnerRoutes); - app.use('/api/benefits', benefitRoutes); - app.use('/api/admin', adminRoutes); - app.use('/api/webhooks', webhookRoutes); - app.use('/api/plugins', pluginRoutes); - app.use('/api/leads', leadRoutes); - app.use('/api/uploads', uploadRoutes); - app.use('/api/referrals', referralRoutes); +// API routes +app.use('/api/auth', authRoutes); +app.use('/api/users', userRoutes); +app.use('/api/partners', partnerRoutes); +app.use('/api/benefits', benefitRoutes); +app.use('/api/admin', adminRoutes); +app.use('/api/webhooks', webhookRoutes); +app.use('/api/plugins', pluginRoutes); - // Static - app.use('/uploads', express.static(config.upload.dir)); - app.use(express.static(path.join(process.cwd(), '../frontend'))); +// WebSocket events +io.on('connection', (socket) => { + logger.info(`Socket connected: ${socket.id}`); - // SPA Wildcard fallback - app.get('*', (req, res, next) => { - if (req.path.startsWith('/api') || req.path.startsWith('/uploads')) { - return next(); - } - res.sendFile(path.join(process.cwd(), '../frontend/index.html')); + socket.on('join:partner', (partnerId: string) => { + socket.join(`partner:${partnerId}`); }); - // ── Load & Activate Plugins ─────────────────────────────── - logger.info('═══════════════════════════════════'); - logger.info(' Clube67 — Plugin Architecture'); - logger.info('═══════════════════════════════════'); - await loadPlugins(app, io); - - server.listen(config.port, () => { - logger.info(`🚀 Server running on port ${config.port}`); - logger.info(`📦 Architecture: Core + ${pluginRegistry.getSummary().active} plugins`); + socket.on('join:user', (userId: string) => { + socket.join(`user:${userId}`); }); + + socket.on('disconnect', () => { + logger.debug(`Socket disconnected: ${socket.id}`); + }); +}); + +// Make io accessible to routes +app.set('io', io); + +// 404 +app.use((_req, res) => { + res.status(404).json({ error: 'Rota não encontrada' }); +}); + +// Error handler +app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => { + logger.error('Unhandled error:', err); + res.status(err.status || 500).json({ + error: config.app.env === 'production' ? 'Erro interno do servidor' : err.message, + }); +}); + +// Start +async function start() { + try { + // Test DB connection + await db.raw('SELECT 1'); + logger.info('✅ Database connected'); + + // Note: Schema managed via SQL (backend/schema.sql) + // Migrations are not auto-run to avoid .d.ts loading issues + + // Load plugins + await pluginService.loadAll(app); + + server.listen(config.app.port, () => { + logger.info(`🚀 ${config.app.name} running on port ${config.app.port}`); + console.log(`🚀 ${config.app.name} running on http://localhost:${config.app.port}`); + console.log(`📋 Health: http://localhost:${config.app.port}/api/health`); + }); + } catch (err) { + logger.error('❌ Failed to start server:', err); + console.error('❌ Failed to start:', err); + process.exit(1); + } } -start().catch(err => { - logger.error('Failed to start server:', err); - process.exit(1); -}); +start(); + +export { app, io }; diff --git a/backend/src/infra/cache/dragonfly.ts b/backend/src/infra/cache/dragonfly.ts deleted file mode 100644 index 66346a6..0000000 --- a/backend/src/infra/cache/dragonfly.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { redis } from '../../config/redis'; - -class DragonflyClient { - async get(key: string): Promise { - try { return await redis.get(key); } catch { return null; } - } - - async set(key: string, value: string, ttlSeconds?: number): Promise { - try { - if (ttlSeconds && ttlSeconds > 0) { - await redis.set(key, value, 'EX', ttlSeconds); - } else { - await redis.set(key, value); - } - } catch { } - } - - async del(key: string): Promise { - try { await redis.del(key); } catch { } - } - - async exists(key: string): Promise { - try { - const count = await redis.exists(key); - return count > 0; - } catch { return false; } - } - - async publish(channel: string, message: string): Promise { - try { await redis.publish(channel, message); } catch { } - } -} - -export const dragonfly = new DragonflyClient(); diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts index b10762e..7601ff4 100644 --- a/backend/src/middleware/auth.ts +++ b/backend/src/middleware/auth.ts @@ -1,44 +1,28 @@ import { Request, Response, NextFunction } from 'express'; import jwt from 'jsonwebtoken'; import { config } from '../config'; -import { db } from '../config/database'; +import db from '../config/database'; -export function generateTokens(user: any) { - const accessToken = jwt.sign( - { id: user.id, role: user.role, partner_id: user.partner_id }, - config.jwt.secret, - { expiresIn: config.jwt.expiresIn as any } - ); - const refreshToken = jwt.sign( - { id: user.id }, - config.jwt.refreshSecret, - { expiresIn: config.jwt.refreshExpiresIn as any } - ); - return { accessToken, refreshToken }; +export interface JwtPayload { + id: number; + email: string; + role: string; } -export async function authenticate(req: Request, res: Response, next: NextFunction) { +export interface AuthRequest extends Request { + user?: JwtPayload; +} + +export function authenticate(req: AuthRequest, res: Response, next: NextFunction) { const authHeader = req.headers.authorization; - if (!authHeader?.startsWith('Bearer ')) { - return res.status(401).json({ error: 'Token não fornecido' }); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Token de autenticação não fornecido' }); } const token = authHeader.split(' ')[1]; try { - const decoded = jwt.verify(token, config.jwt.secret) as any; + const decoded = jwt.verify(token, config.jwt.secret) as JwtPayload; req.user = decoded; - - if (decoded.role === 'partner' && decoded.partner_id) { - const partner = await db('partners').where({ id: decoded.partner_id }).first(); - (req as any).partnerConsent = Boolean(partner?.data_consent); - (req as any).partnerId = partner?.id; - if (!partner?.data_consent) { - const consentRoute = req.method === 'POST' && req.path === '/api/partners/consent'; - if (!consentRoute) { - return res.status(403).json({ error: 'Consentimento LGPD pendente' }); - } - } - } next(); } catch (err: any) { if (err.name === 'TokenExpiredError') { @@ -47,3 +31,37 @@ export async function authenticate(req: Request, res: Response, next: NextFuncti return res.status(401).json({ error: 'Token inválido' }); } } + +export function optionalAuth(req: AuthRequest, res: Response, next: NextFunction) { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return next(); + } + const token = authHeader.split(' ')[1]; + try { + req.user = jwt.verify(token, config.jwt.secret) as JwtPayload; + } catch { /* ignore */ } + next(); +} + +export function generateTokens(user: { id: number; email: string; role: string }) { + const payload: JwtPayload = { id: user.id, email: user.email, role: user.role }; + const accessToken = jwt.sign(payload, config.jwt.secret, { expiresIn: config.jwt.expiresIn as any }); + const refreshToken = jwt.sign(payload, config.jwt.refreshSecret, { expiresIn: config.jwt.refreshExpiresIn as any }); + return { accessToken, refreshToken }; +} + +export async function refreshAccessToken(refreshToken: string) { + try { + const decoded = jwt.verify(refreshToken, config.jwt.refreshSecret) as JwtPayload; + const user = await db('users').where('id', decoded.id).first(); + if (!user || user.refresh_token !== refreshToken) { + throw new Error('Invalid refresh token'); + } + const tokens = generateTokens(user); + await db('users').where('id', user.id).update({ refresh_token: tokens.refreshToken }); + return tokens; + } catch { + throw new Error('Invalid refresh token'); + } +} diff --git a/backend/src/middleware/rbac.ts b/backend/src/middleware/rbac.ts index 85e2577..aca5060 100644 --- a/backend/src/middleware/rbac.ts +++ b/backend/src/middleware/rbac.ts @@ -1,29 +1,40 @@ -import { Request, Response, NextFunction } from 'express'; +import { Response, NextFunction } from 'express'; +import { AuthRequest } from './auth'; -const roleHierarchy: Record = { - user: 1, - secretary: 2, - clinical_manager: 3, - finance: 3, - partner_admin: 4, - admin: 5, - super_admin: 10, +type Role = 'user' | 'partner_admin' | 'secretary' | 'clinical_manager' | 'finance' | 'super_admin'; + +const ROLE_HIERARCHY: Record = { + user: 0, + secretary: 1, + clinical_manager: 2, + finance: 2, + partner_admin: 3, + super_admin: 4, }; -export function authorize(roles: string[]) { - return (req: Request, res: Response, next: NextFunction) => { - if (!req.user || !roles.includes(req.user.role)) { - return res.status(403).json({ error: 'Acesso negado' }); +export function authorize(...allowedRoles: Role[]) { + return (req: AuthRequest, res: Response, next: NextFunction) => { + if (!req.user) { + return res.status(401).json({ error: 'Não autenticado' }); + } + const userRole = req.user.role as Role; + if (userRole === 'super_admin') return next(); // super admin bypasses all + if (!allowedRoles.includes(userRole)) { + return res.status(403).json({ error: 'Permissão insuficiente' }); } next(); }; } -export function minRole(role: string) { - return (req: Request, res: Response, next: NextFunction) => { - const userRole = req.user?.role || 'user'; - if (roleHierarchy[userRole] < roleHierarchy[role]) { - return res.status(403).json({ error: 'Nível de privilégio insuficiente' }); +export function requireRole(minRole: Role) { + return (req: AuthRequest, res: Response, next: NextFunction) => { + if (!req.user) { + return res.status(401).json({ error: 'Não autenticado' }); + } + const userLevel = ROLE_HIERARCHY[req.user.role as Role] ?? -1; + const requiredLevel = ROLE_HIERARCHY[minRole]; + if (userLevel < requiredLevel) { + return res.status(403).json({ error: 'Nível de permissão insuficiente' }); } next(); }; diff --git a/backend/src/middleware/security.ts b/backend/src/middleware/security.ts index 62eef2f..122915d 100644 --- a/backend/src/middleware/security.ts +++ b/backend/src/middleware/security.ts @@ -1,41 +1,81 @@ -import express, { Express } from 'express'; import helmet from 'helmet'; -import cors from 'cors'; import rateLimit from 'express-rate-limit'; +import cors from 'cors'; import compression from 'compression'; import hpp from 'hpp'; +import { Express, Request, Response, NextFunction } from 'express'; +import { config } from '../config'; export function setupSecurity(app: Express) { + // CORS + app.use(cors({ + origin: [config.app.frontendUrl, 'http://localhost:3000'], + credentials: true, + methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'X-CSRF-Token'], + })); + + // Helmet - security headers app.use(helmet({ contentSecurityPolicy: { directives: { - "default-src": ["'self'"], - "script-src": ["'self'", "'unsafe-inline'", "https://unpkg.com"], - "script-src-elem": ["'self'", "'unsafe-inline'", "https://unpkg.com"], - "style-src": ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"], - "img-src": ["'self'", "data:", "https://api.qrserver.com", "https://unpkg.com"], - "font-src": ["'self'", "https://fonts.gstatic.com"], - "connect-src": ["'self'", "https://unpkg.com"], - "object-src": ["'none'"] - } - } + defaultSrc: ["'self'"], + scriptSrc: ["'self'", "'unsafe-inline'"], + styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'], + imgSrc: ["'self'", 'data:', 'https:', 'blob:'], + fontSrc: ["'self'", 'https://fonts.gstatic.com'], + connectSrc: ["'self'", 'https://accounts.google.com', 'wss:', 'ws:'], + }, + }, + crossOriginEmbedderPolicy: false, })); - app.use(cors({ origin: '*' })); - app.use(compression()); - app.use(hpp()); - app.use(express.json({ limit: '10mb' })); - app.use(express.urlencoded({ extended: true, limit: '10mb' })); - const limiter = rateLimit({ + // Rate limiting + const generalLimiter = rateLimit({ windowMs: 15 * 60 * 1000, - max: 1000, // Increased from 100 to avoid blocking frontend navigation + max: 200, + message: { error: 'Muitas requisições. Tente novamente em 15 minutos.' }, + standardHeaders: true, + legacyHeaders: false, }); - app.use('/api/', limiter); + app.use('/api/', generalLimiter); const authLimiter = rateLimit({ - windowMs: 60 * 60 * 1000, - max: 20, - message: 'Muitas tentativas de login, tente novamente mais tarde', + windowMs: 15 * 60 * 1000, + max: 15, + message: { error: 'Muitas tentativas de login. Tente novamente em 15 minutos.' }, }); app.use('/api/auth/login', authLimiter); + app.use('/api/auth/register', authLimiter); + + // Compression + app.use(compression()); + + // HPP - HTTP Parameter Pollution + app.use(hpp()); + + // XSS sanitize via a simple middleware + app.use((req: Request, _res: Response, next: NextFunction) => { + if (req.body) { + sanitizeObject(req.body); + } + next(); + }); + + // Disable directory listing / powered-by + app.disable('x-powered-by'); +} + +function sanitizeObject(obj: any) { + for (const key in obj) { + if (typeof obj[key] === 'string') { + obj[key] = obj[key] + .replace(//g, '>') + .replace(/javascript:/gi, '') + .replace(/on\w+=/gi, ''); + } else if (typeof obj[key] === 'object' && obj[key] !== null) { + sanitizeObject(obj[key]); + } + } } diff --git a/backend/src/middleware/storage-health.ts b/backend/src/middleware/storage-health.ts deleted file mode 100644 index e36f2ed..0000000 --- a/backend/src/middleware/storage-health.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Request, Response, NextFunction } from 'express'; -import { storageProvider } from '../core/StorageProvider'; - -export const checkStorageHealth = (req: Request, res: Response, next: NextFunction) => { - // Bypass check for static assets and frontend pages (non-API routes) - if (!req.path.startsWith('/api')) return next(); - - // Exclude health check and PWA config so we can still monitor and register service workers - if (req.path === '/api/health' || req.path === '/api/pwa/config') return next(); - - if (!storageProvider.isAvailable()) { - return res.status(503).json({ - error: 'Serviço temporariamente indisponível', - message: 'O sistema de armazenamento (Wasabi) está fora do ar. Por segurança, todos os acessos foram bloqueados.' - }); - } - next(); -}; diff --git a/backend/src/middleware/upload.ts b/backend/src/middleware/upload.ts index 91857aa..372d118 100644 --- a/backend/src/middleware/upload.ts +++ b/backend/src/middleware/upload.ts @@ -2,23 +2,34 @@ import multer from 'multer'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; import { config } from '../config'; +import { Request } from 'express'; + +const ALLOWED_TYPES = [ + 'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', + 'application/pdf', 'application/zip', +]; const storage = multer.diskStorage({ - destination: config.upload.dir, - filename: (req, file, cb) => { - const ext = path.extname(file.originalname); - cb(null, `${uuidv4()}${ext}`); + destination: (_req: Request, _file: Express.Multer.File, cb) => { + cb(null, config.upload.dir); + }, + filename: (_req: Request, file: Express.Multer.File, cb) => { + const ext = path.extname(file.originalname).toLowerCase(); + const name = `${uuidv4()}${ext}`; + cb(null, name); }, }); +function fileFilter(_req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) { + if (ALLOWED_TYPES.includes(file.mimetype)) { + cb(null, true); + } else { + cb(new Error(`Tipo de arquivo não permitido: ${file.mimetype}`)); + } +} + export const upload = multer({ storage, + fileFilter, limits: { fileSize: config.upload.maxSize }, - fileFilter: (req, file, cb) => { - const allowed = /jpeg|jpg|png|webp|gif|pdf/; - const ext = allowed.test(path.extname(file.originalname).toLowerCase()); - const mime = allowed.test(file.mimetype); - if (ext && mime) return cb(null, true); - cb(new Error('Tipo de arquivo não permitido')); - }, }); diff --git a/backend/src/migrations/20260210_001_initial_schema.ts b/backend/src/migrations/20260210_001_initial_schema.ts new file mode 100644 index 0000000..8b0dd1d --- /dev/null +++ b/backend/src/migrations/20260210_001_initial_schema.ts @@ -0,0 +1,277 @@ +import { Knex } from 'knex'; + +export async function up(knex: Knex): Promise { + // ── users ── + await knex.schema.createTable('users', (t) => { + t.increments('id').primary(); + t.string('name', 255).notNullable(); + t.string('email', 255).unique().notNullable(); + t.string('password_hash', 255).nullable(); + t.string('phone', 30).nullable(); + t.string('cpf', 14).unique().nullable(); + t.string('city', 100).nullable(); + t.string('neighborhood', 100).nullable(); + t.enum('gender', ['M', 'F', 'O']).nullable(); + t.date('birth_date').nullable(); + t.string('avatar_url', 500).nullable(); + t.enum('role', ['user', 'partner_admin', 'secretary', 'clinical_manager', 'finance', 'super_admin']) + .notNullable().defaultTo('user'); + t.enum('status', ['lead', 'pending', 'active', 'inactive']).notNullable().defaultTo('active'); + t.boolean('email_verified').defaultTo(false); + t.string('refresh_token', 500).nullable(); + t.timestamp('last_login').nullable(); + t.timestamps(true, true); + + t.index('email'); + t.index('role'); + t.index('status'); + }); + + // ── oauth_accounts ── + await knex.schema.createTable('oauth_accounts', (t) => { + t.increments('id').primary(); + t.integer('user_id').unsigned().notNullable() + .references('id').inTable('users').onDelete('CASCADE'); + t.string('provider', 50).notNullable(); // google, facebook, etc + t.string('provider_user_id', 255).notNullable(); + t.string('access_token', 1000).nullable(); + t.string('refresh_token', 1000).nullable(); + t.timestamp('token_expires_at').nullable(); + t.json('profile_data').nullable(); + t.timestamps(true, true); + + t.unique(['provider', 'provider_user_id']); + t.index('user_id'); + }); + + // ── categories ── + await knex.schema.createTable('categories', (t) => { + t.increments('id').primary(); + t.string('name', 100).notNullable().unique(); + t.string('slug', 100).notNullable().unique(); + t.string('icon', 50).nullable(); + t.string('color', 50).nullable(); + t.text('description').nullable(); + t.boolean('active').defaultTo(true); + t.integer('sort_order').defaultTo(0); + t.timestamps(true, true); + }); + + // ── partners ── + await knex.schema.createTable('partners', (t) => { + t.increments('id').primary(); + t.string('company_name', 255).notNullable(); + t.string('slug', 255).unique().notNullable(); + t.string('cnpj', 20).unique().nullable(); + t.string('email', 255).nullable(); + t.string('phone', 30).nullable(); + t.string('address_street', 255).nullable(); + t.string('address_number', 20).nullable(); + t.string('address_neighborhood', 100).nullable(); + t.string('address_city', 100).nullable(); + t.string('address_state', 2).nullable(); + t.string('address_zip', 10).nullable(); + t.string('type', 100).notNullable(); + t.integer('monthly_goal').defaultTo(0); + t.enum('status', ['active', 'inactive']).notNullable().defaultTo('active'); + t.string('gradient_from', 50).nullable(); + t.string('gradient_to', 50).nullable(); + t.string('icon', 10).nullable(); + t.text('description').nullable(); + t.string('logo_url', 500).nullable(); + t.json('images').nullable(); + t.string('domain', 255).nullable(); + t.string('subdomain', 255).nullable(); + t.enum('subscription_plan', ['basic', 'pro', 'enterprise']).defaultTo('basic'); + t.integer('owner_user_id').unsigned().nullable() + .references('id').inTable('users').onDelete('SET NULL'); + t.timestamps(true, true); + + t.index('slug'); + t.index('status'); + }); + + // ── benefits ── + await knex.schema.createTable('benefits', (t) => { + t.increments('id').primary(); + t.integer('partner_id').unsigned().notNullable() + .references('id').inTable('partners').onDelete('CASCADE'); + t.integer('category_id').unsigned().nullable() + .references('id').inTable('categories').onDelete('SET NULL'); + t.string('title', 255).notNullable(); + t.text('description').nullable(); + t.text('rules').nullable(); + t.string('contact', 255).nullable(); + t.string('type', 50).notNullable(); + t.enum('delivery_type', ['local', 'external']).notNullable().defaultTo('local'); + t.boolean('active').defaultTo(true); + t.boolean('is_global').defaultTo(false); + t.boolean('approved_by_admin').defaultTo(false); + t.enum('global_request_status', ['none', 'pending', 'approved']).defaultTo('none'); + t.integer('priority').defaultTo(0); + t.decimal('discount_percent', 5, 2).nullable(); + t.date('valid_until').nullable(); + t.string('featured_image', 500).nullable(); + t.enum('redemption_type', ['coupon', 'qrcode', 'card', 'app']).defaultTo('coupon'); + t.integer('usage_count').defaultTo(0); + t.decimal('savings_amount', 12, 2).defaultTo(0); + t.string('badge', 50).nullable(); + t.timestamps(true, true); + + t.index('partner_id'); + t.index('category_id'); + t.index('active'); + t.index('type'); + }); + + // ── transactions ── + await knex.schema.createTable('transactions', (t) => { + t.increments('id').primary(); + t.integer('user_id').unsigned().notNullable() + .references('id').inTable('users').onDelete('CASCADE'); + t.integer('benefit_id').unsigned().notNullable() + .references('id').inTable('benefits').onDelete('CASCADE'); + t.integer('partner_id').unsigned().notNullable() + .references('id').inTable('partners').onDelete('CASCADE'); + t.enum('type', ['redemption', 'view', 'favorite', 'share']).notNullable(); + t.enum('status', ['pending', 'completed', 'cancelled', 'expired']).defaultTo('pending'); + t.string('redemption_code', 100).nullable(); + t.string('qr_code_data', 500).nullable(); + t.decimal('original_value', 12, 2).nullable(); + t.decimal('discount_value', 12, 2).nullable(); + t.timestamp('redeemed_at').nullable(); + t.timestamp('expires_at').nullable(); + t.json('metadata').nullable(); + t.timestamps(true, true); + + t.index('user_id'); + t.index('benefit_id'); + t.index('status'); + t.index('redemption_code'); + }); + + // ── favorites ── + await knex.schema.createTable('favorites', (t) => { + t.increments('id').primary(); + t.integer('user_id').unsigned().notNullable() + .references('id').inTable('users').onDelete('CASCADE'); + t.integer('benefit_id').unsigned().notNullable() + .references('id').inTable('benefits').onDelete('CASCADE'); + t.timestamps(true, true); + + t.unique(['user_id', 'benefit_id']); + }); + + // ── sessions ── + await knex.schema.createTable('sessions', (t) => { + t.increments('id').primary(); + t.integer('user_id').unsigned().notNullable() + .references('id').inTable('users').onDelete('CASCADE'); + t.string('token_hash', 255).notNullable(); + t.string('ip_address', 45).nullable(); + t.string('user_agent', 500).nullable(); + t.timestamp('expires_at').notNullable(); + t.boolean('is_active').defaultTo(true); + t.timestamps(true, true); + + t.index('user_id'); + t.index('token_hash'); + }); + + // ── logs (audit) ── + await knex.schema.createTable('logs', (t) => { + t.increments('id').primary(); + t.integer('user_id').unsigned().nullable() + .references('id').inTable('users').onDelete('SET NULL'); + t.string('action', 100).notNullable(); + t.string('entity_type', 100).nullable(); + t.integer('entity_id').nullable(); + t.text('description').nullable(); + t.json('old_data').nullable(); + t.json('new_data').nullable(); + t.string('ip_address', 45).nullable(); + t.string('user_agent', 500).nullable(); + t.timestamps(true, true); + + t.index('user_id'); + t.index('action'); + t.index('entity_type'); + t.index('created_at'); + }); + + // ── plugins ── + await knex.schema.createTable('plugins', (t) => { + t.increments('id').primary(); + t.string('name', 255).notNullable().unique(); + t.string('slug', 255).notNullable().unique(); + t.string('version', 20).notNullable(); + t.text('description').nullable(); + t.string('author', 255).nullable(); + t.string('entry_point', 255).notNullable(); + t.boolean('active').defaultTo(false); + t.json('dependencies').nullable(); + t.json('config').nullable(); + t.json('hooks').nullable(); + t.string('directory', 500).notNullable(); + t.timestamps(true, true); + + t.index('slug'); + t.index('active'); + }); + + // ── webhooks ── + await knex.schema.createTable('webhooks', (t) => { + t.increments('id').primary(); + t.integer('partner_id').unsigned().nullable() + .references('id').inTable('partners').onDelete('CASCADE'); + t.string('url', 500).notNullable(); + t.string('secret', 255).nullable(); + t.json('events').notNullable(); // ['benefit.created', 'user.registered', etc] + t.boolean('active').defaultTo(true); + t.integer('retry_count').defaultTo(0); + t.integer('max_retries').defaultTo(3); + t.timestamp('last_triggered').nullable(); + t.enum('last_status', ['success', 'failed', 'pending']).nullable(); + t.timestamps(true, true); + + t.index('partner_id'); + t.index('active'); + }); + + // ── benefit_share_requests ── + await knex.schema.createTable('benefit_share_requests', (t) => { + t.increments('id').primary(); + t.integer('benefit_id').unsigned().notNullable() + .references('id').inTable('benefits').onDelete('CASCADE'); + t.integer('requesting_partner_id').unsigned().notNullable() + .references('id').inTable('partners').onDelete('CASCADE'); + t.integer('owner_partner_id').unsigned().notNullable() + .references('id').inTable('partners').onDelete('CASCADE'); + t.enum('status', ['pending', 'approved', 'rejected']).defaultTo('pending'); + t.timestamps(true, true); + }); + + // ── benefit_partner_links ── + await knex.schema.createTable('benefit_partner_links', (t) => { + t.increments('id').primary(); + t.integer('benefit_id').unsigned().notNullable() + .references('id').inTable('benefits').onDelete('CASCADE'); + t.integer('partner_id').unsigned().notNullable() + .references('id').inTable('partners').onDelete('CASCADE'); + t.timestamps(true, true); + + t.unique(['benefit_id', 'partner_id']); + }); +} + +export async function down(knex: Knex): Promise { + const tables = [ + 'benefit_partner_links', 'benefit_share_requests', + 'webhooks', 'plugins', 'logs', 'sessions', + 'favorites', 'transactions', 'benefits', + 'partners', 'categories', 'oauth_accounts', 'users', + ]; + for (const table of tables) { + await knex.schema.dropTableIfExists(table); + } +} diff --git a/backend/src/modules/whatsapp/engine/index.js b/backend/src/modules/whatsapp/engine/index.js deleted file mode 100644 index a018c18..0000000 --- a/backend/src/modules/whatsapp/engine/index.js +++ /dev/null @@ -1,80 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -Object.defineProperty(exports, "__esModule", { value: true }); -exports.proto = exports.isJidStatusBroadcast = exports.isJidGroup = exports.generateWAMessageFromContent = exports.generateMessageIDV2 = exports.getContentType = exports.downloadMediaMessage = exports.makeCacheableSignalKeyStore = exports.fetchLatestBaileysVersion = exports.DisconnectReason = exports.useMultiFileAuthState = exports.makeWASocket = void 0; -exports.getEngine = getEngine; -const official = __importStar(require("./official")); -const infinite = __importStar(require("./infinite")); -// Engine padrão global: lida de BAILEYS_ENGINE (fallback para 'infinite') -const defaultEngineType = process.env.BAILEYS_ENGINE ?? 'infinite'; -function resolveEngine(type) { - return type === 'infinite' ? infinite : official; -} -/** - * Retorna os exports da engine selecionada para uma instância específica. - * Permite que cada instância use uma engine diferente (infinite ou official). - * Se não informado, usa BAILEYS_ENGINE do ambiente (padrão: 'infinite'). - */ -function getEngine(engineType) { - const e = resolveEngine(engineType ?? defaultEngineType); - return { - makeWASocket: e.makeWASocket, - useMultiFileAuthState: e.useMultiFileAuthState, - fetchLatestBaileysVersion: e.fetchLatestBaileysVersion, - makeCacheableSignalKeyStore: e.makeCacheableSignalKeyStore, - downloadMediaMessage: e.downloadMediaMessage, - getContentType: e.getContentType, - generateMessageIDV2: e.generateMessageIDV2, - generateWAMessageFromContent: e.generateWAMessageFromContent, - isJidGroup: e.isJidGroup, - isJidStatusBroadcast: e.isJidStatusBroadcast, - }; -} -// Exports estáticos: usam a engine global (retrocompatibilidade com código existente) -const selectedEngine = resolveEngine(defaultEngineType); -exports.makeWASocket = selectedEngine.makeWASocket; -exports.useMultiFileAuthState = selectedEngine.useMultiFileAuthState; -exports.DisconnectReason = selectedEngine.DisconnectReason; -exports.fetchLatestBaileysVersion = selectedEngine.fetchLatestBaileysVersion; -exports.makeCacheableSignalKeyStore = selectedEngine.makeCacheableSignalKeyStore; -exports.downloadMediaMessage = selectedEngine.downloadMediaMessage; -exports.getContentType = selectedEngine.getContentType; -exports.generateMessageIDV2 = selectedEngine.generateMessageIDV2; -exports.generateWAMessageFromContent = selectedEngine.generateWAMessageFromContent; -exports.isJidGroup = selectedEngine.isJidGroup; -exports.isJidStatusBroadcast = selectedEngine.isJidStatusBroadcast; -var official_1 = require("./official"); -Object.defineProperty(exports, "proto", { enumerable: true, get: function () { return official_1.proto; } }); -//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/backend/src/modules/whatsapp/engine/index.js.map b/backend/src/modules/whatsapp/engine/index.js.map deleted file mode 100644 index b458f61..0000000 --- a/backend/src/modules/whatsapp/engine/index.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeA,8BAyBC;AAxCD,qDAAsC;AACtC,qDAAsC;AAEtC,0EAA0E;AAC1E,MAAM,iBAAiB,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,UAAU,CAAA;AAElE,SAAS,aAAa,CAAC,IAAY;IACjC,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAA;AAClD,CAAC;AAED;;;;GAIG;AACH,SAAgB,SAAS,CAAC,UAA0B;IAYlD,MAAM,CAAC,GAAG,aAAa,CAAC,UAAU,IAAI,iBAAiB,CAAC,CAAA;IACxD,OAAO;QACL,YAAY,EAAgB,CAAC,CAAC,YAAmB;QACjD,qBAAqB,EAAO,CAAC,CAAC,qBAA4B;QAC1D,yBAAyB,EAAG,CAAC,CAAC,yBAAgC;QAC9D,2BAA2B,EAAE,CAAC,CAAC,2BAAkC;QACjE,oBAAoB,EAAQ,CAAC,CAAC,oBAA2B;QACzD,cAAc,EAAc,CAAC,CAAC,cAAqB;QACnD,mBAAmB,EAAS,CAAC,CAAC,mBAA0B;QACxD,4BAA4B,EAAE,CAAC,CAAC,4BAAmC;QACnE,UAAU,EAAkB,CAAC,CAAC,UAAiB;QAC/C,oBAAoB,EAAQ,CAAC,CAAC,oBAA2B;KAC1D,CAAA;AACH,CAAC;AAED,sFAAsF;AACtF,MAAM,cAAc,GAAG,aAAa,CAAC,iBAAiB,CAAC,CAAA;AAE1C,QAAA,YAAY,GAAG,cAAc,CAAC,YAAmB,CAAA;AACjD,QAAA,qBAAqB,GAAG,cAAc,CAAC,qBAA4B,CAAA;AACnE,QAAA,gBAAgB,GAAG,cAAc,CAAC,gBAAuB,CAAA;AACzD,QAAA,yBAAyB,GAAG,cAAc,CAAC,yBAAgC,CAAA;AAC3E,QAAA,2BAA2B,GAAG,cAAc,CAAC,2BAAkC,CAAA;AAC/E,QAAA,oBAAoB,GAAG,cAAc,CAAC,oBAA2B,CAAA;AACjE,QAAA,cAAc,GAAG,cAAc,CAAC,cAAqB,CAAA;AACrD,QAAA,mBAAmB,GAAG,cAAc,CAAC,mBAA0B,CAAA;AAC/D,QAAA,4BAA4B,GAAG,cAAc,CAAC,4BAAmC,CAAA;AACjF,QAAA,UAAU,GAAG,cAAc,CAAC,UAAiB,CAAA;AAC7C,QAAA,oBAAoB,GAAG,cAAc,CAAC,oBAA2B,CAAA;AAC9E,uCAAkC;AAAzB,iGAAA,KAAK,OAAA"} \ No newline at end of file diff --git a/backend/src/modules/whatsapp/engine/index.ts b/backend/src/modules/whatsapp/engine/index.ts deleted file mode 100644 index 99a5130..0000000 --- a/backend/src/modules/whatsapp/engine/index.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as official from './official' -import * as infinite from './infinite' - -// Engine padrão global: lida de BAILEYS_ENGINE (fallback para 'infinite') -const defaultEngineType = process.env.BAILEYS_ENGINE ?? 'infinite' - -function resolveEngine(type: string) { - return type === 'infinite' ? infinite : official -} - -/** - * Retorna os exports da engine selecionada para uma instância específica. - * Permite que cada instância use uma engine diferente (infinite ou official). - * Se não informado, usa BAILEYS_ENGINE do ambiente (padrão: 'infinite'). - */ -export function getEngine(engineType?: string | null): { - makeWASocket: any - useMultiFileAuthState: any - fetchLatestBaileysVersion: any - makeCacheableSignalKeyStore: any - downloadMediaMessage: any - getContentType: any - generateMessageIDV2: any - generateWAMessageFromContent: any - isJidGroup: any - isJidStatusBroadcast: any -} { - const e = resolveEngine(engineType ?? defaultEngineType) - return { - makeWASocket: e.makeWASocket as any, - useMultiFileAuthState: e.useMultiFileAuthState as any, - fetchLatestBaileysVersion: e.fetchLatestBaileysVersion as any, - makeCacheableSignalKeyStore: e.makeCacheableSignalKeyStore as any, - downloadMediaMessage: e.downloadMediaMessage as any, - getContentType: e.getContentType as any, - generateMessageIDV2: e.generateMessageIDV2 as any, - generateWAMessageFromContent: e.generateWAMessageFromContent as any, - isJidGroup: e.isJidGroup as any, - isJidStatusBroadcast: e.isJidStatusBroadcast as any, - } -} - -// Exports estáticos: usam a engine global (retrocompatibilidade com código existente) -const selectedEngine = resolveEngine(defaultEngineType) - -export const makeWASocket = selectedEngine.makeWASocket as any -export const useMultiFileAuthState = selectedEngine.useMultiFileAuthState as any -export const DisconnectReason = selectedEngine.DisconnectReason as any -export const fetchLatestBaileysVersion = selectedEngine.fetchLatestBaileysVersion as any -export const makeCacheableSignalKeyStore = selectedEngine.makeCacheableSignalKeyStore as any -export const downloadMediaMessage = selectedEngine.downloadMediaMessage as any -export const getContentType = selectedEngine.getContentType as any -export const generateMessageIDV2 = selectedEngine.generateMessageIDV2 as any -export const generateWAMessageFromContent = selectedEngine.generateWAMessageFromContent as any -export const isJidGroup = selectedEngine.isJidGroup as any -export const isJidStatusBroadcast = selectedEngine.isJidStatusBroadcast as any -export { proto } from './official' - -// Exportação estática de tipos para garantir segurança em tempo de compilação -export type { WASocket, ConnectionState, WAMessage, Contact, BinaryNode } from './official' diff --git a/backend/src/modules/whatsapp/engine/infinite.js b/backend/src/modules/whatsapp/engine/infinite.js deleted file mode 100644 index 0334eaf..0000000 --- a/backend/src/modules/whatsapp/engine/infinite.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("baileys"), exports); -//# sourceMappingURL=infinite.js.map \ No newline at end of file diff --git a/backend/src/modules/whatsapp/engine/infinite.js.map b/backend/src/modules/whatsapp/engine/infinite.js.map deleted file mode 100644 index d527562..0000000 --- a/backend/src/modules/whatsapp/engine/infinite.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"infinite.js","sourceRoot":"","sources":["infinite.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0CAAuB"} \ No newline at end of file diff --git a/backend/src/modules/whatsapp/engine/infinite.ts b/backend/src/modules/whatsapp/engine/infinite.ts deleted file mode 100644 index 707df2d..0000000 --- a/backend/src/modules/whatsapp/engine/infinite.ts +++ /dev/null @@ -1 +0,0 @@ -export * from 'baileys' diff --git a/backend/src/modules/whatsapp/engine/official.js b/backend/src/modules/whatsapp/engine/official.js deleted file mode 100644 index 5fa93aa..0000000 --- a/backend/src/modules/whatsapp/engine/official.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("@whiskeysockets/baileys"), exports); -//# sourceMappingURL=official.js.map \ No newline at end of file diff --git a/backend/src/modules/whatsapp/engine/official.js.map b/backend/src/modules/whatsapp/engine/official.js.map deleted file mode 100644 index 91e8421..0000000 --- a/backend/src/modules/whatsapp/engine/official.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"official.js","sourceRoot":"","sources":["official.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0DAAuC"} \ No newline at end of file diff --git a/backend/src/modules/whatsapp/engine/official.ts b/backend/src/modules/whatsapp/engine/official.ts deleted file mode 100644 index 707df2d..0000000 --- a/backend/src/modules/whatsapp/engine/official.ts +++ /dev/null @@ -1 +0,0 @@ -export * from 'baileys' diff --git a/backend/src/modules/whatsapp/rich-message.ts b/backend/src/modules/whatsapp/rich-message.ts deleted file mode 100644 index 81f6780..0000000 --- a/backend/src/modules/whatsapp/rich-message.ts +++ /dev/null @@ -1,411 +0,0 @@ -/** - * rich-message.ts — Construção e envio de mensagens ricas via Baileys padrão - * - * O pacote @whiskeysockets/baileys não suporta os campos `nativeButtons`, - * `nativeList` e `nativeCarousel` que são específicos do fork InfiniteAPI. - * Este módulo replica a lógica de transformação do InfiniteAPI para que - * o motor newwhats possa enviar botões, listas e carrosséis usando o - * sock.relayMessage() com o proto correto. - * - * Referência: github:rsalcara/InfiniteAPI — lib/Socket/messages-send.js - * - * IMPORTANTE: O WhatsApp Web NÃO suporta viewOnceMessage para mensagens interativas. - * Por isso usamos interactiveMessage DIRETO (sem wrapper viewOnce) + additionalNodes. - * - * O que é necessário para botões/listas/carrosséis funcionarem no WhatsApp - * (celular E web): - * - * 1. interactiveMessage direto (NÃO viewOnceMessage) — viewOnce não carrega no Web - * - * 2. Nó adicional - * injetado no stanza via relayMessage(additionalNodes) - * (sem isso o WhatsApp Web exibe "Não foi possível carregar") - * - * 3. Nó para chats 1-a-1 - * (necessário para interactive messages em conversas privadas) - * - * Lista (single_select) também usa o mesmo fluxo — InfiniteAPI converte - * o viewOnceMessage > nativeFlowMessage de volta para listMessage no relayMessage, - * mas testamos que a forma direta com nó biz > list também funciona. - */ -import { generateMessageIDV2, generateWAMessageFromContent, isJidGroup, isJidStatusBroadcast } from './engine' -import { proto } from './engine' -import type { WASocket, BinaryNode } from './engine' - -// ─── Tipos de botão (espelho do formato nativo InfiniteAPI) ────────────────── -type NativeBtn = - | { type: 'reply'; id: string; text: string } - | { type: 'url'; text: string; url: string } - | { type: 'copy'; text: string; copyText: string } - | { type: 'call'; text: string; phoneNumber: string } - -// ─── Payload rico de entrada (o que o frontend/ext-api envia) ──────────────── -export interface RichPayload { - text?: string - footer?: string - nativeButtons?: NativeBtn[] - nativeList?: { - buttonText: string - sections: Array<{ - title: string - rows: Array<{ id: string; title: string; description?: string }> - }> - } - nativeCarousel?: { - cards: Array<{ - title?: string - body?: string - footer?: string - image?: { url: string } - buttons: Array<{ type?: string; id?: string; text: string }> - }> - } - poll?: { - name: string - values: string[] - selectableCount?: number - } -} - -// ─── Converte um botão nativo para o formato buttonParamsJson do WhatsApp ───── -function formatNativeFlowButton(btn: NativeBtn): { name: string; buttonParamsJson: string } { - switch (btn.type) { - case 'url': - return { - name: 'cta_url', - buttonParamsJson: JSON.stringify({ - display_text: btn.text, - url: btn.url, - merchant_url: btn.url, - }), - } - case 'copy': - return { - name: 'cta_copy', - buttonParamsJson: JSON.stringify({ - display_text: btn.text, - copy_code: btn.copyText, - }), - } - case 'reply': - return { - name: 'quick_reply', - buttonParamsJson: JSON.stringify({ - display_text: btn.text, - id: btn.id, - }), - } - case 'call': - return { - name: 'cta_call', - buttonParamsJson: JSON.stringify({ - display_text: btn.text, - phone_number: btn.phoneNumber, - }), - } - } -} - -// ─── Nó biz > interactive > native_flow (obrigatório para renderizar no Web) ── -const BIZ_NATIVE_FLOW_NODE: BinaryNode = { - tag: 'biz', - attrs: {}, - content: [ - { - tag: 'interactive', - attrs: { type: 'native_flow', v: '1' }, - content: [ - { - tag: 'native_flow', - attrs: { v: '9', name: 'mixed' }, - }, - ], - }, - ], -} - -// ─── Nó biz para listMessage legado (formato que renderiza no Web) ─────────── -// InfiniteAPI usa com proto.listMessage -// (não interactiveMessage). Comentário do fork: "Modern format causes rejection -// (error 479) — Legacy format works on all platforms". -const BIZ_LIST_NODE: BinaryNode = { - tag: 'biz', - attrs: {}, - content: [ - { - tag: 'list', - attrs: { type: 'product_list', v: '2' }, - }, - ], -} - -// ─── Nó bot (necessário para chats 1-a-1 com interactive messages) ──────────── -const BOT_NODE: BinaryNode = { - tag: 'bot', - attrs: { biz_bot: '1' }, -} - -// ─── Helper: jid é chat privado (não grupo, não status, não bot) ───────────── -function isPrivateChat(jid: string): boolean { - return ( - !isJidGroup(jid) && - !isJidStatusBroadcast(jid) && - !jid.includes('newsletter') && - !jid.includes('broadcast') - ) -} - -// ─── Helper: envia proto pré-construído pelo MESMO caminho do sendMessage ──── -// Diferença crítica vs `sock.relayMessage` direto: -// sendMessage() → generateWAMessage() → WAProto.Message.fromObject() → relayMessage() -// ↑ normalização proto que o WA Web exige -// Sem esse passo, o Web descarta a mensagem ("Não foi possível carregar"). -// É o mesmo motivo pelo qual nossas enquetes (via sendMessage) funcionam no Web -// e os botões (via relayMessage direto) não funcionavam. -async function sendBuiltProto( - sock: WASocket, - jid: string, - protoMsg: any, - msgId: string, - additionalNodes: BinaryNode[], - quoted?: { key: any; message: any }, -): Promise { - const fullMsg = generateWAMessageFromContent(jid, protoMsg, { - userJid: sock.user?.id, - messageId: msgId, - quoted, - } as any) - - await sock.relayMessage(jid, fullMsg.message as any, { - messageId: fullMsg.key.id!, - additionalNodes, - }) - return fullMsg.key.id! -} - -// ─── Helper: monta interactiveMessage DIRETO (sem viewOnceMessage wrapper) ──── -// viewOnceMessage wrapper faz o WhatsApp Web exibir "Não foi possível carregar": -// o Web trata viewOnce como mensagem efêmera e recusa conteúdo interativo dentro. -// Baileys padrão não adiciona messageContextInfo para interactiveMessage -// (só faz isso para poll, com messageSecret). Usar interactiveMessage direto -// + nó no additionalNodes é o caminho correto com @whiskeysockets/baileys. -function makeInteractiveProto(interactiveMsg: any): any { - return { - interactiveMessage: interactiveMsg, - } -} - -/** - * Envia uma mensagem rica pelo socket Baileys padrão. - * - * - Buttons / CTAs: interactiveMessage direto + nó biz>interactive>native_flow + nó bot - * - Carousel: interactiveMessage direto + nó biz>interactive>native_flow - * - List: interactiveMessage direto + nó biz>interactive>native_flow + nó bot - * - Poll: sock.sendMessage (requer encriptação especial do Baileys) - * - Text: sock.sendMessage normal - * - * Retorna o messageId gerado. - */ -export async function sendRichMessage( - sock: WASocket, - jid: string, - payload: RichPayload, - quoted?: { key: any; message: any }, - engineType?: string, -): Promise { - const { text, footer, nativeButtons, nativeList, nativeCarousel, poll } = payload - const engine = engineType ?? process.env.BAILEYS_ENGINE ?? 'infinite' - - // Se a engine ativa for 'infinite', utilizamos o suporte nativo do fork InfiniteAPI - if (engine === 'infinite') { - // ── Poll: delegado ao sendMessage padrão (requer encriptação especial do Baileys) ── - if (poll) { - const sent = await sock.sendMessage( - jid, - { - poll: { - name: poll.name, - values: poll.values, - selectableCount: Math.min(Math.max(1, poll.selectableCount ?? 1), poll.values.length), - }, - }, - quoted ? { quoted } : undefined, - ) - return sent?.key?.id ?? `poll-${Date.now()}` - } - - // ── Botões / CTAs nativos (InfiniteAPI) ── - if (nativeButtons) { - const sent = await sock.sendMessage( - jid, - { - text: text ?? '', - footer, - nativeButtons, - } as any, - quoted ? { quoted } : undefined, - ) - return sent?.key?.id ?? `btn-${Date.now()}` - } - - // ── Carrossel nativo (InfiniteAPI) ── - if (nativeCarousel) { - const sent = await sock.sendMessage( - jid, - { - text: text ?? '', - footer, - nativeCarousel, - } as any, - quoted ? { quoted } : undefined, - ) - return sent?.key?.id ?? `carousel-${Date.now()}` - } - - // ── Lista nativa (InfiniteAPI) ── - if (nativeList) { - const sent = await sock.sendMessage( - jid, - { - text: text ?? '', - footer, - nativeList, - } as any, - quoted ? { quoted } : undefined, - ) - return sent?.key?.id ?? `list-${Date.now()}` - } - - // ── Texto simples (InfiniteAPI) ── - const sent = await sock.sendMessage( - jid, - { text: text ?? '' }, - quoted ? { quoted } : undefined, - ) - return sent?.key?.id ?? `text-${Date.now()}` - } - - // FALLBACK: Lógica de montagem manual de nós binários para o Baileys oficial - // ── Poll: delegado ao sendMessage padrão (requer messageSecret) ─────────── - if (poll) { - const sent = await sock.sendMessage( - jid, - { - poll: { - name: poll.name, - values: poll.values, - selectableCount: Math.min(Math.max(1, poll.selectableCount ?? 1), poll.values.length), - }, - }, - quoted ? { quoted } : undefined, - ) - return sent?.key?.id ?? `poll-${Date.now()}` - } - - // ── Texto simples: delegado ao sendMessage padrão ───────────────────────── - if (!nativeButtons && !nativeList && !nativeCarousel) { - const sent = await sock.sendMessage( - jid, - { text: text ?? '' }, - quoted ? { quoted } : undefined, - ) - return sent?.key?.id ?? `text-${Date.now()}` - } - - const msgId = generateMessageIDV2(sock.user?.id) - const isPrivate = isPrivateChat(jid) - - // ── Botões / CTAs: interactiveMessage + nativeFlowMessage ──────────────────── - if (nativeButtons) { - const formattedButtons = nativeButtons.map(formatNativeFlowButton) - - const protoMsg = makeInteractiveProto({ - body: { text: text ?? '' }, - footer: footer ? { text: footer } : undefined, - header: { title: '', subtitle: '', hasMediaAttachment: false }, - nativeFlowMessage: { - buttons: formattedButtons, - messageParamsJson: JSON.stringify({}), - messageVersion: 2, - }, - }) - - const additionalNodes: BinaryNode[] = [BIZ_NATIVE_FLOW_NODE] - return await sendBuiltProto(sock, jid, protoMsg, msgId, additionalNodes, quoted) - } - - // ── Carrossel: viewOnceMessage > interactiveMessage > carouselMessage ───── - if (nativeCarousel) { - const cards = nativeCarousel.cards - if (cards.length < 2) throw new Error('Carrossel requer no mínimo 2 cards') - if (cards.length > 10) throw new Error('Carrossel suporta no máximo 10 cards') - - const carouselCards: any[] = cards.map((card) => { - const rawButtons = card.buttons ?? [] - const cardButtons = rawButtons.length > 0 - ? rawButtons.map((btn) => formatNativeFlowButton({ - type: (btn.type as any) ?? 'reply', - id: btn.id ?? btn.text.toLowerCase().replace(/\s+/g, '_'), - text: btn.text, - } as any)) - : [{ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'Ver mais', id: 'see_more' }) }] - - return { - header: { - title: card.title ?? '', - subtitle: '', - hasMediaAttachment: !!(card.image?.url), - }, - body: { text: card.body ?? '' }, - footer: card.footer ? { text: card.footer } : undefined, - nativeFlowMessage: { - buttons: cardButtons, - messageParamsJson: JSON.stringify({}), - }, - } - }) - - const protoMsg = makeInteractiveProto({ - body: { text: text ?? '' }, - footer: footer ? { text: footer } : undefined, - header: { title: '', subtitle: '', hasMediaAttachment: false }, - carouselMessage: { - cards: carouselCards, - messageVersion: 1, - }, - }) - - const additionalNodes: BinaryNode[] = [BIZ_NATIVE_FLOW_NODE] - return await sendBuiltProto(sock, jid, protoMsg, msgId, additionalNodes, quoted) - } - - // ── Lista: proto.listMessage LEGADO + nó ── - if (nativeList) { - const protoMsg: any = { - listMessage: { - title: '', - description: text ?? '', - buttonText: nativeList.buttonText, - footerText: footer ?? '', - listType: 2, - sections: nativeList.sections.map((s) => ({ - title: s.title, - rows: s.rows.map((r) => ({ - rowId: r.id, - title: r.title, - description: r.description ?? '', - })), - })), - }, - } - - const additionalNodes: BinaryNode[] = [BIZ_LIST_NODE] - if (isPrivate) additionalNodes.push(BOT_NODE) - - return await sendBuiltProto(sock, jid, protoMsg, msgId, additionalNodes, quoted) - } - - // Fallback (não deve chegar aqui) - const sent = await sock.sendMessage(jid, { text: text ?? '' }) - return sent?.key?.id ?? `fallback-${Date.now()}` -} diff --git a/backend/src/routes/admin.routes.ts b/backend/src/routes/admin.routes.ts index ef14377..948294f 100644 --- a/backend/src/routes/admin.routes.ts +++ b/backend/src/routes/admin.routes.ts @@ -1,678 +1,15 @@ import { Router } from 'express'; +import { adminController } from '../controllers/admin.controller'; import { authenticate } from '../middleware/auth'; import { authorize } from '../middleware/rbac'; -import { db } from '../config/database'; -import fs from 'fs/promises'; -import path from 'path'; -import net from 'net'; -import { spawn } from 'child_process'; const router = Router(); -const PROJECT_ROOT = path.resolve(process.cwd()); -const FRONTEND_DIR = path.join(PROJECT_ROOT, 'frontend'); -const LIVE_DIR = path.join(FRONTEND_DIR, '.next'); -const STAGING_DIR = path.join(FRONTEND_DIR, '.next-staging-build'); -const HISTORY_DIR = path.join(FRONTEND_DIR, '.next-history'); -const HISTORY_FILE = path.join(PROJECT_ROOT, 'storage', 'version-manager-history.json'); +router.use(authenticate); +router.use(authorize('super_admin')); -type VersionAction = 'deploy' | 'rollback'; -type VersionStatus = 'success' | 'failed'; -type HistoryEntry = { - id: string; - action: VersionAction; - status: VersionStatus; - from_version?: string | null; - to_version?: string | null; - user_id?: number | string; - user_name?: string; - created_at: string; - meta?: Record; -}; - -async function readHistory(): Promise { - try { - const raw = await fs.readFile(HISTORY_FILE, 'utf-8'); - const data = JSON.parse(raw); - return Array.isArray(data) ? data : []; - } catch { - return []; - } -} - -async function writeHistory(entries: HistoryEntry[]): Promise { - await fs.mkdir(path.dirname(HISTORY_FILE), { recursive: true }); - await fs.writeFile(HISTORY_FILE, JSON.stringify(entries, null, 2)); -} - -async function getBuildId(dir: string): Promise { - try { - const raw = await fs.readFile(path.join(dir, 'BUILD_ID'), 'utf-8'); - return raw.trim() || null; - } catch { - return null; - } -} - -async function getDirMtime(dir: string): Promise { - try { - const stat = await fs.stat(dir); - return stat.mtime.toISOString(); - } catch { - return null; - } -} - -function checkPort(port: number, host = '127.0.0.1', timeoutMs = 500): Promise { - return new Promise(resolve => { - const socket = new net.Socket(); - const onDone = (ok: boolean) => { - socket.destroy(); - resolve(ok); - }; - socket.setTimeout(timeoutMs); - socket.once('connect', () => onDone(true)); - socket.once('timeout', () => onDone(false)); - socket.once('error', () => onDone(false)); - socket.connect(port, host); - }); -} - -async function snapshotLiveIfExists(): Promise { - const liveId = await getBuildId(LIVE_DIR); - if (!liveId) return null; - const target = path.join(HISTORY_DIR, liveId); - try { - await fs.mkdir(HISTORY_DIR, { recursive: true }); - await fs.access(target); - return liveId; // already snapped - } catch { - await fs.cp(LIVE_DIR, target, { recursive: true }); - return liveId; - } -} - -async function replaceLiveWith(sourceDir: string): Promise { - const backupDir = `${LIVE_DIR}-backup-${Date.now()}`; - const tempDeployDir = `${LIVE_DIR}-deploying-${Date.now()}`; - - // 1. Copiar para pasta temporária de deploy para garantir integridade - await fs.cp(sourceDir, tempDeployDir, { recursive: true }); - - // 2. Se houver live atual, move para backup (Atomic Swap part 1) - if (await fs.access(LIVE_DIR).then(() => true).catch(() => false)) { - await fs.rename(LIVE_DIR, backupDir); - } - - try { - // 3. Move temp para live (Atomic Swap part 2) - await fs.rename(tempDeployDir, LIVE_DIR); - - // Limpar backup antigo se tudo correu bem (opcional, aqui mantemos por segurança 1 versão) - const oldBackups = await fs.readdir(path.dirname(LIVE_DIR)); - for (const f of oldBackups) { - if (f.startsWith('.next-backup-') && f !== path.basename(backupDir)) { - await fs.rm(path.join(path.dirname(LIVE_DIR), f), { recursive: true, force: true }).catch(() => { }); - } - } - } catch (err) { - // Rollback imediato se o swap falhar - if (await fs.access(backupDir).then(() => true).catch(() => false)) { - await fs.rename(backupDir, LIVE_DIR); - } - throw err; - } -} - -async function restartLive(): Promise { - const { exec } = await import('child_process'); - const cmd = 'npx pm2 restart ecosystem.config.js --only clube67-frontend --update-env'; - const env = { - ...process.env, - PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}`, - }; - await new Promise((resolve, reject) => { - exec(cmd, { cwd: PROJECT_ROOT, env }, (err, stdout, stderr) => { - if (err) return reject(new Error(stderr || err.message)); - resolve(); - }); - }); -} - -async function buildStaging(): Promise { - const { exec } = await import('child_process'); - const cmd = 'BUILD_STAGING_ONLY=1 BUILD_STAGING=1 bash build.sh'; - const env = { - ...process.env, - PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}`, - }; - await new Promise((resolve, reject) => { - exec(cmd, { cwd: PROJECT_ROOT, env }, (err, stdout, stderr) => { - if (err) return reject(new Error(stderr || err.message)); - resolve(); - }); - }); -} - -async function buildLive(): Promise { - const { exec } = await import('child_process'); - const cmd = 'env -u NEXT_BASE_PATH -u NEXT_DIST_DIR -u NEXT_PUBLIC_BACKEND_URL npm run build'; - const env = { - ...process.env, - PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}`, - }; - await new Promise((resolve, reject) => { - exec(cmd, { cwd: FRONTEND_DIR, env }, (err, stdout, stderr) => { - if (err) return reject(new Error(stderr || err.message)); - resolve(); - }); - }); -} - -function createStreamWriter(res: any) { - return (line: string) => { - if (!res.writableEnded) { - res.write(line.endsWith('\n') ? line : `${line}\n`); - } - }; -} - -async function streamCommand(cmd: string, cwd: string, env: NodeJS.ProcessEnv, write: (line: string) => void): Promise { - await new Promise((resolve, reject) => { - const child = spawn(cmd, { cwd, env, shell: true }); - child.stdout.on('data', chunk => write(chunk.toString())); - child.stderr.on('data', chunk => write(chunk.toString())); - child.on('error', err => reject(err)); - child.on('close', code => { - if (code === 0) resolve(); - else reject(new Error(`Command failed (${code}): ${cmd}`)); - }); - }); -} - -router.get('/dashboard', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const [activePartners] = await db('partners').where({ status: 'active' }).count('id as count'); - const [totalBenefits] = await db('benefits').count('id as count'); - const [globalBenefits] = await db('benefits').where({ is_global: true }).count('id as count'); - - // Month Leads: count leads created in current month - const now = new Date(); - const firstDay = new Date(now.getFullYear(), now.getMonth(), 1); - const [monthLeads] = await db('leads') - .where('created_at', '>=', firstDay) - .count('id as count'); - - res.json({ - activePartners: Number(activePartners.count), - totalBenefits: Number(totalBenefits.count), - monthLeads: Number(monthLeads.count), - globalBenefits: Number(globalBenefits.count) - }); - } catch (err: any) { - console.error(err); - res.status(500).json({ error: 'Erro ao carregar dashboard' }); - } -}); - -router.get('/partners', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const partners = await db('partners') - .join('users', 'partners.owner_user_id', 'users.id') - .select( - 'partners.id', - 'partners.company_name', - 'partners.slug', - 'partners.cnpj', - 'partners.email as partner_email', - 'partners.status', - 'partners.type', - 'partners.created_at', - 'users.name as owner_name', - 'users.email as owner_email' - ) - .orderBy('partners.created_at', 'desc'); - res.json({ partners }); - } catch (err: any) { - console.error(err); - res.status(500).json({ error: 'Erro ao listar parceiros' }); - } -}); - -router.patch('/partners/:id/status', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const { id } = req.params; - const { status } = req.body; - if (!['active', 'inactive', 'pending'].includes(status)) { - return res.status(400).json({ error: 'Status inválido' }); - } - - await db('partners').where({ id }).update({ status }); - res.json({ success: true }); - } catch (err: any) { - console.error(err); - res.status(500).json({ error: 'Erro ao atualizar status do parceiro' }); - } -}); - -router.put('/partners/:id', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const { id } = req.params; - const { company_name, slug, cnpj, partner_email, status, type } = req.body; - - const updates: any = { - company_name, - slug, - cnpj, - email: partner_email, - status, - type - }; - - // Remove undefined values - Object.keys(updates).forEach(key => updates[key] === undefined && delete updates[key]); - - await db('partners').where({ id }).update(updates); - res.json({ success: true }); - } catch (err: any) { - console.error(err); - res.status(500).json({ error: 'Erro ao editar parceiro' }); - } -}); - -// Version Manager -router.get('/version-manager/status', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const [history, liveId, testId, liveMtime, testMtime] = await Promise.all([ - readHistory(), - getBuildId(LIVE_DIR), - getBuildId(STAGING_DIR), - getDirMtime(LIVE_DIR), - getDirMtime(STAGING_DIR), - ]); - - const lastDeploy = history.find(h => h.action === 'deploy' && h.status === 'success'); - - const [liveUp, testUp, backendUp] = await Promise.all([ - checkPort(3000), - checkPort(3001), - checkPort(3002), - ]); - - res.json({ - live: { - version: liveId, - published_at: lastDeploy?.created_at || liveMtime, - published_by: lastDeploy?.user_name || null, - }, - test: { - version: testId, - last_build_at: testMtime, - }, - health: { - live: liveUp, - test: testUp, - backend: backendUp, - }, - }); - } catch (err: any) { - console.error(err); - res.status(500).json({ error: 'Erro ao carregar status' }); - } -}); - -router.get('/version-manager/history', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const limit = Number(req.query.limit || 50); - const history = await readHistory(); - res.json(history.slice(0, Math.max(1, limit))); - } catch (err: any) { - console.error(err); - res.status(500).json({ error: 'Erro ao carregar historico' }); - } -}); - -router.post('/version-manager/prepare-test', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const confirm = Boolean(req.body?.confirm); - if (!confirm) return res.status(400).json({ error: 'Confirmacao obrigatoria' }); - - const stream = String(req.query.stream || '') === '1'; - if (stream) { - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.setHeader('Cache-Control', 'no-store'); - res.setHeader('X-Accel-Buffering', 'no'); - if (typeof (res as any).flushHeaders === 'function') (res as any).flushHeaders(); - const write = createStreamWriter(res); - write('== Preparar TEST =='); - write('> Rodando build staging...'); - await streamCommand( - 'BUILD_STAGING_ONLY=1 BUILD_STAGING=1 bash build.sh', - PROJECT_ROOT, - { ...process.env, PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}` }, - write - ); - write('> Reiniciando staging...'); - await streamCommand( - 'npx pm2 restart ecosystem.config.js --only clube67-frontend-staging --update-env', - PROJECT_ROOT, - { ...process.env, PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}` }, - write - ); - const testId = await getBuildId(STAGING_DIR); - const testMtime = await getDirMtime(STAGING_DIR); - if (!testId) throw new Error('Build TEST nao encontrado'); - write(`> Build TEST: ${testId}`); - write(`> Atualizado em: ${testMtime || '—'}`); - write('== OK =='); - res.end(); - return; - } - - await buildStaging(); - await streamCommand( - 'npx pm2 restart ecosystem.config.js --only clube67-frontend-staging --update-env', - PROJECT_ROOT, - { ...process.env, PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}` }, - () => { } - ); - const testId = await getBuildId(STAGING_DIR); - const testMtime = await getDirMtime(STAGING_DIR); - - if (!testId) return res.status(500).json({ error: 'Build TEST nao encontrado' }); - - res.json({ success: true, test: { version: testId, last_build_at: testMtime } }); - } catch (err: any) { - console.error(err); - if (!res.headersSent) { - res.status(500).json({ error: 'Erro ao preparar build TEST' }); - } else { - try { - const write = createStreamWriter(res); - write(`ERROR: ${err?.message || 'Erro ao preparar build TEST'}`); - } finally { - res.end(); - } - } - } -}); - -router.post('/version-manager/restart-test', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const confirm = Boolean(req.body?.confirm); - if (!confirm) return res.status(400).json({ error: 'Confirmacao obrigatoria' }); - - const stream = String(req.query.stream || '') === '1'; - if (stream) { - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.setHeader('Cache-Control', 'no-store'); - res.setHeader('X-Accel-Buffering', 'no'); - if (typeof (res as any).flushHeaders === 'function') (res as any).flushHeaders(); - const write = createStreamWriter(res); - write('== Reiniciar TEST =='); - await streamCommand( - 'npx pm2 restart ecosystem.config.js --only clube67-frontend-staging --update-env', - PROJECT_ROOT, - { ...process.env, PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}` }, - write - ); - const testId = await getBuildId(STAGING_DIR); - write(`> Test build: ${testId || '—'}`); - write('== OK =='); - res.end(); - return; - } - - await streamCommand( - 'npx pm2 restart ecosystem.config.js --only clube67-frontend-staging --update-env', - PROJECT_ROOT, - { ...process.env, PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}` }, - () => { } - ); - const testId = await getBuildId(STAGING_DIR); - res.json({ success: true, test: { version: testId || '—' } }); - } catch (err: any) { - console.error(err); - if (!res.headersSent) { - res.status(500).json({ error: 'Erro ao reiniciar TEST' }); - } else { - try { - const write = createStreamWriter(res); - write(`ERROR: ${err?.message || 'Erro ao reiniciar TEST'}`); - } finally { - res.end(); - } - } - } -}); - -router.post('/version-manager/fix-live', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const confirm = Boolean(req.body?.confirm); - if (!confirm) return res.status(400).json({ error: 'Confirmacao obrigatoria' }); - - const stream = String(req.query.stream || '') === '1'; - if (stream) { - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.setHeader('Cache-Control', 'no-store'); - res.setHeader('X-Accel-Buffering', 'no'); - if (typeof (res as any).flushHeaders === 'function') (res as any).flushHeaders(); - const write = createStreamWriter(res); - write('== Corrigir LIVE =='); - write('> Rodando build live...'); - await streamCommand( - 'env -u NEXT_BASE_PATH -u NEXT_DIST_DIR -u NEXT_PUBLIC_BACKEND_URL npm run build', - FRONTEND_DIR, - { ...process.env, PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}` }, - write - ); - write('> Reiniciando live...'); - await streamCommand( - 'npx pm2 restart ecosystem.config.js --only clube67-frontend --update-env', - PROJECT_ROOT, - { ...process.env, PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}` }, - write - ); - const liveId = await getBuildId(LIVE_DIR); - const liveMtime = await getDirMtime(LIVE_DIR); - write(`> Live: ${liveId || '—'}`); - write(`> Atualizado em: ${liveMtime || '—'}`); - write('== OK =='); - res.end(); - return; - } - - await buildLive(); - await restartLive(); - - const liveId = await getBuildId(LIVE_DIR); - const liveMtime = await getDirMtime(LIVE_DIR); - - res.json({ success: true, live: { version: liveId, published_at: liveMtime } }); - } catch (err: any) { - console.error(err); - if (!res.headersSent) { - res.status(500).json({ error: 'Erro ao corrigir LIVE' }); - } else { - try { - const write = createStreamWriter(res); - write(`ERROR: ${err?.message || 'Erro ao corrigir LIVE'}`); - } finally { - res.end(); - } - } - } -}); - -router.post('/version-manager/deploy', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const confirm = Boolean(req.body?.confirm); - if (!confirm) return res.status(400).json({ error: 'Confirmacao obrigatoria' }); - - const stream = String(req.query.stream || '') === '1'; - if (stream) { - res.setHeader('Content-Type', 'text/plain; charset=utf-8'); - res.setHeader('Cache-Control', 'no-store'); - res.setHeader('X-Accel-Buffering', 'no'); - if (typeof (res as any).flushHeaders === 'function') (res as any).flushHeaders(); - const write = createStreamWriter(res); - write('== Publicar TEST -> LIVE =='); - const testId = await getBuildId(STAGING_DIR); - if (!testId) throw new Error('Build TEST nao encontrado'); - write(`> Test build: ${testId}`); - write('> Fazendo snapshot da live...'); - const fromId = await snapshotLiveIfExists(); - write(`> Snapshot: ${fromId || '—'}`); - write('> Substituindo live...'); - await replaceLiveWith(STAGING_DIR); - write('> Reiniciando live...'); - await streamCommand( - 'npx pm2 restart ecosystem.config.js --only clube67-frontend --update-env', - PROJECT_ROOT, - { ...process.env, PATH: `/www/server/nodejs/v24.13.0/bin:${process.env.PATH || ''}` }, - write - ); - - const userId = (req.user as any)?.id; - const userRow = userId ? await db('users').where({ id: userId }).first() : null; - const userName = userRow?.name || 'Super Admin'; - - const entry: HistoryEntry = { - id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - action: 'deploy', - status: 'success', - from_version: fromId, - to_version: testId, - user_id: userId, - user_name: userName, - created_at: new Date().toISOString(), - }; - - const history = await readHistory(); - await writeHistory([entry, ...history]); - - write(`> Live agora: ${testId}`); - write(`> Publicado por: ${userName}`); - - write('> Aguardando inicialização (Health Check)...'); - await new Promise(r => setTimeout(r, 5000)); - const isAlive = await checkPort(3000); - if (isAlive) { - write('✅ Health Check: LIVE está respondendo na porta 3000!'); - } else { - write('⚠️ Health Check: LIVE não respondeu na porta 3000 após 5s. Verifique os logs do PM2.'); - } - - write('== OK =='); - res.end(); - return; - } - - const testId = await getBuildId(STAGING_DIR); - if (!testId) return res.status(400).json({ error: 'Build TEST nao encontrado' }); - - const fromId = await snapshotLiveIfExists(); - await replaceLiveWith(STAGING_DIR); - await restartLive(); - - // Health check silencioso para o log de histórico - await new Promise(r => setTimeout(r, 5000)); - const finalHealth = await checkPort(3000); - - const userId = (req.user as any)?.id; - const userRow = userId ? await db('users').where({ id: userId }).first() : null; - const userName = userRow?.name || 'Super Admin'; - - const entry: HistoryEntry = { - id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - action: 'deploy', - status: 'success', - from_version: fromId, - to_version: testId, - user_id: userId, - user_name: userName, - created_at: new Date().toISOString(), - meta: { health: finalHealth ? 'ok' : 'failed' } - }; - - const history = await readHistory(); - await writeHistory([entry, ...history]); - - res.json({ success: true, live: { version: testId, published_at: entry.created_at, published_by: userName } }); - } catch (err: any) { - console.error(err); - const history = await readHistory(); - const entry: HistoryEntry = { - id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - action: 'deploy', - status: 'failed', - created_at: new Date().toISOString(), - meta: { error: err?.message || 'Erro desconhecido' }, - }; - await writeHistory([entry, ...history]); - if (!res.headersSent) { - res.status(500).json({ error: 'Erro ao publicar TEST em LIVE' }); - } else { - try { - const write = createStreamWriter(res); - write(`ERROR: ${err?.message || 'Erro ao publicar TEST em LIVE'}`); - } finally { - res.end(); - } - } - } -}); - -router.post('/version-manager/rollback', authenticate, authorize(['super_admin']), async (req, res) => { - try { - const confirm = Boolean(req.body?.confirm); - const target = String(req.body?.target_version || '').trim(); - if (!confirm) return res.status(400).json({ error: 'Confirmacao obrigatoria' }); - if (!target) return res.status(400).json({ error: 'Versao alvo obrigatoria' }); - - const targetDir = path.join(HISTORY_DIR, target); - try { - await fs.access(targetDir); - } catch { - return res.status(404).json({ error: 'Versao alvo nao encontrada' }); - } - - const fromId = await snapshotLiveIfExists(); - await replaceLiveWith(targetDir); - await restartLive(); - - const userId = (req.user as any)?.id; - const userRow = userId ? await db('users').where({ id: userId }).first() : null; - const userName = userRow?.name || 'Super Admin'; - - const entry: HistoryEntry = { - id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - action: 'rollback', - status: 'success', - from_version: fromId, - to_version: target, - user_id: userId, - user_name: userName, - created_at: new Date().toISOString(), - }; - - const history = await readHistory(); - await writeHistory([entry, ...history]); - - res.json({ success: true, live: { version: target, published_at: entry.created_at, published_by: userName } }); - } catch (err: any) { - console.error(err); - const history = await readHistory(); - const entry: HistoryEntry = { - id: `${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, - action: 'rollback', - status: 'failed', - created_at: new Date().toISOString(), - meta: { error: err?.message || 'Erro desconhecido' }, - }; - await writeHistory([entry, ...history]); - res.status(500).json({ error: 'Erro ao realizar rollback' }); - } -}); +router.get('/dashboard', adminController.getDashboard); +router.get('/logs', adminController.getAuditLogs); +router.get('/metrics', adminController.getMetrics); export default router; diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index f4838c0..0989184 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -1,11 +1,14 @@ import { Router } from 'express'; -import * as authController from '../controllers/auth.controller'; +import { authController } from '../controllers/auth.controller'; import { authenticate } from '../middleware/auth'; const router = Router(); + router.post('/register', authController.register); router.post('/login', authController.login); -router.get('/me', authenticate, authController.profile); -router.post('/complete', authenticate, authController.completeProfile); +router.post('/refresh', authController.refresh); +router.post('/google', authController.googleCallback); +router.get('/me', authenticate, authController.me); +router.post('/logout', authenticate, authController.logout); export default router; diff --git a/backend/src/routes/benefit.routes.ts b/backend/src/routes/benefit.routes.ts index 77d2cbd..c46b815 100644 --- a/backend/src/routes/benefit.routes.ts +++ b/backend/src/routes/benefit.routes.ts @@ -1,14 +1,19 @@ import { Router } from 'express'; -import * as benefitController from '../controllers/benefit.controller'; -import { authenticate } from '../middleware/auth'; +import { benefitController } from '../controllers/benefit.controller'; +import { authenticate, optionalAuth } from '../middleware/auth'; +import { authorize } from '../middleware/rbac'; +import { upload } from '../middleware/upload'; const router = Router(); -router.get('/', benefitController.getAll); + +router.get('/', optionalAuth, benefitController.list); router.get('/categories', benefitController.getCategories); -router.post('/', authenticate, benefitController.create); -router.put('/:id', authenticate, benefitController.update); -router.delete('/:id', authenticate, benefitController.deleteBenefit); -router.post('/:id/toggle', authenticate, benefitController.toggle); -router.post('/:id/use', authenticate, benefitController.useBenefit); +router.get('/:id', optionalAuth, benefitController.getById); +router.post('/', authenticate, authorize('partner_admin', 'super_admin'), benefitController.create); +router.put('/:id', authenticate, authorize('partner_admin', 'super_admin'), benefitController.update); +router.delete('/:id', authenticate, authorize('partner_admin', 'super_admin'), benefitController.delete); +router.post('/:id/approve', authenticate, authorize('super_admin'), benefitController.approve); +router.post('/:id/promote-global', authenticate, authorize('super_admin'), benefitController.promoteGlobal); +router.post('/:id/redeem', authenticate, benefitController.redeem); export default router; diff --git a/backend/src/routes/lead.routes.ts b/backend/src/routes/lead.routes.ts deleted file mode 100644 index cc696e4..0000000 --- a/backend/src/routes/lead.routes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Router } from 'express'; -import * as leadController from '../controllers/lead.controller'; -import { authenticate } from '../middleware/auth'; -import { minRole } from '../middleware/rbac'; - -const router = Router(); - -router.get('/', authenticate, minRole('partner_admin'), leadController.getAll); -router.put('/:id', authenticate, minRole('partner_admin'), leadController.updateStatus); - -export default router; diff --git a/backend/src/routes/partner.routes.ts b/backend/src/routes/partner.routes.ts index 41f9567..caa297a 100644 --- a/backend/src/routes/partner.routes.ts +++ b/backend/src/routes/partner.routes.ts @@ -1,12 +1,18 @@ import { Router } from 'express'; -import * as partnerController from '../controllers/partner.controller'; -import { authenticate } from '../middleware/auth'; +import { partnerController } from '../controllers/partner.controller'; +import { authenticate, optionalAuth } from '../middleware/auth'; +import { authorize } from '../middleware/rbac'; +import { upload } from '../middleware/upload'; const router = Router(); -router.get('/', partnerController.getAll); -router.get('/:id', partnerController.getById); -router.get('/:slug', partnerController.getBySlug); -router.put('/:id', authenticate, partnerController.updatePartner); -router.delete('/:id', authenticate, partnerController.deletePartner); + +router.get('/', optionalAuth, partnerController.list); +router.get('/slug/:slug', optionalAuth, partnerController.getBySlug); +router.get('/:id', optionalAuth, partnerController.getById); +router.get('/:id/stats', authenticate, authorize('partner_admin', 'super_admin'), partnerController.getDashboardStats); +router.post('/', authenticate, authorize('super_admin'), partnerController.create); +router.put('/:id', authenticate, authorize('partner_admin', 'super_admin'), partnerController.update); +router.post('/:id/logo', authenticate, authorize('partner_admin', 'super_admin'), upload.single('logo'), partnerController.uploadLogo); +router.delete('/:id', authenticate, authorize('super_admin'), partnerController.delete); export default router; diff --git a/backend/src/routes/plugin.routes.ts b/backend/src/routes/plugin.routes.ts index 535d01c..f6ca7db 100644 --- a/backend/src/routes/plugin.routes.ts +++ b/backend/src/routes/plugin.routes.ts @@ -1,28 +1,95 @@ -import { Router } from 'express'; -import { pluginConfig } from '../core/plugin-config'; -import { authenticate } from '../middleware/auth'; +import { Router, Response } from 'express'; +import { authenticate, AuthRequest } from '../middleware/auth'; import { authorize } from '../middleware/rbac'; +import { pluginService } from '../services/plugin.service'; +import { upload } from '../middleware/upload'; +import fs from 'fs'; +import path from 'path'; +import { execSync } from 'child_process'; const router = Router(); -// Get Plugin Config -router.get('/:name/config', authenticate, authorize(['super_admin']), (req, res) => { - const { name } = req.params; - const config = pluginConfig.get(name); - // Mask secrets if needed? For now, super admin sees all. - res.json(config); +router.use(authenticate); +router.use(authorize('super_admin')); + +// List all plugins +router.get('/', async (_req: AuthRequest, res: Response) => { + try { + const plugins = await pluginService.getAll(); + const loaded = pluginService.getLoadedPlugins(); + res.json({ plugins, loaded }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao listar plugins' }); + } }); -// Set Plugin Config -router.post('/:name/config', authenticate, authorize(['super_admin']), (req, res) => { - const { name } = req.params; - const config = req.body; - +// Install plugin from ZIP +router.post('/install', upload.single('plugin'), async (req: AuthRequest, res: Response) => { try { - pluginConfig.set(name, config); - res.json({ success: true, message: 'Configuração salva com sucesso' }); - } catch (error) { - res.status(500).json({ error: 'Erro ao salvar configuração' }); + if (!req.file) return res.status(400).json({ error: 'Arquivo ZIP obrigatório' }); + + const zipPath = req.file.path; + const pluginsDir = path.resolve(__dirname, '../../../plugins'); + const tempDir = path.join(pluginsDir, '_temp_' + Date.now()); + + fs.mkdirSync(tempDir, { recursive: true }); + execSync(`unzip -o "${zipPath}" -d "${tempDir}"`); + + // Find manifest + const items = fs.readdirSync(tempDir); + let pluginRoot = tempDir; + if (items.length === 1 && fs.statSync(path.join(tempDir, items[0])).isDirectory()) { + pluginRoot = path.join(tempDir, items[0]); + } + + const manifestPath = path.join(pluginRoot, 'manifest.json'); + if (!fs.existsSync(manifestPath)) { + fs.rmSync(tempDir, { recursive: true }); + return res.status(400).json({ error: 'manifest.json not found in plugin' }); + } + + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); + const finalDir = path.join(pluginsDir, manifest.slug); + if (fs.existsSync(finalDir)) fs.rmSync(finalDir, { recursive: true }); + fs.renameSync(pluginRoot, finalDir); + if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true }); + fs.unlinkSync(zipPath); + + const result = await pluginService.install(manifest.slug); + res.status(201).json({ plugin: result }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao instalar plugin', details: err.message }); + } +}); + +// Activate +router.post('/:slug/activate', async (req: AuthRequest, res: Response) => { + try { + const app = req.app as any; + await pluginService.activate(app, req.params.slug); + res.json({ message: 'Plugin ativado' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao ativar plugin', details: err.message }); + } +}); + +// Deactivate +router.post('/:slug/deactivate', async (req: AuthRequest, res: Response) => { + try { + await pluginService.deactivate(req.params.slug); + res.json({ message: 'Plugin desativado' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao desativar plugin' }); + } +}); + +// Remove +router.delete('/:slug', async (req: AuthRequest, res: Response) => { + try { + await pluginService.remove(req.params.slug); + res.json({ message: 'Plugin removido' }); + } catch (err: any) { + res.status(500).json({ error: 'Erro ao remover plugin' }); } }); diff --git a/backend/src/routes/referral.routes.ts b/backend/src/routes/referral.routes.ts deleted file mode 100644 index a6e0f66..0000000 --- a/backend/src/routes/referral.routes.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Router } from 'express'; -import * as referralController from '../controllers/referral.controller'; -import { authenticate } from '../middleware/auth'; - -const router = Router(); - -// Publico: registra clique no link de indicacao -router.post('/track', referralController.trackClick); - -// Autenticado: estatisticas do usuario -router.get('/my-stats', authenticate, referralController.getMyStats); - -// Interno: converter indicacao -router.post('/convert', authenticate, referralController.convert); - -export default router; diff --git a/backend/src/routes/upload.routes.ts b/backend/src/routes/upload.routes.ts deleted file mode 100644 index 9cf6c0c..0000000 --- a/backend/src/routes/upload.routes.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Router } from 'express'; -import { upload } from '../middleware/upload'; - -const router = Router(); - -router.post('/', upload.single('file'), (req, res) => { - if (!req.file) { - return res.status(400).json({ error: 'Nenhum arquivo enviado' }); - } - - const url = `/uploads/${req.file.filename}`; - - res.json({ - success: true, - url, - filename: req.file.filename, - mimetype: req.file.mimetype, - size: req.file.size - }); -}); - -export default router; diff --git a/backend/src/routes/user.routes.ts b/backend/src/routes/user.routes.ts index c267ccf..1eac284 100644 --- a/backend/src/routes/user.routes.ts +++ b/backend/src/routes/user.routes.ts @@ -1,24 +1,19 @@ import { Router } from 'express'; -import * as userController from '../controllers/user.controller'; +import { userController } from '../controllers/user.controller'; import { authenticate } from '../middleware/auth'; import { authorize } from '../middleware/rbac'; const router = Router(); -// List Users (Admin/Partner Admin) - Assuming partner admin can list users assigned to them or global? -// Current impl allows global view. -router.get('/', authenticate, authorize(['super_admin', 'partner_admin']), userController.getAll); - -// User Stats & Benefits (Self/Authenticated) -router.get('/me/stats', authenticate, userController.getStats); -router.get('/me/benefits', authenticate, userController.getMyBenefits); - -// Admin Actions (Super Admin Only) -// Delete User -router.delete('/:id', authenticate, authorize(['super_admin']), userController.deleteUser); -// Update Status (Ban/Block) -router.patch('/:id/status', authenticate, authorize(['super_admin']), userController.updateStatus); -// Edit User (Name/Email/Role) -router.put('/:id', authenticate, authorize(['super_admin']), userController.updateUser); +router.get('/', authenticate, authorize('super_admin', 'partner_admin'), userController.list); +router.get('/me/favorites', authenticate, userController.getFavorites); +router.post('/me/favorites', authenticate, userController.addFavorite); +router.delete('/me/favorites/:benefitId', authenticate, userController.removeFavorite); +router.get('/me/history', authenticate, userController.getHistory); +router.get('/me/stats', authenticate, userController.getDashboardStats); +router.get('/:id', authenticate, userController.getById); +router.put('/:id', authenticate, userController.update); +router.patch('/:id/role', authenticate, authorize('super_admin'), userController.updateRole); +router.delete('/:id', authenticate, authorize('super_admin'), userController.delete); export default router; diff --git a/backend/src/routes/webhook.routes.ts b/backend/src/routes/webhook.routes.ts index a156bff..2e7280c 100644 --- a/backend/src/routes/webhook.routes.ts +++ b/backend/src/routes/webhook.routes.ts @@ -1,2 +1,17 @@ import { Router } from 'express'; -export default Router(); +import { webhookController } from '../controllers/webhook.controller'; +import { authenticate } from '../middleware/auth'; +import { authorize } from '../middleware/rbac'; + +const router = Router(); + +router.use(authenticate); +router.use(authorize('super_admin', 'partner_admin')); + +router.get('/', webhookController.list); +router.post('/', webhookController.create); +router.put('/:id', webhookController.update); +router.delete('/:id', webhookController.delete); +router.post('/:id/test', webhookController.test); + +export default router; diff --git a/backend/src/seeds/001_initial_data.ts b/backend/src/seeds/001_initial_data.ts new file mode 100644 index 0000000..fef8add --- /dev/null +++ b/backend/src/seeds/001_initial_data.ts @@ -0,0 +1,183 @@ +import { Knex } from 'knex'; +import bcrypt from 'bcryptjs'; + +export async function seed(knex: Knex): Promise { + // ── Super Admin ── + const passwordHash = await bcrypt.hash('Rc362514', 12); + const [existingAdmin] = await knex('users').where('email', 'ruibto@gmail.com'); + if (!existingAdmin) { + await knex('users').insert({ + name: 'Super Admin', + email: 'ruibto@gmail.com', + password_hash: passwordHash, + role: 'super_admin', + status: 'active', + email_verified: true, + }); + } + + // ── Categories ── + const categories = [ + { name: 'Gastronomia', slug: 'gastronomia', icon: '🍽️', color: '#FF6B35' }, + { name: 'Saúde', slug: 'saude', icon: '🏥', color: '#4CAF50' }, + { name: 'Educação', slug: 'educacao', icon: '📚', color: '#2196F3' }, + { name: 'Lazer', slug: 'lazer', icon: '🎭', color: '#9C27B0' }, + { name: 'Esportes', slug: 'esportes', icon: '⚽', color: '#FF9800' }, + { name: 'Compras', slug: 'compras', icon: '🛍️', color: '#E91E63' }, + { name: 'Serviços', slug: 'servicos', icon: '🔧', color: '#607D8B' }, + { name: 'Viagens', slug: 'viagens', icon: '✈️', color: '#00BCD4' }, + { name: 'Beleza & Estética', slug: 'beleza-estetica', icon: '💅', color: '#F06292' }, + { name: 'Odontologia', slug: 'odontologia', icon: '🦷', color: '#26A69A' }, + { name: 'Nutrição', slug: 'nutricao', icon: '🥗', color: '#8BC34A' }, + { name: 'Farmácia', slug: 'farmacia', icon: '💊', color: '#EF5350' }, + ]; + for (const cat of categories) { + const exists = await knex('categories').where('slug', cat.slug).first(); + if (!exists) await knex('categories').insert(cat); + } + + // ── Partners ── + const superAdmin = await knex('users').where('email', 'ruibto@gmail.com').first(); + const partners = [ + { + company_name: 'Escola Coração de Maria', + slug: 'coracaodemaria', + cnpj: '11.222.333/0001-44', + phone: '(67) 3333-1111', + address_street: 'Rua das Flores', address_number: '123', + address_neighborhood: 'Centro', address_city: 'Campo Grande', + address_state: 'MS', address_zip: '79000-001', + type: 'Escola', monthly_goal: 50, status: 'active', + gradient_from: '#3B82F6', gradient_to: '#1D4ED8', + icon: '🏫', description: 'Instituição de ensino fundamental e médio.', + owner_user_id: superAdmin?.id, + }, + { + company_name: 'Drogasil Filial Centro', + slug: 'drogasilcentro', + cnpj: '44.555.666/0001-77', + phone: '(67) 3333-2222', + address_street: 'Avenida Principal', address_number: '456', + address_neighborhood: 'Tiradentes', address_city: 'Campo Grande', + address_state: 'MS', address_zip: '79000-002', + type: 'Farmácia', monthly_goal: 75, status: 'active', + gradient_from: '#EF4444', gradient_to: '#B91C1C', + icon: '⚕️', description: 'Rede de farmácias com ampla variedade.', + owner_user_id: superAdmin?.id, + }, + { + company_name: 'SmartFit Unidade Centro', + slug: 'smartfitcentro', + cnpj: null, + phone: '(67) 3333-3333', + address_street: 'Travessa Esportiva', address_number: '789', + address_neighborhood: 'Aero Rancho', address_city: 'Campo Grande', + address_state: 'MS', address_zip: '79000-003', + type: 'Academia', monthly_goal: 100, status: 'active', + gradient_from: '#EAB308', gradient_to: '#F97316', + icon: '🏋️', description: 'Academia com equipamentos modernos.', + owner_user_id: superAdmin?.id, + }, + { + company_name: 'Consultt Clinic', + slug: 'consulttclinic', + cnpj: null, + phone: '(67) 9999-0000', + address_street: 'Rua Odonto', address_number: '100', + address_neighborhood: 'Centro', address_city: 'Campo Grande', + address_state: 'MS', address_zip: '79000-100', + type: 'Odontologia', monthly_goal: 30, status: 'active', + gradient_from: '#06B6D4', gradient_to: '#3B82F6', + icon: '🦷', description: 'Clínica odontológica de referência.', + owner_user_id: superAdmin?.id, + }, + { + company_name: 'Escola de Música Som do Coração', + slug: 'somdocoracao', + cnpj: null, + phone: '(67) 9999-1112', + address_street: 'Rua da Harmonia', address_number: '200', + address_neighborhood: 'Centro', address_city: 'Campo Grande', + address_state: 'MS', address_zip: '79000-200', + type: 'Curso', monthly_goal: 20, status: 'active', + gradient_from: '#A855F7', gradient_to: '#6366F1', + icon: '🎵', description: 'Aulas de música para todas as idades.', + owner_user_id: superAdmin?.id, + }, + { + company_name: 'Ótica Central', + slug: 'oticacentral', + cnpj: null, + phone: '(67) 9999-4444', + address_street: 'Avenida da Visão', address_number: '300', + address_neighborhood: 'Tiradentes', address_city: 'Campo Grande', + address_state: 'MS', address_zip: '79000-300', + type: 'Ótica', monthly_goal: 40, status: 'active', + gradient_from: '#14B8A6', gradient_to: '#22C55E', + icon: '👓', description: 'Soluções para saúde visual.', + owner_user_id: superAdmin?.id, + }, + ]; + for (const p of partners) { + const exists = await knex('partners').where('slug', p.slug).first(); + if (!exists) await knex('partners').insert(p); + } + + // ── Benefits ── + const consultt = await knex('partners').where('slug', 'consulttclinic').first(); + const som = await knex('partners').where('slug', 'somdocoracao').first(); + const otica = await knex('partners').where('slug', 'oticacentral').first(); + const catSaude = await knex('categories').where('slug', 'saude').first(); + const catEducacao = await knex('categories').where('slug', 'educacao').first(); + const catOdonto = await knex('categories').where('slug', 'odontologia').first(); + + const benefits = [ + { + partner_id: consultt?.id, + category_id: catOdonto?.id, + title: 'Benefício Saúde Bucal', + description: 'Cuidado odontológico completo com condições especiais para membros.', + rules: 'Benefício válido para membros ativos. Descontos não acumulativos.', + contact: '(67) 9999-0000', + type: 'dental', delivery_type: 'external', + active: true, is_global: true, approved_by_admin: true, + global_request_status: 'approved', priority: 1, + discount_percent: 30, valid_until: '2026-12-31', + redemption_type: 'qrcode', + }, + { + partner_id: som?.id, + category_id: catEducacao?.id, + title: 'Aula de Violão Gratuita', + description: 'Experimente uma aula de violão com nossos melhores professores.', + rules: 'Válido para novos alunos.', + contact: '(67) 9999-1112', + type: 'education', delivery_type: 'local', + active: true, is_global: false, approved_by_admin: true, + global_request_status: 'none', priority: 3, + discount_percent: 100, valid_until: '2026-06-30', + redemption_type: 'coupon', + }, + { + partner_id: otica?.id, + category_id: catSaude?.id, + title: '25% OFF em Armações', + description: 'Escolha qualquer armação da loja com desconto exclusivo.', + rules: 'Válido para pagamentos à vista.', + contact: '(67) 9999-4444', + type: 'health', delivery_type: 'external', + active: true, is_global: false, approved_by_admin: true, + global_request_status: 'none', priority: 3, + discount_percent: 25, valid_until: '2026-12-31', + redemption_type: 'coupon', + }, + ]; + for (const b of benefits) { + if (b.partner_id) { + const exists = await knex('benefits').where('title', b.title).first(); + if (!exists) await knex('benefits').insert(b); + } + } + + console.log('✅ Seed completed: super_admin + categories + partners + benefits'); +} diff --git a/backend/src/services/UserLifecycleService.ts b/backend/src/services/UserLifecycleService.ts deleted file mode 100644 index bbba6d4..0000000 --- a/backend/src/services/UserLifecycleService.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { db } from '../config/database'; -import { v4 as uuidv4 } from 'uuid'; -import { auditLog } from '../utils/audit'; - -export class UserLifecycleService { - - /** - * Soft Deletes a user (Anonymization) - * This process is irreversible and designed to comply with LGPD/GDPR - */ - static async anonymizeUser(userId: number, adminId: number): Promise { - return await db.transaction(async (trx) => { - // 1. Get User - const user = await trx('users').where({ id: userId }).first(); - if (!user) throw new Error('Usuário não encontrado'); - - if (user.role === 'partner' || user.role === 'partner_admin') { - const partner = await trx('partners').where({ owner_user_id: user.id }).first(); - if (partner && partner.status === 'active') { - throw new Error('Não é possível excluir um parceiro ativo. Desative o parceiro primeiro.'); - } - } - - // 2. Anonymize User Data - const anonEmail = `deleted_${uuidv4()}@clube67.com`; - const randomHash = await import('bcryptjs').then(b => b.hash(uuidv4(), 10)); - - // Backup Leads info before scrubbing user (optional, if we want to keep name in leads table static) - // But actually we want to anonymize leads too to remove PII - - // 3. Update Leads (Keep stats, remove PII) - await trx('leads') - .where({ user_id: userId }) - .update({ - name: `Usuário Excluído ${userId}`, - email: anonEmail, - phone: null, - updated_at: new Date() - }); - - // 4. Delete Favorites - await trx('favorites').where({ user_id: userId }).delete(); - - // 5. Update User Record - await trx('users') - .where({ id: userId }) - .update({ - name: `Usuário Excluído ${userId}`, - email: anonEmail, - password_hash: randomHash, - whatsapp: null, - // cpf: null, // if exists - status: 'banned', // Prevent login logic - deleted_at: new Date(), - is_anonymized: true, - updated_at: new Date() - }); - - // 6. Log - // Audit log needs to happen AFTER transaction or be part of it, - // but our auditLog util might use a different connection or be simple. - // We'll log it separately. - - return true; - }); - } - - /** - * Ban/Block User - */ - static async banUser(userId: number, reason: string, adminId: number): Promise { - await db('users').where({ id: userId }).update({ - status: 'banned', - updated_at: new Date() - }); - await auditLog(adminId, 'BAN_USER', 'user', userId, `Usuário banido: ${reason}`, '0.0.0.0'); - } -} diff --git a/backend/src/services/plugin.service.ts b/backend/src/services/plugin.service.ts index 0baa035..d9fce8b 100644 --- a/backend/src/services/plugin.service.ts +++ b/backend/src/services/plugin.service.ts @@ -1,13 +1,173 @@ -import { Express } from 'express'; -import { logger } from '../utils/logger'; +import fs from 'fs'; +import path from 'path'; +import { Express, Router } from 'express'; +import db from '../config/database'; +import logger from '../utils/logger'; + +export interface PluginManifest { + name: string; + slug: string; + version: string; + description: string; + author: string; + entryPoint: string; + dependencies?: string[]; + hooks?: string[]; +} + +export interface PluginInstance { + manifest: PluginManifest; + router?: Router; + hooks: Record; + activate: () => Promise; + deactivate: () => Promise; +} + +const loadedPlugins: Map = new Map(); +const hookRegistry: Record = {}; + +const PLUGINS_DIR = path.resolve(__dirname, '../../../plugins'); export const pluginService = { - async loadPlugins(app: Express) { - logger.info('Loading plugins...'); - // Implementation details... + async loadAll(app: Express) { + if (!fs.existsSync(PLUGINS_DIR)) { + fs.mkdirSync(PLUGINS_DIR, { recursive: true }); + return; + } + + const activePlugins = await db('plugins').where('active', true); + for (const pluginRecord of activePlugins) { + try { + await this.load(app, pluginRecord.slug); + } catch (err) { + logger.error(`Failed to load plugin: ${pluginRecord.slug}`, err); + } + } + logger.info(`Loaded ${loadedPlugins.size} plugins`); + }, + + async load(app: Express, slug: string) { + const pluginDir = path.join(PLUGINS_DIR, slug); + const manifestPath = path.join(pluginDir, 'manifest.json'); + + if (!fs.existsSync(manifestPath)) { + throw new Error(`Plugin manifest not found: ${manifestPath}`); + } + + const manifest: PluginManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); + const entryPath = path.join(pluginDir, manifest.entryPoint || 'index.js'); + + if (!fs.existsSync(entryPath)) { + throw new Error(`Plugin entry point not found: ${entryPath}`); + } + + const pluginModule = require(entryPath); + const instance: PluginInstance = { + manifest, + router: pluginModule.router, + hooks: pluginModule.hooks || {}, + activate: pluginModule.activate || (async () => { }), + deactivate: pluginModule.deactivate || (async () => { }), + }; + + // Register routes + if (instance.router) { + app.use(`/api/plugins/${slug}`, instance.router); + } + + // Register hooks + for (const [hookName, handlers] of Object.entries(instance.hooks)) { + if (!hookRegistry[hookName]) hookRegistry[hookName] = []; + if (Array.isArray(handlers)) { + hookRegistry[hookName].push(...handlers); + } + } + + await instance.activate(); + loadedPlugins.set(slug, instance); + logger.info(`Plugin loaded: ${manifest.name} v${manifest.version}`); + }, + + async unload(slug: string) { + const instance = loadedPlugins.get(slug); + if (instance) { + await instance.deactivate(); + loadedPlugins.delete(slug); + logger.info(`Plugin unloaded: ${slug}`); + } + }, + + async install(slug: string) { + const pluginDir = path.join(PLUGINS_DIR, slug); + const manifestPath = path.join(pluginDir, 'manifest.json'); + if (!fs.existsSync(manifestPath)) { + throw new Error('Manifest not found'); + } + const manifest: PluginManifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); + + const existing = await db('plugins').where('slug', slug).first(); + if (existing) { + await db('plugins').where('slug', slug).update({ + version: manifest.version, + description: manifest.description, + author: manifest.author, + entry_point: manifest.entryPoint, + dependencies: JSON.stringify(manifest.dependencies || []), + hooks: JSON.stringify(manifest.hooks || []), + }); + } else { + await db('plugins').insert({ + name: manifest.name, + slug: manifest.slug, + version: manifest.version, + description: manifest.description, + author: manifest.author, + entry_point: manifest.entryPoint, + active: false, + dependencies: JSON.stringify(manifest.dependencies || []), + hooks: JSON.stringify(manifest.hooks || []), + directory: pluginDir, + }); + } + return manifest; + }, + + async activate(app: Express, slug: string) { + await db('plugins').where('slug', slug).update({ active: true }); + await this.load(app, slug); + }, + + async deactivate(slug: string) { + await db('plugins').where('slug', slug).update({ active: false }); + await this.unload(slug); + }, + + async getAll() { + return db('plugins').orderBy('name'); + }, + + async remove(slug: string) { + await this.unload(slug); + await db('plugins').where('slug', slug).delete(); + // Note: does not delete plugin files for safety + }, + + async triggerHook(hookName: string, data: any) { + const handlers = hookRegistry[hookName] || []; + for (const handler of handlers) { + try { + await handler(data); + } catch (err) { + logger.error(`Hook error [${hookName}]:`, err); + } + } + }, + + getLoadedPlugins() { + return Array.from(loadedPlugins.entries()).map(([slug, instance]) => ({ + slug, + name: instance.manifest.name, + version: instance.manifest.version, + })); }, - async installPlugin(zipPath: string) { }, - async activatePlugin(slug: string) { }, - async deactivatePlugin(slug: string) { }, - async removePlugin(slug: string) { }, }; diff --git a/backend/src/types/express.d.ts b/backend/src/types/express.d.ts deleted file mode 100644 index f65a014..0000000 --- a/backend/src/types/express.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Express Request augmentation for user property -// Added by auth middleware after JWT verification - -declare namespace Express { - interface Request { - user?: { - id: string; - email: string; - role: string; - partner_id?: string; - name?: string; - }; - partnerConsent?: boolean; - partnerId?: number; - } -} diff --git a/backend/src/utils/audit.ts b/backend/src/utils/audit.ts index 6229314..8b4138a 100644 --- a/backend/src/utils/audit.ts +++ b/backend/src/utils/audit.ts @@ -1,16 +1,28 @@ -import { db } from '../config/database'; +import db from '../config/database'; +import { Request } from 'express'; -export async function auditLog(userId: number, action: string, entityType: string, entityId?: number, description?: string, ip?: string) { +interface AuditEntry { + user_id?: number; + action: string; + entity_type?: string; + entity_id?: number; + description?: string; + old_data?: any; + new_data?: any; + ip_address?: string; + user_agent?: string; +} + +export async function auditLog(req: Request | null, entry: Omit) { try { await db('logs').insert({ - user_id: userId, - action, - entity_type: entityType, - entity_id: entityId, - description, - ip_address: ip + ...entry, + old_data: entry.old_data ? JSON.stringify(entry.old_data) : null, + new_data: entry.new_data ? JSON.stringify(entry.new_data) : null, + ip_address: req?.ip || null, + user_agent: req?.get('user-agent')?.substring(0, 500) || null, }); } catch (err) { - console.error('Failed to save audit log:', err); + console.error('Audit log error:', err); } } diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts index b2e5270..cf4cdb7 100644 --- a/backend/src/utils/logger.ts +++ b/backend/src/utils/logger.ts @@ -1,32 +1,39 @@ import winston from 'winston'; -import 'winston-daily-rotate-file'; import path from 'path'; import { config } from '../config'; -const logFormat = winston.format.combine( - winston.format.timestamp(), - winston.format.json() -); +const logDir = config.log.dir; -export const logger = winston.createLogger({ - format: logFormat, +const logger = winston.createLogger({ + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + format: winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.errors({ stack: true }), + winston.format.json() + ), + defaultMeta: { service: 'clube67-api' }, transports: [ - new winston.transports.Console({ - format: winston.format.combine( - winston.format.colorize(), - winston.format.simple() - ), - }), - new (winston.transports as any).DailyRotateFile({ - dirname: config.logging.dir, - filename: 'error-%DATE%.log', + new winston.transports.File({ + filename: path.join(logDir, 'error.log'), level: 'error', - maxFiles: '14d', + maxsize: 5242880, + maxFiles: 5, }), - new (winston.transports as any).DailyRotateFile({ - dirname: config.logging.dir, - filename: 'combined-%DATE%.log', - maxFiles: '14d', + new winston.transports.File({ + filename: path.join(logDir, 'combined.log'), + maxsize: 5242880, + maxFiles: 10, }), ], }); + +if (process.env.NODE_ENV !== 'production') { + logger.add(new winston.transports.Console({ + format: winston.format.combine( + winston.format.colorize(), + winston.format.simple() + ), + })); +} + +export default logger; diff --git a/backend/storage/plugin-configs.json b/backend/storage/plugin-configs.json deleted file mode 100644 index 9e26dfe..0000000 --- a/backend/storage/plugin-configs.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/backend/test_pg_conn.js b/backend/test_pg_conn.js deleted file mode 100644 index ccdbe91..0000000 --- a/backend/test_pg_conn.js +++ /dev/null @@ -1,28 +0,0 @@ -const { Client } = require('pg'); -const dotenv = require('dotenv'); -const path = require('path'); - -dotenv.config({ path: path.join(__dirname, '.env') }); - -const client = new Client({ - host: process.env.DB_HOST, - port: parseInt(process.env.DB_PORT || '5432'), - user: process.env.DB_USER, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, -}); - -async function main() { - try { - console.log('Connecting to:', process.env.DB_HOST, 'as user:', process.env.DB_USER, 'to database:', process.env.DB_NAME); - await client.connect(); - console.log('Connected!'); - const res = await client.query('SELECT NOW()'); - console.log('Result:', res.rows[0]); - await client.end(); - } catch (err) { - console.error('Connection failed:', err); - } -} - -main(); diff --git a/backend/tsconfig.json b/backend/tsconfig.json index b3804e1..2af0810 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -2,20 +2,30 @@ "compilerOptions": { "target": "ES2020", "module": "commonjs", + "lib": [ + "ES2020" + ], "outDir": "./dist", - "rootDir": "..", + "rootDir": "./src", "strict": false, "esModuleInterop": true, "skipLibCheck": true, + "noImplicitAny": false, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "declaration": true, - "declarationDir": "./dist/types", - "sourceMap": true + "declarationMap": true, + "sourceMap": true, + "moduleResolution": "node", + "baseUrl": "./src", + "paths": { + "@/*": [ + "./*" + ] + } }, "include": [ - "src/**/*", - "../plugins/**/*" + "src/**/*" ], "exclude": [ "node_modules", diff --git a/deploy-prod-builder.sh b/deploy-prod-builder.sh index d138083..9bbad85 100755 --- a/deploy-prod-builder.sh +++ b/deploy-prod-builder.sh @@ -1,55 +1,44 @@ #!/bin/bash -# ─── deploy-prod-builder.sh ────────────────────────────────────────────────── -# Script sênior de automação de build e despacho (GitOps) de VPS 4 -> VPS 1. -# ───────────────────────────────────────────────────────────────────────────── set -e -LOCK_FILE="/tmp/deploy-prod-clube67.lock" -if [ -f "$LOCK_FILE" ]; then - echo "🚨 Deploy já está em andamento! Aguarde..." - exit 1 -fi -touch "$LOCK_FILE" +# Configurações do Deploy +PROD_VPS="10.99.0.1" +PROD_USER="deploy" +PROD_PATH="/home/deploy/stack/clube67" -# Garante que o lock file será apagado no final do script mesmo se houver erro -trap 'rm -f "$LOCK_FILE"' EXIT +echo "==========================================================" +echo "🚀 INICIANDO DEPLOY DO CLUBE67 ORIGINAL (PRODUÇÃO)" +echo "==========================================================" -LOG_FILE="/home/deploy/stack/webhook-listener/deploy.log" +# 1. Sincronizar arquivos locais com a VPS 1 de produção +echo "📦 Enviando arquivos para a VPS 1 (${PROD_VPS})..." +rsync -avz --delete \ + --exclude='.git' \ + --exclude='node_modules' \ + --exclude='frontend/node_modules' \ + --exclude='backend/node_modules' \ + --exclude='frontend/.next' \ + --exclude='backend/dist' \ + --exclude='logs' \ + ./ ${PROD_USER}@${PROD_VPS}:${PROD_PATH}/ -log() { - echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $1" | tee -a "$LOG_FILE" -} - -log "🚀 Iniciando ciclo de build e deploy automático para Clube67..." - -# 1. Atualizar repositório Git local na VPS 4 -log "📦 Sincronizando repositório Git local na VPS 4..." -cd /home/deploy/stack/clube67 -git fetch origin main -git reset --hard origin/main - -# 2. Compilar o Backend -log "⚙️ Instalando dependências e compilando o Backend..." -cd /home/deploy/stack/clube67/backend -npm install -npm run build - -# 3. Sincronizar arquivos compilados e arquivos de configuração com a VPS 1 (Produção) via Rsync VPN -log "🚚 Despachando Backend compilado e dependências para a VPS 1 (Produção)..." -rsync -avz --delete --exclude '.env' /home/deploy/stack/clube67/backend/dist/ deploy@10.99.0.1:/home/deploy/stack/clube67/backend/dist/ -rsync -avz --delete /home/deploy/stack/clube67/backend/node_modules/ deploy@10.99.0.1:/home/deploy/stack/clube67/backend/node_modules/ -rsync -avz /home/deploy/stack/clube67/backend/package.json /home/deploy/stack/clube67/backend/package-lock.json deploy@10.99.0.1:/home/deploy/stack/clube67/backend/ - -log "🚚 Despachando Frontend estático para a VPS 1 (Produção)..." -rsync -avz --delete /home/deploy/stack/clube67/frontend/ deploy@10.99.0.1:/home/deploy/stack/clube67/frontend/ - -log "🚚 Despachando Nginx e Docker Compose de produção para a VPS 1..." -rsync -avz /home/deploy/stack/clube67/nginx/default.conf deploy@10.99.0.1:/home/deploy/stack/clube67/nginx/default.conf -rsync -avz /home/deploy/stack/clube67/docker-compose.yml deploy@10.99.0.1:/home/deploy/stack/clube67/docker-compose.yml - -# 4. Parar containers antigos obsoletos e iniciar os novos na VPS 1 -log "🔄 Reiniciando containers e aplicando novas definições na VPS 1..." -ssh -o BatchMode=yes deploy@10.99.0.1 "cd /home/deploy/stack/clube67 && DOCKER_HOST=unix:///run/user/1000/docker.sock docker compose down && DOCKER_HOST=unix:///run/user/1000/docker.sock docker compose up -d" - -log "✅ Deploy do Clube67 concluído com absoluto sucesso!" +# 2. Reiniciar e compilar contêineres em produção +echo "🐳 Reiniciando contêineres Docker na VPS 1..." +ssh -o BatchMode=yes ${PROD_USER}@${PROD_VPS} " + cd ${PROD_PATH} + # Garantir redes externas + docker network inspect web >/dev/null 2>&1 || docker network create web + docker network inspect soc >/dev/null 2>&1 || docker network create soc + + echo '🐳 Derrubando contêineres antigos...' + DOCKER_HOST=unix:///run/user/1000/docker.sock docker compose down || true + + echo '🐳 Subindo e compilando novos contêineres...' + DOCKER_HOST=unix:///run/user/1000/docker.sock docker compose up -d --build + + echo '✅ Contêineres de produção iniciados com sucesso!' +" +echo "==========================================================" +echo "🎉 DEPLOY CONCLUÍDO COM SUCESSO!" +echo "==========================================================" diff --git a/docker-compose.yml b/docker-compose.yml index 0955c3e..24af154 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,9 +1,64 @@ services: + clube67-db: + image: mysql:8.0 + container_name: clube67-db-prod + restart: always + environment: + MYSQL_DATABASE: sis_cb67 + MYSQL_USER: sis_cb67 + MYSQL_PASSWORD: FsBtLbEStZ3AfBHC + MYSQL_ROOT_PASSWORD: root_cb67_secret_pass_2026 + volumes: + - clube67-db-data:/var/lib/mysql + networks: + - soc + - web + + clube67-backend: + image: node:20-alpine + container_name: clube67-backend-prod + restart: always + working_dir: /app/backend + env_file: + - ".env" + volumes: + - ".:/app" + - "./uploads:/www/wwwroot/clube67.com/uploads" + - "./logs:/www/wwwroot/clube67.com/logs" + command: sh -c "npm install --include=dev && npm run build && npm run migrate && npm run seed && npm run start" + depends_on: + - clube67-db + security_opt: + - no-new-privileges:true + networks: + - soc + - web + + clube67-frontend: + image: node:20-alpine + container_name: clube67-frontend-prod + restart: always + working_dir: /app/frontend + env_file: + - ".env" + volumes: + - ".:/app" + command: sh -c "npm install --include=dev && npm run build && npm run start" + depends_on: + - clube67-backend + networks: + - soc + - web + nginx: image: nginx:alpine + container_name: clube67-nginx-prod restart: always + ports: + - "8085:80" volumes: - "./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro" + - "./uploads:/www/wwwroot/clube67.com/uploads:ro" labels: - "traefik.enable=true" # Roteador de Produção do Clube67 (Apenas clube67.com e www.clube67.com) @@ -16,35 +71,14 @@ services: - soc - web depends_on: + - clube67-frontend - clube67-backend - clube67-backend: - image: node:20 - container_name: clube67-backend-prod - restart: always - working_dir: /app - env_file: - - "./backend/.env" - environment: - - PORT=3001 - - NODE_ENV=production - volumes: - # Montar diretórios e arquivos do Clube67 reais - - "./backend/uploads:/app/uploads" - - "./backend/dist:/app/dist" - - "./backend/package.json:/app/package.json" - - "./backend/node_modules:/app/node_modules" - - "./frontend:/frontend" - command: node dist/backend/src/index.js - security_opt: - - no-new-privileges:true - networks: - - soc - - web +volumes: + clube67-db-data: networks: soc: external: true web: external: true - diff --git a/ecosystem.config.js b/ecosystem.config.js new file mode 100644 index 0000000..96276de --- /dev/null +++ b/ecosystem.config.js @@ -0,0 +1,29 @@ +module.exports = { + apps: [ + { + name: 'clube67-api', + script: './backend/dist/index.js', + cwd: '/www/wwwroot/clube67.com', + instances: 'max', + exec_mode: 'cluster', + env: { + NODE_ENV: 'production', + PORT: 3001, + }, + env_development: { + NODE_ENV: 'development', + PORT: 3001, + }, + max_memory_restart: '500M', + log_file: '/www/wwwroot/clube67.com/logs/pm2-combined.log', + error_file: '/www/wwwroot/clube67.com/logs/pm2-error.log', + out_file: '/www/wwwroot/clube67.com/logs/pm2-out.log', + log_date_format: 'YYYY-MM-DD HH:mm:ss Z', + merge_logs: true, + watch: false, + autorestart: true, + restart_delay: 1000, + max_restarts: 10, + }, + ], +}; diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx new file mode 100644 index 0000000..2e5ef20 --- /dev/null +++ b/frontend/app/admin/page.tsx @@ -0,0 +1,196 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import DashboardLayout from '@/components/layout/DashboardLayout'; +import { api } from '@/lib/api'; + +export default function AdminDashboard() { + const [dashboard, setDashboard] = useState(null); + const [users, setUsers] = useState([]); + const [partners, setPartners] = useState([]); + const [logs, setLogs] = useState([]); + const [tab, setTab] = useState('dashboard'); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const t = params.get('tab'); + if (t) setTab(t); + }, []); + + useEffect(() => { + (async () => { + setLoading(true); + try { + if (tab === 'dashboard') { + const d = await api.getDashboard(); + setDashboard(d); + } else if (tab === 'users') { + const d = await api.getUsers(); + setUsers(d.users || []); + } else if (tab === 'partners') { + const d = await api.getPartners(); + setPartners(d.partners || []); + } else if (tab === 'logs') { + const d = await api.getAuditLogs(); + setLogs(d.logs || []); + } + } catch (e) { console.error(e); } + setLoading(false); + })(); + }, [tab]); + + const statCards = dashboard ? [ + { icon: '👥', label: 'Total Usuários', value: dashboard.totalUsers || 0, color: 'from-blue-500 to-cyan-400' }, + { icon: '🏢', label: 'Parceiros', value: dashboard.totalPartners || 0, color: 'from-green-500 to-emerald-400' }, + { icon: '🎁', label: 'Benefícios', value: dashboard.totalBenefits || 0, color: 'from-purple-500 to-pink-400' }, + { icon: '📊', label: 'Transações', value: dashboard.totalTransactions || 0, color: 'from-orange-500 to-red-400' }, + ] : []; + + return ( + +
        +
        +
        +

        Painel Administrativo

        +

        Gerencie toda a plataforma em um só lugar.

        +
        +
        + + {/* Tabs */} +
        + {[ + { key: 'dashboard', label: '📊 Dashboard' }, + { key: 'users', label: '👥 Usuários' }, + { key: 'partners', label: '🏢 Parceiros' }, + { key: 'benefits', label: '🎁 Benefícios' }, + { key: 'plugins', label: '🔌 Plugins' }, + { key: 'logs', label: '📋 Logs' }, + ].map(t => ( + + ))} +
        + + {/* Dashboard Tab */} + {tab === 'dashboard' && ( +
        +
        + {loading ? [1, 2, 3, 4].map(i =>
        ) : + statCards.map((card, i) => ( + +
        +
        {card.icon}
        + {card.label} +
        +
        {card.value}
        +
        + ))} +
        +
        + )} + + {/* Users Tab */} + {tab === 'users' && ( +
        +
        + + + + + + + + + + + + {loading ? : + users.map(u => ( + + + + + + + + ))} + +
        NomeE-mailFunçãoStatusData
        Carregando...
        +
        +
        + {u.name?.charAt(0)} +
        + {u.name} +
        +
        {u.email} + {u.role} + + {u.status} + {new Date(u.created_at).toLocaleDateString('pt-BR')}
        +
        +
        + )} + + {/* Partners Tab */} + {tab === 'partners' && ( +
        + {loading ? [1, 2, 3].map(i =>
        ) : + partners.map(p => ( + +
        +
        + {p.icon || '🏢'} +
        +
        +

        {p.company_name}

        +

        {p.type} • {p.slug}

        +
        +
        +
        + {p.status === 'active' ? 'Ativo' : 'Inativo'} + {p.subscription_plan || 'basic'} +
        +
        + ))} +
        + )} + + {/* Logs Tab */} + {tab === 'logs' && ( +
        +

        Logs de Auditoria

        +
        + {loading ? [1, 2, 3, 4, 5].map(i =>
        ) : + logs.length === 0 ? ( +

        Nenhum log registrado.

        + ) : logs.map((l: any) => ( +
        +
        📋
        +
        +

        {l.action}: {l.description || l.entity_type}

        +

        {new Date(l.created_at).toLocaleString('pt-BR')}

        +
        + {l.ip_address} +
        + ))} +
        +
        + )} +
        + + ); +} diff --git a/frontend/app/benefits/page.tsx b/frontend/app/benefits/page.tsx new file mode 100644 index 0000000..4a12d45 --- /dev/null +++ b/frontend/app/benefits/page.tsx @@ -0,0 +1,138 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import DashboardLayout from '@/components/layout/DashboardLayout'; +import { api } from '@/lib/api'; +import { useAuth } from '@/lib/auth'; + +export default function BenefitsPage() { + const { user } = useAuth(); + const [benefits, setBenefits] = useState([]); + const [categories, setCategories] = useState([]); + const [search, setSearch] = useState(''); + const [selectedCategory, setSelectedCategory] = useState(''); + const [loading, setLoading] = useState(true); + + useEffect(() => { + api.getCategories().then(d => setCategories(d.categories || [])).catch(console.error); + loadBenefits(); + }, []); + + const loadBenefits = async (catId?: string) => { + setLoading(true); + try { + const params = new URLSearchParams(); + if (catId) params.set('category', catId); + if (search) params.set('search', search); + const d = await api.getBenefits(params.toString()); + setBenefits(d.benefits || []); + } catch (e) { console.error(e); } + setLoading(false); + }; + + const handleFilter = (catId: string) => { + setSelectedCategory(catId); + loadBenefits(catId); + }; + + const handleRedeem = async (id: number) => { + try { + await api.redeemBenefit(id); + alert('Benefício resgatado com sucesso! Verifique seu e-mail.'); + } catch (err: any) { + alert(err.message || 'Erro ao resgatar'); + } + }; + + const handleFavorite = async (id: number) => { + try { + await api.addFavorite(id); + alert('Adicionado aos favoritos!'); + } catch { } + }; + + return ( + +
        +
        +

        🎁 Benefícios

        +

        Explore e resgate benefícios exclusivos dos nossos parceiros.

        +
        + + {/* Search + Filter */} +
        +
        + setSearch(e.target.value)} + onKeyDown={e => e.key === 'Enter' && loadBenefits(selectedCategory)} + placeholder="Buscar benefícios..." + className="w-full px-4 py-3 bg-dark-900 border border-dark-700 rounded-xl text-white focus:border-brand-500 outline-none" /> +
        +
        + + {categories.map(c => ( + + ))} +
        +
        + + {/* Benefits Grid */} +
        + {loading ? [1, 2, 3, 4, 5, 6].map(i =>
        ) : + benefits.length === 0 ? ( +
        +
        🔍
        +

        Nenhum benefício encontrado

        +

        Tente ajustar os filtros de busca.

        +
        + ) : + benefits.map((b, i) => ( + + {/* Gradient Header */} +
        +
        +
        +
        +

        {b.title}

        +

        {b.partner_name || 'Parceiro'}

        +
        + {b.discount_percent && ( +
        + -{b.discount_percent}% +
        + )} +
        +

        {b.description}

        + +
        +
        + {b.badge && {b.badge}} + {b.is_global && 🌐 Global} +
        +
        + + +
        +
        +
        + + ))} +
        +
        + + ); +} diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx new file mode 100644 index 0000000..73f9972 --- /dev/null +++ b/frontend/app/dashboard/page.tsx @@ -0,0 +1,127 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import DashboardLayout from '@/components/layout/DashboardLayout'; +import { api } from '@/lib/api'; +import { useAuth } from '@/lib/auth'; + +interface Stats { + totalBenefits?: number; + totalFavorites?: number; + totalRedemptions?: number; + totalSavings?: number; +} + +export default function UserDashboard() { + const { user } = useAuth(); + const [stats, setStats] = useState({}); + const [favorites, setFavorites] = useState([]); + const [history, setHistory] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + Promise.all([ + api.getUserStats().catch(() => ({})), + api.getFavorites().catch(() => ({ favorites: [] })), + api.getHistory().catch(() => ({ transactions: [] })), + ]).then(([s, f, h]) => { + setStats(s); + setFavorites(f.favorites || []); + setHistory(h.transactions || []); + setLoading(false); + }); + }, []); + + const statCards = [ + { icon: '🎁', label: 'Benefícios Disponíveis', value: stats.totalBenefits || 0, color: 'from-blue-500 to-cyan-400' }, + { icon: '⭐', label: 'Favoritos', value: stats.totalFavorites || 0, color: 'from-yellow-500 to-orange-400' }, + { icon: '✅', label: 'Resgates', value: stats.totalRedemptions || 0, color: 'from-green-500 to-emerald-400' }, + { icon: '💰', label: 'Economia Total', value: `R$ ${(stats.totalSavings || 0).toFixed(0)}`, color: 'from-purple-500 to-pink-400' }, + ]; + + return ( + +
        + {/* Header */} +
        +

        + Olá, {user?.name?.split(' ')[0]} 👋 +

        +

        Confira seus benefícios e acompanhe sua economia.

        +
        + + {/* Stat Cards */} +
        + {statCards.map((card, i) => ( + +
        +
        + {card.icon} +
        + {card.label} +
        +
        {loading ?
        : card.value}
        + + ))} +
        + + {/* Content Grid */} +
        + {/* Recent History */} + +

        Resgates Recentes

        + {loading ? ( +
        {[1, 2, 3].map(i =>
        )}
        + ) : history.length === 0 ? ( +

        Nenhum resgate ainda. Explore os benefícios!

        + ) : ( +
        + {history.slice(0, 5).map((t: any) => ( +
        +
        +

        {t.benefit_title || 'Benefício'}

        +

        {new Date(t.created_at).toLocaleDateString('pt-BR')}

        +
        + {t.status === 'completed' ? 'Resgatado' : 'Pendente'} +
        + ))} +
        + )} + + + {/* Favorites */} + +

        ⭐ Favoritos

        + {loading ? ( +
        {[1, 2, 3].map(i =>
        )}
        + ) : favorites.length === 0 ? ( +

        Nenhum favorito salvo ainda.

        + ) : ( +
        + {favorites.slice(0, 5).map((f: any) => ( +
        +
        +

        {f.title}

        +

        {f.partner_name || 'Parceiro'}

        +
        + {f.discount_percent && ( + + -{f.discount_percent}% + + )} +
        + ))} +
        + )} + +
        +
        + + ); +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..36fa384 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,72 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap'); + +:root { + --brand: #2a8bff; + --brand-dark: #0d54e1; +} + +* { margin: 0; padding: 0; box-sizing: border-box; } +html { scroll-behavior: smooth; } + +body { + font-family: 'Inter', system-ui, -apple-system, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Scrollbar */ +::-webkit-scrollbar { width: 6px; height: 6px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; } +::-webkit-scrollbar-thumb:hover { background: #4b5563; } + +/* Dark mode scrollbar */ +.dark ::-webkit-scrollbar-thumb { background: #4b5563; } +.dark ::-webkit-scrollbar-thumb:hover { background: #6b7280; } + +/* Glass effect */ +.glass { + background: rgba(255, 255, 255, 0.08); + backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.12); +} + +.glass-dark { + background: rgba(0, 0, 0, 0.3); + backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +/* Gradient text */ +.gradient-text { + background: linear-gradient(135deg, #2a8bff 0%, #a855f7 50%, #ec4899 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Skeleton loading */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} +.skeleton { + background: linear-gradient(90deg, #1e1f2b 25%, #2a2b3d 50%, #1e1f2b 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 8px; +} + +/* Focus ring */ +.focus-ring:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(42, 139, 255, 0.4); +} + +@layer utilities { + .text-balance { text-wrap: balance; } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..5d6a87f --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,21 @@ +import type { Metadata } from 'next'; +import './globals.css'; +import { AuthProvider } from '@/lib/auth'; + +export const metadata: Metadata = { + title: 'Clube de Benefícios | Clube67', + description: 'Plataforma de benefícios exclusivos para membros. Descontos, vantagens e muito mais com parceiros selecionados.', + keywords: 'benefícios, descontos, clube, parceiros, vantagens', +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + {children} + + + + ); +} diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx new file mode 100644 index 0000000..a55dac2 --- /dev/null +++ b/frontend/app/login/page.tsx @@ -0,0 +1,86 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import { useAuth } from '@/lib/auth'; + +export default function LoginPage() { + const { login } = useAuth(); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + try { + await login(email, password); + } catch (err: any) { + setError(err.message || 'Erro ao fazer login'); + } finally { + setLoading(false); + } + }; + + return ( +
        +
        +
        + + +
        + Clube67 +

        Bem-vindo de volta

        +

        Entre na sua conta para continuar

        +
        + +
        +
        + {error && ( +
        + {error} +
        + )} + +
        + + setEmail(e.target.value)} required + className="w-full px-4 py-3 bg-dark-900 border border-dark-700 rounded-xl text-white focus:border-brand-500 focus:ring-1 focus:ring-brand-500/50 transition-all outline-none" + placeholder="seu@email.com" /> +
        + +
        + + setPassword(e.target.value)} required + className="w-full px-4 py-3 bg-dark-900 border border-dark-700 rounded-xl text-white focus:border-brand-500 focus:ring-1 focus:ring-brand-500/50 transition-all outline-none" + placeholder="••••••••" /> +
        + + +
        + +
        +

        + Não tem conta?{' '} + + Criar conta + +

        +
        +
        +
        +
        + ); +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000..f6822f1 --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,164 @@ +'use client'; + +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import { useAuth } from '@/lib/auth'; + +const features = [ + { icon: '🎯', title: 'Benefícios Exclusivos', desc: 'Descontos e vantagens em parceiros selecionados da sua região.' }, + { icon: '💳', title: 'Cartão Digital', desc: 'Apresente seu QR Code e resgate benefícios de forma rápida e segura.' }, + { icon: '🏢', title: 'Parceiros Premium', desc: 'Rede de empresas verificadas oferecendo os melhores descontos.' }, + { icon: '📊', title: 'Painel Completo', desc: 'Acompanhe seus resgates, economia e benefícios favoritos em tempo real.' }, +]; + +const stats = [ + { value: '500+', label: 'Benefícios Ativos' }, + { value: '150+', label: 'Parceiros' }, + { value: '10k+', label: 'Membros' }, + { value: 'R$ 2M+', label: 'Economia Gerada' }, +]; + +export default function HomePage() { + const { user } = useAuth(); + + return ( +
        + {/* Header */} +
        +
        + + Clube67 + + +
        + {user ? ( + + Painel + + ) : ( + <> + + Entrar + + + Criar Conta + + + )} +
        +
        +
        + + {/* Hero */} +
        + {/* Background orbs */} +
        +
        +
        + +
        + +
        + + Plataforma ativa — novos parceiros toda semana +
        +

        + Seus benefícios em +
        + um só lugar +

        +

        + Conectamos você a descontos exclusivos em saúde, educação, gastronomia e muito mais. + Tudo em uma plataforma moderna e segura. +

        +
        + + Começar Agora → + + + Como Funciona + +
        +
        +
        +
        + + {/* Stats */} +
        +
        + {stats.map((s, i) => ( + +
        {s.value}
        +
        {s.label}
        +
        + ))} +
        +
        + + {/* Features */} +
        +
        +
        +

        Tudo que você precisa

        +

        Uma plataforma completa de benefícios para você e sua família.

        +
        +
        + {features.map((f, i) => ( + +
        {f.icon}
        +

        {f.title}

        +

        {f.desc}

        +
        + ))} +
        +
        +
        + + {/* CTA */} +
        +
        + +
        +

        + Pronto para economizar? +

        +

        + Junte-se a milhares de membros que já estão aproveitando benefícios exclusivos. +

        + + Criar Conta Grátis + + +
        +
        + + {/* Footer */} + +
        + ); +} diff --git a/frontend/app/partner/page.tsx b/frontend/app/partner/page.tsx new file mode 100644 index 0000000..07d495c --- /dev/null +++ b/frontend/app/partner/page.tsx @@ -0,0 +1,77 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import DashboardLayout from '@/components/layout/DashboardLayout'; +import { api } from '@/lib/api'; +import { useAuth } from '@/lib/auth'; + +export default function PartnerDashboard() { + const { user } = useAuth(); + const [stats, setStats] = useState(null); + const [tab, setTab] = useState('dashboard'); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const params = new URLSearchParams(window.location.search); + const t = params.get('tab'); + if (t) setTab(t); + }, []); + + useEffect(() => { + api.getPartnerStats(1).then(d => { setStats(d); setLoading(false); }).catch(() => setLoading(false)); + }, []); + + const statCards = stats ? [ + { icon: '🎁', label: 'Benefícios Ativos', value: stats.activeBenefits || 0, color: 'from-blue-500 to-cyan-400' }, + { icon: '👥', label: 'Membros', value: stats.totalMembers || 0, color: 'from-green-500 to-emerald-400' }, + { icon: '✅', label: 'Resgates este Mês', value: stats.monthlyRedemptions || 0, color: 'from-purple-500 to-pink-400' }, + { icon: '📈', label: 'Meta Mensal', value: `${stats.goalProgress || 0}%`, color: 'from-orange-500 to-red-400' }, + ] : []; + + return ( + +
        +
        +

        Painel do Parceiro

        +

        Gerencie seus benefícios e acompanhe o desempenho.

        +
        + + {/* Stat Cards */} +
        + {loading ? [1, 2, 3, 4].map(i =>
        ) : + statCards.map((card, i) => ( + +
        +
        {card.icon}
        + {card.label} +
        +
        {card.value}
        +
        + ))} +
        + + {/* Quick Actions */} +
        +

        Ações Rápidas

        +
        + {[ + { icon: '➕', label: 'Novo Benefício', action: () => { } }, + { icon: '📊', label: 'Relatórios', action: () => { } }, + { icon: '👥', label: 'Ver Membros', action: () => { } }, + { icon: '⚙️', label: 'Configurações', action: () => { } }, + ].map((a, i) => ( + + ))} +
        +
        +
        + + ); +} diff --git a/frontend/app/profile/page.tsx b/frontend/app/profile/page.tsx new file mode 100644 index 0000000..035705a --- /dev/null +++ b/frontend/app/profile/page.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import DashboardLayout from '@/components/layout/DashboardLayout'; +import { api } from '@/lib/api'; +import { useAuth } from '@/lib/auth'; + +export default function ProfilePage() { + const { user } = useAuth(); + const [form, setForm] = useState({ + name: user?.name || '', + phone: user?.phone || '', + }); + const [saving, setSaving] = useState(false); + const [msg, setMsg] = useState(''); + + const handleSave = async () => { + setSaving(true); + setMsg(''); + try { + if (user) { + await api.updateUser(user.id, form); + setMsg('Perfil atualizado com sucesso!'); + } + } catch (err: any) { + setMsg(err.message || 'Erro ao salvar'); + } + setSaving(false); + }; + + return ( + +
        +
        +

        👤 Meu Perfil

        +

        Gerencie suas informações pessoais.

        +
        + + + {/* Avatar */} +
        +
        + {user?.name?.charAt(0) || '?'} +
        +
        +

        {user?.name}

        +

        {user?.email}

        + {user?.role} +
        +
        + + {msg && ( +
        {msg}
        + )} + +
        +
        + + setForm(p => ({ ...p, name: e.target.value }))} + className="w-full px-4 py-3 bg-dark-900 border border-dark-700 rounded-xl text-white focus:border-brand-500 outline-none" /> +
        +
        + + +
        +
        + + setForm(p => ({ ...p, phone: e.target.value }))} + className="w-full px-4 py-3 bg-dark-900 border border-dark-700 rounded-xl text-white focus:border-brand-500 outline-none" + placeholder="(67) 99999-0000" /> +
        + +
        +
        +
        +
        + ); +} diff --git a/frontend/app/register/page.tsx b/frontend/app/register/page.tsx new file mode 100644 index 0000000..e6763d5 --- /dev/null +++ b/frontend/app/register/page.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { motion } from 'framer-motion'; +import { useAuth } from '@/lib/auth'; + +export default function RegisterPage() { + const { register } = useAuth(); + const [form, setForm] = useState({ name: '', email: '', password: '', phone: '' }); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + try { + await register(form); + } catch (err: any) { + setError(err.message || 'Erro ao criar conta'); + } finally { + setLoading(false); + } + }; + + const update = (k: string, v: string) => setForm(p => ({ ...p, [k]: v })); + + return ( +
        +
        +
        + + +
        + Clube67 +

        Criar Conta

        +

        Junte-se ao clube e aproveite benefícios exclusivos

        +
        + +
        +
        + {error && ( +
        {error}
        + )} + +
        + + update('name', e.target.value)} required + className="w-full px-4 py-3 bg-dark-900 border border-dark-700 rounded-xl text-white focus:border-brand-500 focus:ring-1 focus:ring-brand-500/50 transition-all outline-none" + placeholder="Seu nome" /> +
        + +
        + + update('email', e.target.value)} required + className="w-full px-4 py-3 bg-dark-900 border border-dark-700 rounded-xl text-white focus:border-brand-500 focus:ring-1 focus:ring-brand-500/50 transition-all outline-none" + placeholder="seu@email.com" /> +
        + +
        + + update('phone', e.target.value)} + className="w-full px-4 py-3 bg-dark-900 border border-dark-700 rounded-xl text-white focus:border-brand-500 focus:ring-1 focus:ring-brand-500/50 transition-all outline-none" + placeholder="(67) 99999-0000" /> +
        + +
        + + update('password', e.target.value)} required minLength={6} + className="w-full px-4 py-3 bg-dark-900 border border-dark-700 rounded-xl text-white focus:border-brand-500 focus:ring-1 focus:ring-brand-500/50 transition-all outline-none" + placeholder="Mínimo 6 caracteres" /> +
        + + +
        + +
        +

        + Já tem uma conta?{' '} + Entrar +

        +
        +
        +
        +
        + ); +} diff --git a/frontend/components/layout/DashboardLayout.tsx b/frontend/components/layout/DashboardLayout.tsx new file mode 100644 index 0000000..627ef90 --- /dev/null +++ b/frontend/components/layout/DashboardLayout.tsx @@ -0,0 +1,37 @@ +'use client'; + +import { useAuth } from '@/lib/auth'; +import Sidebar from '@/components/layout/Sidebar'; +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; + +export default function DashboardLayout({ children }: { children: React.ReactNode }) { + const { user, loading } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!loading && !user) router.push('/login'); + }, [loading, user, router]); + + if (loading) { + return ( +
        +
        +
        +

        Carregando...

        +
        +
        + ); + } + + if (!user) return null; + + return ( +
        + +
        + {children} +
        +
        + ); +} diff --git a/frontend/components/layout/Sidebar.tsx b/frontend/components/layout/Sidebar.tsx new file mode 100644 index 0000000..a5bd2be --- /dev/null +++ b/frontend/components/layout/Sidebar.tsx @@ -0,0 +1,89 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { useAuth } from '@/lib/auth'; +import { motion, AnimatePresence } from 'framer-motion'; +import { useState } from 'react'; + +const userMenu = [ + { icon: '📊', label: 'Painel', href: '/dashboard' }, + { icon: '🎁', label: 'Benefícios', href: '/benefits' }, + { icon: '⭐', label: 'Favoritos', href: '/dashboard?tab=favorites' }, + { icon: '📜', label: 'Histórico', href: '/dashboard?tab=history' }, + { icon: '👤', label: 'Perfil', href: '/profile' }, +]; + +const adminMenu = [ + { icon: '📊', label: 'Dashboard', href: '/admin' }, + { icon: '👥', label: 'Usuários', href: '/admin?tab=users' }, + { icon: '🏢', label: 'Parceiros', href: '/admin?tab=partners' }, + { icon: '🎁', label: 'Benefícios', href: '/admin?tab=benefits' }, + { icon: '🔌', label: 'Plugins', href: '/admin?tab=plugins' }, + { icon: '📋', label: 'Logs', href: '/admin?tab=logs' }, + { icon: '⚙️', label: 'Configurações', href: '/settings' }, +]; + +const partnerMenu = [ + { icon: '📊', label: 'Dashboard', href: '/partner' }, + { icon: '🎁', label: 'Benefícios', href: '/partner?tab=benefits' }, + { icon: '👥', label: 'Membros', href: '/partner?tab=members' }, + { icon: '📈', label: 'Relatórios', href: '/partner?tab=reports' }, + { icon: '⚙️', label: 'Configurações', href: '/settings' }, +]; + +export default function Sidebar() { + const { user, logout, isAdmin, isPartner } = useAuth(); + const pathname = usePathname(); + const [collapsed, setCollapsed] = useState(false); + + const menu = isAdmin ? adminMenu : isPartner ? partnerMenu : userMenu; + + return ( + + ); +} diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index 30eb2c1..0000000 --- a/frontend/index.html +++ /dev/null @@ -1,330 +0,0 @@ - - - - - - Clube67 — Clube de Benefícios Premium - - - - - - - - - - - -
        -
        - - -
        -
        -
        - -
        -

        Clube67

        - Premium Benefícios -
        -
        - - - -
        -
        - -
        - -
        -
        -

        - Seus Benefícios Exclusivos -

        -

        - Explore descontos e vantagens premium selecionados sob medida para você nos melhores estabelecimentos parceiros do Mato Grosso do Sul. -

        -
        - - -
        - -
        - - - - -
        - - -
        - - - - - - - -
        -
        - - -
        - -
        -
        -

        Carregando benefícios exclusivos...

        -
        - -
        -
        - - - - - - -
        - - -
        - © 2026 Clube67. Todos os direitos reservados. -
        - - - - - diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts new file mode 100644 index 0000000..5864041 --- /dev/null +++ b/frontend/lib/api.ts @@ -0,0 +1,135 @@ +import Cookies from 'js-cookie'; + +const API_BASE = '/api'; + +interface FetchOptions extends RequestInit { + skipAuth?: boolean; +} + +async function fetchApi(url: string, options: FetchOptions = {}) { + const { skipAuth, ...fetchOpts } = options; + const headers: Record = { + 'Content-Type': 'application/json', + ...(fetchOpts.headers as Record), + }; + + if (!skipAuth) { + const token = Cookies.get('accessToken'); + if (token) headers['Authorization'] = `Bearer ${token}`; + } + + const res = await fetch(`${API_BASE}${url}`, { ...fetchOpts, headers }); + + if (res.status === 401) { + const data = await res.json().catch(() => ({})); + if (data.code === 'TOKEN_EXPIRED') { + const refreshed = await refreshToken(); + if (refreshed) { + headers['Authorization'] = `Bearer ${Cookies.get('accessToken')}`; + const retry = await fetch(`${API_BASE}${url}`, { ...fetchOpts, headers }); + return retry.json(); + } + } + if (typeof window !== 'undefined') { + Cookies.remove('accessToken'); + Cookies.remove('refreshToken'); + window.location.href = '/login'; + } + throw new Error('Não autenticado'); + } + + if (!res.ok) { + const data = await res.json().catch(() => ({ error: 'Erro desconhecido' })); + throw new Error(data.error || 'Erro na requisição'); + } + + return res.json(); +} + +async function refreshToken(): Promise { + try { + const rt = Cookies.get('refreshToken'); + if (!rt) return false; + const res = await fetch(`${API_BASE}/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: rt }), + }); + if (!res.ok) return false; + const data = await res.json(); + Cookies.set('accessToken', data.accessToken, { expires: 1 }); + Cookies.set('refreshToken', data.refreshToken, { expires: 7 }); + return true; + } catch { return false; } +} + +export const api = { + // Auth + login: (email: string, password: string) => + fetchApi('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }), skipAuth: true }), + register: (data: any) => + fetchApi('/auth/register', { method: 'POST', body: JSON.stringify(data), skipAuth: true }), + googleAuth: (data: any) => + fetchApi('/auth/google', { method: 'POST', body: JSON.stringify(data), skipAuth: true }), + me: () => fetchApi('/auth/me'), + logout: () => fetchApi('/auth/logout', { method: 'POST' }), + + // Users + getUsers: (params?: string) => fetchApi(`/users${params ? `?${params}` : ''}`), + getUser: (id: number) => fetchApi(`/users/${id}`), + updateUser: (id: number, data: any) => fetchApi(`/users/${id}`, { method: 'PUT', body: JSON.stringify(data) }), + deleteUser: (id: number) => fetchApi(`/users/${id}`, { method: 'DELETE' }), + getUserStats: () => fetchApi('/users/me/stats'), + getFavorites: () => fetchApi('/users/me/favorites'), + addFavorite: (benefitId: number) => fetchApi('/users/me/favorites', { method: 'POST', body: JSON.stringify({ benefitId }) }), + removeFavorite: (benefitId: number) => fetchApi(`/users/me/favorites/${benefitId}`, { method: 'DELETE' }), + getHistory: () => fetchApi('/users/me/history'), + + // Partners + getPartners: (params?: string) => fetchApi(`/partners${params ? `?${params}` : ''}`), + getPartner: (id: number) => fetchApi(`/partners/${id}`), + getPartnerBySlug: (slug: string) => fetchApi(`/partners/slug/${slug}`), + createPartner: (data: any) => fetchApi('/partners', { method: 'POST', body: JSON.stringify(data) }), + updatePartner: (id: number, data: any) => fetchApi(`/partners/${id}`, { method: 'PUT', body: JSON.stringify(data) }), + deletePartner: (id: number) => fetchApi(`/partners/${id}`, { method: 'DELETE' }), + getPartnerStats: (id: number) => fetchApi(`/partners/${id}/stats`), + + // Benefits + getBenefits: (params?: string) => fetchApi(`/benefits${params ? `?${params}` : ''}`), + getBenefit: (id: number) => fetchApi(`/benefits/${id}`), + createBenefit: (data: any) => fetchApi('/benefits', { method: 'POST', body: JSON.stringify(data) }), + updateBenefit: (id: number, data: any) => fetchApi(`/benefits/${id}`, { method: 'PUT', body: JSON.stringify(data) }), + deleteBenefit: (id: number) => fetchApi(`/benefits/${id}`, { method: 'DELETE' }), + approveBenefit: (id: number) => fetchApi(`/benefits/${id}/approve`, { method: 'POST' }), + redeemBenefit: (id: number) => fetchApi(`/benefits/${id}/redeem`, { method: 'POST' }), + getCategories: () => fetchApi('/benefits/categories'), + + // Admin + getDashboard: () => fetchApi('/admin/dashboard'), + getAuditLogs: (params?: string) => fetchApi(`/admin/logs${params ? `?${params}` : ''}`), + getMetrics: (period?: string) => fetchApi(`/admin/metrics${period ? `?period=${period}` : ''}`), + + // Plugins + getPlugins: () => fetchApi('/plugins'), + activatePlugin: (slug: string) => fetchApi(`/plugins/${slug}/activate`, { method: 'POST' }), + deactivatePlugin: (slug: string) => fetchApi(`/plugins/${slug}/deactivate`, { method: 'POST' }), + removePlugin: (slug: string) => fetchApi(`/plugins/${slug}`, { method: 'DELETE' }), + + // Webhooks + getWebhooks: () => fetchApi('/webhooks'), + createWebhook: (data: any) => fetchApi('/webhooks', { method: 'POST', body: JSON.stringify(data) }), +}; + +export function setTokens(accessToken: string, refreshToken: string) { + Cookies.set('accessToken', accessToken, { expires: 1 }); + Cookies.set('refreshToken', refreshToken, { expires: 7 }); +} + +export function clearTokens() { + Cookies.remove('accessToken'); + Cookies.remove('refreshToken'); +} + +export function getAccessToken() { + return Cookies.get('accessToken'); +} diff --git a/frontend/lib/auth.tsx b/frontend/lib/auth.tsx new file mode 100644 index 0000000..d1edb86 --- /dev/null +++ b/frontend/lib/auth.tsx @@ -0,0 +1,101 @@ +'use client'; + +import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; +import { api, setTokens, clearTokens, getAccessToken } from './api'; +import { useRouter } from 'next/navigation'; + +interface User { + id: number; + name: string; + email: string; + role: string; + avatar_url?: string; + phone?: string; + status: string; +} + +interface AuthContextType { + user: User | null; + loading: boolean; + login: (email: string, password: string) => Promise; + register: (data: any) => Promise; + googleLogin: (data: any) => Promise; + logout: () => void; + isAdmin: boolean; + isPartner: boolean; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const router = useRouter(); + + const fetchUser = useCallback(async () => { + try { + const token = getAccessToken(); + if (!token) { setLoading(false); return; } + const data = await api.me(); + setUser(data.user); + } catch { + clearTokens(); + setUser(null); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { fetchUser(); }, [fetchUser]); + + const login = async (email: string, password: string) => { + const data = await api.login(email, password); + setTokens(data.accessToken, data.refreshToken); + setUser(data.user); + redirectByRole(data.user.role); + }; + + const register = async (formData: any) => { + const data = await api.register(formData); + setTokens(data.accessToken, data.refreshToken); + setUser(data.user); + router.push('/dashboard'); + }; + + const googleLogin = async (googleData: any) => { + const data = await api.googleAuth(googleData); + setTokens(data.accessToken, data.refreshToken); + setUser(data.user); + redirectByRole(data.user.role); + }; + + const logout = () => { + api.logout().catch(() => { }); + clearTokens(); + setUser(null); + router.push('/login'); + }; + + const redirectByRole = (role: string) => { + switch (role) { + case 'super_admin': router.push('/admin'); break; + case 'partner_admin': router.push('/partner'); break; + default: router.push('/dashboard'); break; + } + }; + + const isAdmin = user?.role === 'super_admin'; + const isPartner = user?.role === 'partner_admin'; + + return ( + + {children} + + ); +} + +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error('useAuth must be used within AuthProvider'); + return ctx; +} diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts new file mode 100644 index 0000000..40c3d68 --- /dev/null +++ b/frontend/next-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/frontend/next.config.js b/frontend/next.config.js new file mode 100644 index 0000000..a8820e1 --- /dev/null +++ b/frontend/next.config.js @@ -0,0 +1,24 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: 'standalone', + images: { + remotePatterns: [ + { protocol: 'https', hostname: 'clube67.com' }, + { protocol: 'https', hostname: 'lh3.googleusercontent.com' }, + ], + }, + async rewrites() { + return [ + { + source: '/api/:path*', + destination: 'http://clube67-backend:3001/api/:path*', + }, + { + source: '/uploads/:path*', + destination: 'http://clube67-backend:3001/uploads/:path*', + }, + ]; + }, +}; + +module.exports = nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..2ae17c7 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,1904 @@ +{ + "name": "clube67-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "clube67-frontend", + "version": "1.0.0", + "dependencies": { + "clsx": "^2.1.1", + "framer-motion": "^11.5.0", + "js-cookie": "^3.0.5", + "lucide-react": "^0.447.0", + "next": "^14.2.0", + "qrcode.react": "^4.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "recharts": "^2.12.0" + }, + "devDependencies": { + "@types/js-cookie": "^3.0.6", + "@types/node": "^20.0.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "autoprefixer": "^10.4.0", + "postcss": "^8.4.0", + "tailwindcss": "^3.4.0", + "typescript": "^5.5.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "14.2.35", + "resolved": "https://registry.npmmirror.com/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "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.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmmirror.com/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.24", + "resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.4.24.tgz", + "integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001766", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "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" + } + ] + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmmirror.com/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "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.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmmirror.com/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "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.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "engines": { + "node": ">=14" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lucide-react": { + "version": "0.447.0", + "resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.447.0.tgz", + "integrity": "sha512-SZ//hQmvi+kDKrNepArVkYK7/jfeZ5uFNEnYmd45RKZcbGD78KLnrcNXmgeg6m+xNHFvTG+CblszXCy4n6DN4w==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmmirror.com/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmmirror.com/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "14.2.35", + "resolved": "https://registry.npmmirror.com/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", + "dependencies": { + "@next/env": "14.2.35", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/qrcode.react": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/qrcode.react/-/qrcode.react-4.2.0.tgz", + "integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/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" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmmirror.com/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmmirror.com/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmmirror.com/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "dependencies": { + "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/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/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" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmmirror.com/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "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" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..23ef7c6 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,32 @@ +{ + "name": "clube67-frontend", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev -p 3000", + "build": "next build", + "start": "next start -p 3000", + "lint": "next lint" + }, + "dependencies": { + "next": "^14.2.0", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "framer-motion": "^11.5.0", + "recharts": "^2.12.0", + "lucide-react": "^0.447.0", + "qrcode.react": "^4.0.0", + "js-cookie": "^3.0.5", + "clsx": "^2.1.1" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@types/js-cookie": "^3.0.6", + "typescript": "^5.5.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} \ No newline at end of file diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..67cdf1a --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/script.js b/frontend/script.js deleted file mode 100644 index 5b0d433..0000000 --- a/frontend/script.js +++ /dev/null @@ -1,393 +0,0 @@ -// ── CLUBE67 PREMIUM INTERACTIVE SCRIPT ── - -document.addEventListener('DOMContentLoaded', () => { - // Initialize Lucide Icons - lucide.createIcons(); - - // ── STATE MANAGEMENT ── - let activeTab = 'catalog'; - let benefits = []; - let activeVoucher = null; - let countdownInterval = null; - let timerTotalSeconds = 300; // 5 minutes - let timerRemainingSeconds = 300; - - // Premium Mock Data in case backend returns empty/offline to guarantee flawless display - const mockBenefits = [ - { id: 1, title: "Consulta + Limpeza Geral", description: "Ganhe 50% de desconto na primeira consulta preventiva e profilaxia.", category: "odontologia", partner: "ScoreOdonto MS", icon: "🦷", rating: "4.9", discount: "50% OFF" }, - { id: 2, title: "Checkout Cardiológico Completo", description: "Desconto especial de 30% em exames de eletrocardiograma e ecocardiograma.", category: "saude", partner: "CardioClinic CG", icon: "🩺", rating: "4.8", discount: "30% OFF" }, - { id: 3, title: "Menu Degustação Premium (Casal)", description: "Compre um menu degustação de 5 etapas e ganhe uma garrafa de vinho importado.", category: "gastronomia", partner: "Le Jardin Bistrô", icon: "🍔", rating: "5.0", discount: "Vinho Cortesia" }, - { id: 4, title: "Day Use Completo no Eco Resort", description: "Acesso total às piscinas naturais, trilhas e almoço pantaneiro incluso com 25% OFF.", category: "lazer", partner: "Bonito Eco Resort", icon: "🌴", rating: "4.7", discount: "25% OFF" }, - { id: 5, title: "Fórmulas Manipuladas e Vitaminas", description: "Desconto exclusivo de 20% em qualquer fórmula médica manipulada.", category: "farmacia", partner: "Fórmula Ativa", icon: "💊", rating: "4.9", discount: "20% OFF" }, - { id: 6, title: "MBA Executivo & Especializações", description: "Isenção na taxa de matrícula e 35% de desconto nas mensalidades do curso.", category: "educacao", partner: "Unigran Capital", icon: "📚", rating: "4.6", discount: "35% OFF" } - ]; - - // Analytics Dashboard Data - const analyticsData = { - members: 1248, - redemptions: 384, - savings: 14820, - goalPercentage: 96, - weeklyDistribution: { - seg: 25, - ter: 45, - qua: 70, - qui: 55, - sex: 85, - sab: 100, - dom: 35 - } - }; - - // ── DOM ELEMENTS ── - const navTabs = { - catalog: document.getElementById('tab-catalog'), - redeem: document.getElementById('tab-redeem'), - analytics: document.getElementById('tab-analytics') - }; - - const sections = { - catalog: document.getElementById('section-catalog'), - redeem: document.getElementById('section-redeem'), - analytics: document.getElementById('section-analytics') - }; - - const searchInput = document.getElementById('search-input'); - const categoryFilters = document.getElementById('category-filters'); - const benefitsGrid = document.getElementById('benefits-grid'); - - // Redeem tab elements - const redeemCardEmpty = document.getElementById('redeem-card-empty'); - const redeemCardActive = document.getElementById('redeem-card-active'); - const voucherIcon = document.getElementById('voucher-icon'); - const voucherTitle = document.getElementById('voucher-title'); - const voucherPartner = document.getElementById('voucher-partner'); - const voucherToken = document.getElementById('voucher-token'); - const qrCodeImg = document.getElementById('qr-code-img'); - const timerText = document.getElementById('timer-text'); - const timerBar = document.getElementById('timer-bar'); - const btnValidate = document.getElementById('btn-validate'); - const btnCancel = document.getElementById('btn-cancel'); - - // Analytics elements - const statMembers = document.getElementById('stat-members'); - const statRedemptions = document.getElementById('stat-redemptions'); - const statSavings = document.getElementById('stat-savings'); - const radialProgressBar = document.getElementById('radial-progress-bar'); - const radialPercentage = document.getElementById('radial-percentage'); - - const barSeg = document.getElementById('bar-seg'); - const barTer = document.getElementById('bar-ter'); - const barQua = document.getElementById('bar-qua'); - const barQui = document.getElementById('bar-qui'); - const barSex = document.getElementById('bar-sex'); - const barSab = document.getElementById('bar-sab'); - const barDom = document.getElementById('bar-dom'); - - // ── NAVIGATION LOGIC ── - function switchTab(tabId) { - if (activeTab === tabId) return; - - // Update tabs active state - Object.keys(navTabs).forEach(key => { - if (key === tabId) { - navTabs[key].classList.add('active'); - } else { - navTabs[key].classList.remove('active'); - } - }); - - // Hide old section, show new section - Object.keys(sections).forEach(key => { - if (key === tabId) { - sections[key].classList.remove('hidden'); - } else { - sections[key].classList.add('hidden'); - } - }); - - activeTab = tabId; - - // Perform specific triggers per tab - if (tabId === 'analytics') { - loadAnalyticsCharts(); - } - } - - Object.keys(navTabs).forEach(key => { - navTabs[key].addEventListener('click', () => switchTab(key)); - }); - - // ── BENEFIT FETCHING & RENDERING ── - async function loadBenefits() { - try { - // Sincronized backend API consume - const response = await fetch('/api/benefits'); - if (response.ok) { - const data = await response.json(); - benefits = data && data.length > 0 ? data : mockBenefits; - } else { - benefits = mockBenefits; - } - } catch (err) { - console.warn('[Clube67] API offline, running with premium fallback simulation.'); - benefits = mockBenefits; - } - - renderBenefits(benefits); - } - - function renderBenefits(list) { - const loadingEl = document.getElementById('catalog-loading'); - if (loadingEl) loadingEl.remove(); - - benefitsGrid.innerHTML = ''; - if (list.length === 0) { - benefitsGrid.innerHTML = ` -
        - -

        Nenhum benefício encontrado para esta categoria ou busca.

        -
        - `; - lucide.createIcons(); - return; - } - - list.forEach(benefit => { - const card = document.createElement('div'); - card.className = 'benefit-card'; - - // Mouse moves 3D lighting effect - card.addEventListener('mousemove', e => { - const rect = card.getBoundingClientRect(); - const x = e.clientX - rect.left; - const y = e.clientY - rect.top; - card.style.setProperty('--mouse-x', `${x}px`); - card.style.setProperty('--mouse-y', `${y}px`); - }); - - card.innerHTML = ` -
        - ${benefit.discount || 'Desconto'} -
        - -
        -
        - ${benefit.icon || '🎁'} -
        -
        - ${benefit.partner || 'Estabelecimento'} -

        ${benefit.title}

        -
        -
        - -

        ${benefit.description}

        - -
        -
        - - ${benefit.rating || '4.8'} -
        - -
        - `; - - benefitsGrid.appendChild(card); - }); - - // Add event listeners to voucher buttons - document.querySelectorAll('.btn-redeem-trigger').forEach(btn => { - btn.addEventListener('click', () => { - const id = parseInt(btn.getAttribute('data-id'), 10); - const selected = benefits.find(b => b.id === id); - if (selected) { - triggerVoucherRedemption(selected); - } - }); - }); - - lucide.createIcons(); - } - - // ── SEARCH & FILTERING ── - let activeCategory = 'all'; - - function filterBenefits() { - const query = searchInput.value.toLowerCase().trim(); - const filtered = benefits.filter(b => { - const matchesCategory = activeCategory === 'all' || b.category === activeCategory; - const matchesSearch = b.title.toLowerCase().includes(query) || - b.description.toLowerCase().includes(query) || - (b.partner && b.partner.toLowerCase().includes(query)); - return matchesCategory && matchesSearch; - }); - renderBenefits(filtered); - } - - searchInput.addEventListener('input', filterBenefits); - - categoryFilters.querySelectorAll('.filter-pill').forEach(pill => { - pill.addEventListener('click', () => { - categoryFilters.querySelector('.filter-pill.active').classList.remove('active'); - pill.classList.add('active'); - activeCategory = pill.getAttribute('data-category'); - filterBenefits(); - }); - }); - - // ── VOUCHER REDEMPTION & SCANNER TIMER ── - function triggerVoucherRedemption(benefit) { - activeVoucher = benefit; - - // Populate DOM elements - voucherIcon.textContent = benefit.icon || '🎁'; - voucherTitle.textContent = benefit.title; - voucherPartner.textContent = benefit.partner || 'Estabelecimento'; - - // Generate dynamic unique token - const randToken = 'C67-' + Math.random().toString(36).substring(2, 7).toUpperCase(); - voucherToken.textContent = randToken; - - // Update QR Code Source with parameters - qrCodeImg.src = `https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${randToken}&color=0f172a`; - - // Switch to redeem container view - redeemCardEmpty.classList.add('hidden'); - redeemCardActive.classList.remove('hidden'); - - // Switch view tab - switchTab('redeem'); - - // Start 5 minutes expiration timer - startVoucherTimer(); - } - - function startVoucherTimer() { - if (countdownInterval) clearInterval(countdownInterval); - - timerRemainingSeconds = timerTotalSeconds; - updateTimerDisplay(); - - countdownInterval = setInterval(() => { - timerRemainingSeconds--; - updateTimerDisplay(); - - if (timerRemainingSeconds <= 0) { - handleVoucherExpiration(); - } - }, 1000); - } - - function updateTimerDisplay() { - const min = Math.floor(timerRemainingSeconds / 60); - const sec = timerRemainingSeconds % 60; - - timerText.textContent = `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`; - - // Progress bar percentage shrink - const pct = (timerRemainingSeconds / timerTotalSeconds) * 100; - timerBar.style.width = `${pct}%`; - - // Danger colors for low time - if (timerRemainingSeconds < 60) { - timerBar.className = 'h-full bg-gradient-to-r from-red-500 to-rose-500 transition-all duration-1000'; - timerText.className = 'text-red-400 font-mono'; - } else { - timerBar.className = 'h-full bg-gradient-to-r from-indigo-500 to-violet-500 transition-all duration-1000'; - timerText.className = 'text-indigo-400 font-mono'; - } - } - - function handleVoucherExpiration() { - clearInterval(countdownInterval); - alert('Este token de resgate expirou por limite de tempo de segurança. Por favor, gere um novo voucher.'); - cancelVoucher(); - } - - function cancelVoucher() { - if (countdownInterval) clearInterval(countdownInterval); - activeVoucher = null; - redeemCardActive.classList.add('hidden'); - redeemCardEmpty.classList.remove('hidden'); - } - - btnCancel.addEventListener('click', cancelVoucher); - - // ── VOUCHER VALIDATION SIMULATOR (API CONSUME) ── - btnValidate.addEventListener('click', async () => { - if (!activeVoucher) return; - - btnValidate.disabled = true; - btnValidate.textContent = 'VALIDANDO...'; - - try { - // Dynamic validation call to backend API - const response = await fetch('/api/benefits/validate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - tokenId: voucherToken.textContent, - benefitId: activeVoucher.id - }) - }); - - // Simulated UI validation popup for high wow-factor and offline resilience - setTimeout(() => { - alert(`✅ SUCESSO! -O Voucher "${activeVoucher.title}" foi validado com sucesso e consumido pelo parceiro! -Token: ${voucherToken.textContent} -Estabelecimento: ${activeVoucher.partner || 'Clube67'}`); - - // Update analytics counters to reflect live usage! - analyticsData.redemptions++; - analyticsData.savings += 45; // average discount saving value - - cancelVoucher(); - btnValidate.disabled = false; - btnValidate.textContent = 'Simular Validação (Estabelecimento)'; - - // Automatically redirect to Analytics to see the metric update live! - switchTab('analytics'); - }, 1200); - - } catch (err) { - console.error('Validation API fail:', err); - btnValidate.disabled = false; - btnValidate.textContent = 'Simular Validação (Estabelecimento)'; - } - }); - - // ── ANALYTICS CHARTS & COUNTERS ── - function loadAnalyticsCharts() { - // Load live dynamic counters - statMembers.textContent = analyticsData.members.toLocaleString(); - statRedemptions.textContent = analyticsData.redemptions.toLocaleString(); - statSavings.textContent = `R$ ${analyticsData.savings.toLocaleString('pt-BR')}`; - - // Load SVG gauge radial chart progress - const radius = 64; - const circumference = 2 * Math.PI * radius; // 402.12 - const pct = analyticsData.goalPercentage; - const offset = circumference - (pct / 100) * circumference; - - radialProgressBar.style.strokeDasharray = `${circumference}`; - radialProgressBar.style.strokeDashoffset = `${offset}`; - radialPercentage.textContent = `${pct}%`; - - // Load vertical bar charts heights - setTimeout(() => { - barSeg.style.height = `${analyticsData.weeklyDistribution.seg}%`; - barTer.style.height = `${analyticsData.weeklyDistribution.ter}%`; - barQua.style.height = `${analyticsData.weeklyDistribution.qua}%`; - barQui.style.height = `${analyticsData.weeklyDistribution.qui}%`; - barSex.style.height = `${analyticsData.weeklyDistribution.sex}%`; - barSab.style.height = `${analyticsData.weeklyDistribution.sab}%`; - barDom.style.height = `${analyticsData.weeklyDistribution.dom}%`; - }, 100); - } - - // Initialize Catalog Fetching - loadBenefits(); -}); diff --git a/frontend/style.css b/frontend/style.css deleted file mode 100644 index 2ef26b1..0000000 --- a/frontend/style.css +++ /dev/null @@ -1,324 +0,0 @@ -/* ── DESIGN SYSTEM & STYLING — CLUBE67 PREMIUM ── */ - -/* Typography & Root variables */ -:root { - --font-display: 'Outfit', 'Inter', sans-serif; - --font-sans: 'Inter', sans-serif; - - /* Sleek harmonized dark palette */ - --color-bg: 15 23 42; /* slate-900 equivalent */ - --color-primary: 99 102 241; /* indigo-500 */ - --color-primary-rgb: 99, 102, 241; - --color-accent: 139 92 246; /* violet-500 */ - --color-accent-rgb: 139, 92, 246; - - --glass-bg: rgba(15, 23, 42, 0.45); - --glass-border: rgba(255, 255, 255, 0.05); - --glass-border-hover: rgba(99, 102, 241, 0.25); -} - -/* Base resets & styles */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - background-color: rgb(10, 15, 30); - color: #e2e8f0; - font-family: var(--font-sans); - -webkit-font-smoothing: antialiased; -} - -.font-display { - font-family: var(--font-display); -} - -/* Custom Webkit scrollbar for sleek premium look */ -::-webkit-scrollbar { - width: 8px; -} -::-webkit-scrollbar-track { - background: rgb(10, 15, 30); -} -::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 9999px; -} -::-webkit-scrollbar-thumb:hover { - background: rgba(99, 102, 241, 0.3); -} - -/* Typography elements styling */ -h1, h2, h3, h4 { - font-family: var(--font-display); - color: #ffffff; -} - -/* Header & Navigation styling */ -.nav-tab { - background: transparent; - border: none; - color: #64748b; /* slate-500 */ - cursor: pointer; -} - -.nav-tab:hover { - color: #cbd5e1; /* slate-300 */ -} - -.nav-tab.active { - background-color: rgba(99, 102, 241, 0.15); - color: #818cf8; /* indigo-400 */ - box-shadow: 0 4px 12px rgba(99, 102, 241, 0.08); -} - -/* Pills & Filter chips */ -.filter-pill { - background: rgba(15, 23, 42, 0.6); - border: 1px border rgba(255, 255, 255, 0.04); - color: #94a3b8; /* slate-400 */ - padding: 8px 18px; - border-radius: 9999px; - font-size: 0.75rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - border: 1px solid rgba(255, 255, 255, 0.04); -} - -.filter-pill:hover { - background: rgba(99, 102, 241, 0.08); - border-color: rgba(99, 102, 241, 0.2); - color: #cbd5e1; -} - -.filter-pill.active { - background: gradient-to-r from-indigo-600 to-violet-600; - background: linear-gradient(135deg, rgb(79, 70, 229), rgb(124, 58, 237)); - border-color: rgba(99, 102, 241, 0.4); - color: #ffffff; - box-shadow: 0 4px 15px rgba(99, 102, 241, 0.25); -} - -/* Glassmorphism Benefit Cards styling */ -.benefit-card { - background: var(--glass-bg); - border: 1px solid var(--glass-border); - border-radius: 24px; - padding: 24px; - backdrop-filter: blur(12px); - transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); - overflow: hidden; - position: relative; -} - -.benefit-card::before { - content: ''; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: radial-gradient(800px circle at var(--mouse-x, 0) var(--mouse-y, 0), rgba(255, 255, 255, 0.03), transparent 40%); - z-index: 1; - pointer-events: none; -} - -.benefit-card:hover { - transform: translateY(-5px); - border-color: var(--glass-border-hover); - box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3), 0 0 20px rgba(99, 102, 241, 0.05); -} - -/* Laser scan animation for voucher validation page */ -@keyframes scan { - 0%, 100% { - top: 0%; - opacity: 0.8; - } - 50% { - top: 98%; - opacity: 0.8; - } -} - -.animate-scan { - animation: scan 3s ease-in-out infinite; -} - -/* Spacing & Layout Utilities */ -.p-1 { padding: 0.25rem !important; } -.p-4 { padding: 1rem !important; } -.p-6 { padding: 1.5rem !important; } -.p-8 { padding: 2rem !important; } -.px-4 { padding-left: 1rem !important; padding-right: 1rem !important; } -.px-5 { padding-left: 1.25rem !important; padding-right: 1.25rem !important; } -.px-6 { padding-left: 1.5rem !important; padding-right: 1.5rem !important; } -.py-2 { padding-top: 0.5rem !important; padding-bottom: 0.5rem !important; } -.py-2\.5 { padding-top: 0.625rem !important; padding-bottom: 0.625rem !important; } -.py-3 { padding-top: 0.75rem !important; padding-bottom: 0.75rem !important; } -.py-4 { padding-top: 1rem !important; padding-bottom: 1rem !important; } -.py-8 { padding-top: 2rem !important; padding-bottom: 2rem !important; } -.py-12 { padding-top: 3rem !important; padding-bottom: 3rem !important; } -.py-20 { padding-top: 5rem !important; padding-bottom: 5rem !important; } -.pl-4 { padding-left: 1rem !important; } -.pl-11 { padding-left: 2.75rem !important; } -.pr-4 { padding-right: 1rem !important; } - -/* Display & Flexbox */ -.flex { display: flex !important; } -.flex-col { flex-direction: column !important; } -.items-center { align-items: center !important; } -.justify-between { justify-content: space-between !important; } -.justify-center { justify-content: center !important; } -.flex-wrap { flex-wrap: wrap !important; } -.flex-1 { flex: 1 1 0% !important; } - -/* Gap Utilities */ -.gap-1 { gap: 0.25rem !important; } -.gap-2 { gap: 0.5rem !important; } -.gap-3 { gap: 0.75rem !important; } -.gap-4 { gap: 1rem !important; } -.gap-6 { gap: 1.5rem !important; } - -/* Grid Utilities */ -.grid { display: grid !important; } -.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)) !important; } - -@media (min-width: 768px) { - .md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; } - .md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; } - .md\:flex-row { flex-direction: row !important; } - .md\:text-left { text-align: left !important; } -} - -@media (min-width: 1024px) { - .lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; } - .lg\:grid-cols-12 { grid-template-columns: repeat(12, minmax(0, 1fr)) !important; } - .lg\:col-span-4 { grid-column: span 4 / span 4 !important; } - .lg\:col-span-8 { grid-column: span 8 / span 8 !important; } -} - -/* Sizing Utilities */ -.w-full { width: 100% !important; } -.max-w-7xl { max-width: 80rem !important; } -.max-w-2xl { max-width: 42rem !important; } -.max-w-md { max-width: 28rem !important; } -.mx-auto { margin-left: auto !important; margin-right: auto !important; } - -.w-3\.5 { width: 0.875rem !important; } -.h-3\.5 { height: 0.875rem !important; } -.w-4 { width: 1rem !important; } -.h-4 { height: 1rem !important; } -.w-8 { width: 2rem !important; } -.h-8 { height: 2rem !important; } -.w-10 { width: 2.5rem !important; } -.h-10 { height: 2.5rem !important; } -.w-12 { width: 3rem !important; } -.h-12 { height: 3rem !important; } -.w-40 { width: 10rem !important; } -.h-40 { height: 10rem !important; } -.w-48 { width: 12rem !important; } -.h-48 { height: 12rem !important; } -.w-96 { width: 24rem !important; } -.h-96 { height: 24rem !important; } -.w-\[500px\] { width: 500px !important; } -.h-\[500px\] { height: 500px !important; } - -/* Borders & Styles */ -.border { border: 1px solid rgba(255, 255, 255, 0.08) !important; } -.border-b { border-bottom: 1px solid rgba(255, 255, 255, 0.08) !important; } -.border-slate-800 { border-color: rgba(30, 41, 59, 0.6) !important; } -.border-slate-800\/50 { border-color: rgba(30, 41, 59, 0.4) !important; } -.border-slate-800\/80 { border-color: rgba(30, 41, 59, 0.8) !important; } -.border-slate-900\/80 { border-color: rgba(15, 23, 42, 0.8) !important; } -.border-slate-900\/20 { border-color: rgba(15, 23, 42, 0.2) !important; } - -/* Border Radius */ -.rounded-lg { border-radius: 0.5rem !important; } -.rounded-xl { border-radius: 0.75rem !important; } -.rounded-2xl { border-radius: 1rem !important; } -.rounded-3xl { border-radius: 1.5rem !important; } - -/* Typography & Colors */ -.text-center { text-align: center !important; } -.text-slate-400 { color: #94a3b8 !important; } -.text-slate-500 { color: #64748b !important; } -.text-slate-600 { color: #475569 !important; } -.text-slate-100 { color: #f1f5f9 !important; } -.text-white { color: #ffffff !important; } -.text-indigo-400 { color: #818cf8 !important; } -.text-emerald-400 { color: #34d399 !important; } -.font-bold { font-weight: 700 !important; } -.font-semibold { font-weight: 600 !important; } -.font-black { font-weight: 900 !important; } - -.text-xs { font-size: 0.75rem !important; } -.text-sm { font-size: 0.875rem !important; } -.text-base { font-size: 1rem !important; } -.text-lg { font-size: 1.125rem !important; } -.text-xl { font-size: 1.25rem !important; } -.text-3xl { font-size: 1.875rem !important; } -.text-4xl { font-size: 2.25rem !important; } - -.uppercase { text-transform: uppercase !important; } -.tracking-wide { letter-spacing: 0.025em !important; } -.tracking-wider { letter-spacing: 0.05em !important; } -.tracking-widest { letter-spacing: 0.1em !important; } -.leading-none { line-height: 1 !important; } -.leading-relaxed { line-height: 1.625 !important; } -.leading-snug { line-height: 1.375 !important; } - -/* Transitions & Shadows */ -.transition-all { transition-property: all !important; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1) !important; transition-duration: 150ms !important; } -.duration-300 { transition-duration: 300ms !important; } -.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important; } -.shadow-2xl { box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5) !important; } - -/* Semantic Classes */ -.brand-subtitle { - font-size: 9px !important; - letter-spacing: 0.25em !important; -} -.trend-pct { - font-size: 10px !important; -} -.shadow-logo { - box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.2), 0 4px 6px -2px rgba(99, 102, 241, 0.1) !important; -} -.shadow-button { - box-shadow: 0 10px 15px -3px rgba(99, 102, 241, 0.25), 0 4px 6px -2px rgba(99, 102, 241, 0.15) !important; -} - -.text-9px { font-size: 9px !important; } -.text-10px { font-size: 10px !important; } -.letter-spacing-25 { letter-spacing: 0.25em !important; } - -.selection\:bg-indigo-500\/30::selection { background-color: rgba(99, 102, 241, 0.3); } -.selection\:text-indigo-200::selection { color: #c7d2fe; } - -.fixed { position: fixed !important; } -.top-0 { top: 0 !important; } -.sticky { position: sticky !important; } -.z-40 { z-index: 40 !important; } -.backdrop-blur-md { backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; } -.backdrop-blur-xl { backdrop-filter: blur(24px) !important; -webkit-backdrop-filter: blur(24px) !important; } -.pointer-events-none { pointer-events: none !important; } -.overflow-x-hidden { overflow-x: hidden !important; } -.relative { position: relative !important; } -.absolute { position: absolute !important; } -.inset-y-0 { top: 0; bottom: 0; } -.left-0 { left: 0 !important; } -.right-0 { right: 0 !important; } -.pl-4 { padding-left: 1rem !important; } -.object-contain { object-fit: contain !important; } -.bg-slate-950 { background-color: rgb(2, 6, 23) !important; } -.bg-slate-950\/60 { background-color: rgba(2, 6, 23, 0.6) !important; } -.bg-slate-950\/80 { background-color: rgba(2, 6, 23, 0.8) !important; } -.bg-slate-900\/40 { background-color: rgba(15, 23, 42, 0.4) !important; } -.bg-slate-900\/60 { background-color: rgba(15, 23, 42, 0.6) !important; } -.bg-slate-900\/35 { background-color: rgba(15, 23, 42, 0.35) !important; } diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..0fc1c20 --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,68 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: 'class', + content: [ + './app/**/*.{js,ts,jsx,tsx,mdx}', + './components/**/*.{js,ts,jsx,tsx,mdx}', + ], + theme: { + extend: { + colors: { + brand: { + 50: '#eef7ff', + 100: '#d8ecff', + 200: '#b9deff', + 300: '#89caff', + 400: '#52adff', + 500: '#2a8bff', + 600: '#1469f5', + 700: '#0d54e1', + 800: '#1244b6', + 900: '#153d8f', + 950: '#112757', + }, + dark: { + 50: '#f6f6f9', + 100: '#ededf1', + 200: '#d7d8e0', + 300: '#b4b5c5', + 400: '#8b8da5', + 500: '#6d6f8a', + 600: '#575972', + 700: '#47495d', + 800: '#3d3f4f', + 900: '#1e1f2b', + 950: '#13141d', + }, + }, + fontFamily: { + sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'], + }, + animation: { + 'fade-in': 'fadeIn 0.5s ease-out', + 'slide-up': 'slideUp 0.5s ease-out', + 'slide-in': 'slideIn 0.3s ease-out', + 'pulse-soft': 'pulseSoft 2s infinite', + }, + keyframes: { + fadeIn: { + '0%': { opacity: '0' }, + '100%': { opacity: '1' }, + }, + slideUp: { + '0%': { opacity: '0', transform: 'translateY(20px)' }, + '100%': { opacity: '1', transform: 'translateY(0)' }, + }, + slideIn: { + '0%': { opacity: '0', transform: 'translateX(-20px)' }, + '100%': { opacity: '1', transform: 'translateX(0)' }, + }, + pulseSoft: { + '0%, 100%': { opacity: '1' }, + '50%': { opacity: '0.7' }, + }, + }, + }, + }, + plugins: [], +}; diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..860f962 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..1bd54ae --- /dev/null +++ b/index.html @@ -0,0 +1,55 @@ + + + + + Site is created successfully! + + + +
        +

        Congratulations, the site is created successfully!

        +

        This is the default index.html, this page is automatically generated by the system

        +
          +
        • The index.html of this page is in the site root directory
        • +
        • You can modify, delete or overwrite this page
        • +
        +
        + + + \ No newline at end of file diff --git a/nginx/clube67.conf b/nginx/clube67.conf new file mode 100644 index 0000000..f05f206 --- /dev/null +++ b/nginx/clube67.conf @@ -0,0 +1,114 @@ +# ── Upstream: Next.js frontend ── +upstream frontend { + server 127.0.0.1:3000; +} + +# ── Upstream: Express API backend ── +upstream backend { + server 127.0.0.1:3001; +} + +# ── HTTP → HTTPS redirect ── +server { + listen 80; + server_name clube67.com www.clube67.com; + return 301 https://$host$request_uri; +} + +# ── Main HTTPS server ── +server { + listen 443 ssl http2; + server_name clube67.com www.clube67.com; + + # SSL (managed by certbot) + ssl_certificate /etc/letsencrypt/live/clube67.com/fullchain.pem; # managed by Certbot + ssl_certificate_key /etc/letsencrypt/live/clube67.com/privkey.pem; # managed by Certbot + include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot + + # ── Security Headers ── + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; + + # ── Gzip / Brotli ── + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_min_length 256; + gzip_types + text/plain text/css text/xml text/javascript + application/json application/javascript application/xml + application/rss+xml image/svg+xml font/woff2; + + # ── Uploads (static) ── + location /uploads/ { + alias /www/wwwroot/clube67.com/uploads/; + expires 30d; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # ── API proxy ── + location /api/ { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # Disable directory listing + autoindex off; + } + + # ── WebSocket ── + location /socket.io/ { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400s; + } + + # ── Next.js static assets ── + location /_next/static/ { + proxy_pass http://frontend; + expires 365d; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # ── Frontend (catch-all) ── + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # ── Disable hidden files ── + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } + + # ── File size ── + client_max_body_size 10M; +} diff --git a/nginx/default.conf b/nginx/default.conf index a61c43e..d3ce794 100644 --- a/nginx/default.conf +++ b/nginx/default.conf @@ -1,37 +1,96 @@ +# ── Upstream: Next.js frontend ── +upstream frontend { + server clube67-frontend:3000; +} + +# ── Upstream: Express API backend ── +upstream backend { + server clube67-backend:3001; +} + server { listen 80; server_name clube67.com www.clube67.com; - client_max_body_size 50M; + # Limite de upload para arquivos + client_max_body_size 10M; - # Resolver DNS interno do Docker para resolução dinâmica - resolver 127.0.0.11 valid=30s; + # ── Security Headers ── + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; - # Proxy para o Socket.io (Realtime Events) - location /socket.io/ { - set $backend_upstream http://clube67-backend:3001; - proxy_pass $backend_upstream; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "Upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; + # ── Gzip ── + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_min_length 256; + gzip_types + text/plain text/css text/xml text/javascript + application/json application/javascript application/xml + application/rss+xml image/svg+xml font/woff2; + + # ── Uploads (static) ── + location /uploads/ { + alias /www/wwwroot/clube67.com/uploads/; + expires 30d; + add_header Cache-Control "public, immutable"; + access_log off; } - # Proxy para tudo (Landing Page Estática & API Express do Clube67) - location / { - set $backend_upstream http://clube67-backend:3001; - proxy_pass $backend_upstream; + # ── API proxy ── + location /api/ { + proxy_pass http://backend; proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + autoindex off; + } + + # ── WebSocket ── + location /socket.io/ { + proxy_pass http://backend; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400s; + } + + # ── Next.js static assets ── + location /_next/static/ { + proxy_pass http://frontend; + expires 365d; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # ── Frontend (catch-all) ── + location / { + proxy_pass http://frontend; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # ── Disable hidden files ── + location ~ /\. { + deny all; + access_log off; + log_not_found off; } } - diff --git a/plugins/SmartVision/backend/Optimizer.js b/plugins/SmartVision/backend/Optimizer.js new file mode 100644 index 0000000..95491f9 --- /dev/null +++ b/plugins/SmartVision/backend/Optimizer.js @@ -0,0 +1,60 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ImageOptimizer = void 0; +const sharp_1 = __importDefault(require("sharp")); +class ImageOptimizer { + /** + * Optimizes an image: converts to WebP, strips metadata, + * and applies a 1:1 Smart Crop if needed. + */ + static async optimize(buffer, category = 'media', options = {}) { + try { + let pipeline = (0, sharp_1.default)(buffer); + const metadata = await pipeline.metadata(); + if (!metadata.format) + return { buffer, mimetype: 'application/octet-stream' }; + // 1. Identify Target Size + // Default sizes based on category + let targetSize = options.size || 1024; + if (category === 'partners' || category === 'users') + targetSize = 400; + if (category === 'benefits') + targetSize = 800; + // 2. Smart Crop 1:1 if aspect=1:1 is requested or implied by category + const shouldCrop = options.aspect === '1:1' || ['partners', 'users'].includes(category); + if (shouldCrop) { + pipeline = pipeline.resize({ + width: targetSize, + height: targetSize, + fit: sharp_1.default.fit.cover, + position: sharp_1.default.strategy.entropy // Smart focal point detection + }); + } + else { + // Resize without cropping if it's too large + pipeline = pipeline.resize({ + width: targetSize, + withoutEnlargement: true, + fit: sharp_1.default.fit.inside + }); + } + // 3. Convert to WebP & Strip Metadata + const optimizedBuffer = await pipeline + .webp({ quality: 85, effort: 4 }) + .toBuffer(); + return { + buffer: optimizedBuffer, + mimetype: 'image/webp' + }; + } + catch (err) { + console.error('[SmartVision] Optimization failed:', err); + return { buffer, mimetype: 'image/jpeg' }; // Fallback + } + } +} +exports.ImageOptimizer = ImageOptimizer; +//# sourceMappingURL=Optimizer.js.map \ No newline at end of file diff --git a/plugins/SmartVision/backend/Optimizer.js.map b/plugins/SmartVision/backend/Optimizer.js.map new file mode 100644 index 0000000..f613bb5 --- /dev/null +++ b/plugins/SmartVision/backend/Optimizer.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Optimizer.js","sourceRoot":"","sources":["Optimizer.ts"],"names":[],"mappings":";;;;;;AAAA,kDAA0B;AAO1B,MAAa,cAAc;IACvB;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,QAAQ,CACjB,MAAc,EACd,WAAmB,OAAO,EAC1B,UAA8C,EAAE;QAEhD,IAAI,CAAC;YACD,IAAI,QAAQ,GAAG,IAAA,eAAK,EAAC,MAAM,CAAC,CAAC;YAC7B,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,CAAC;YAE3C,IAAI,CAAC,QAAQ,CAAC,MAAM;gBAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,0BAA0B,EAAE,CAAC;YAE9E,0BAA0B;YAC1B,kCAAkC;YAClC,IAAI,UAAU,GAAG,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC;YACtC,IAAI,QAAQ,KAAK,UAAU,IAAI,QAAQ,KAAK,OAAO;gBAAE,UAAU,GAAG,GAAG,CAAC;YACtE,IAAI,QAAQ,KAAK,UAAU;gBAAE,UAAU,GAAG,GAAG,CAAC;YAE9C,sEAAsE;YACtE,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAExF,IAAI,UAAU,EAAE,CAAC;gBACb,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC;oBACvB,KAAK,EAAE,UAAU;oBACjB,MAAM,EAAE,UAAU;oBAClB,GAAG,EAAE,eAAK,CAAC,GAAG,CAAC,KAAK;oBACpB,QAAQ,EAAE,eAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,8BAA8B;iBAClE,CAAC,CAAC;YACP,CAAC;iBAAM,CAAC;gBACJ,4CAA4C;gBAC5C,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC;oBACvB,KAAK,EAAE,UAAU;oBACjB,kBAAkB,EAAE,IAAI;oBACxB,GAAG,EAAE,eAAK,CAAC,GAAG,CAAC,MAAM;iBACxB,CAAC,CAAC;YACP,CAAC;YAED,sCAAsC;YACtC,MAAM,eAAe,GAAG,MAAM,QAAQ;iBACjC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;iBAChC,QAAQ,EAAE,CAAC;YAEhB,OAAO;gBACH,MAAM,EAAE,eAAe;gBACvB,QAAQ,EAAE,YAAY;aACzB,CAAC;QACN,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACX,OAAO,CAAC,KAAK,CAAC,oCAAoC,EAAE,GAAG,CAAC,CAAC;YACzD,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC,CAAC,WAAW;QAC1D,CAAC;IACL,CAAC;CACJ;AAvDD,wCAuDC"} \ No newline at end of file diff --git a/plugins/SmartVision/backend/Optimizer.ts b/plugins/SmartVision/backend/Optimizer.ts new file mode 100644 index 0000000..f072bf1 --- /dev/null +++ b/plugins/SmartVision/backend/Optimizer.ts @@ -0,0 +1,63 @@ +import sharp from 'sharp'; + +export interface OptimizationResult { + buffer: Buffer; + mimetype: string; +} + +export class ImageOptimizer { + /** + * Optimizes an image: converts to WebP, strips metadata, + * and applies a 1:1 Smart Crop if needed. + */ + static async optimize( + buffer: Buffer, + category: string = 'media', + options: { aspect?: string; size?: number } = {} + ): Promise { + try { + let pipeline = sharp(buffer); + const metadata = await pipeline.metadata(); + + if (!metadata.format) return { buffer, mimetype: 'application/octet-stream' }; + + // 1. Identify Target Size + // Default sizes based on category + let targetSize = options.size || 1024; + if (category === 'partners' || category === 'users') targetSize = 400; + if (category === 'benefits') targetSize = 800; + + // 2. Smart Crop 1:1 if aspect=1:1 is requested or implied by category + const shouldCrop = options.aspect === '1:1' || ['partners', 'users'].includes(category); + + if (shouldCrop) { + pipeline = pipeline.resize({ + width: targetSize, + height: targetSize, + fit: sharp.fit.cover, + position: sharp.strategy.entropy // Smart focal point detection + }); + } else { + // Resize without cropping if it's too large + pipeline = pipeline.resize({ + width: targetSize, + withoutEnlargement: true, + fit: sharp.fit.inside + }); + } + + // 3. Convert to WebP & Strip Metadata + const optimizedBuffer = await pipeline + .webp({ quality: 85, effort: 4 }) + .toBuffer(); + + return { + buffer: optimizedBuffer, + mimetype: 'image/webp' + }; + } catch (err) { + console.error('[SmartVision] Optimization failed:', err); + return { buffer, mimetype: 'image/jpeg' }; // Fallback + } + } +} diff --git a/plugins/SmartVision/index.js b/plugins/SmartVision/index.js new file mode 100644 index 0000000..26431e1 --- /dev/null +++ b/plugins/SmartVision/index.js @@ -0,0 +1,33 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const Optimizer_1 = require("./backend/Optimizer"); +const manifest_json_1 = __importDefault(require("./manifest.json")); +const plugin = { + manifest: manifest_json_1.default, + async activate(ctx) { + // Register for the upload:transform hook + ctx.hooks.register('upload:transform', async (payload) => { + const { buffer, mimetype, category } = payload; + // Only process common image formats + const imageTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/avif']; + if (!imageTypes.includes(mimetype)) { + return null; // Skip non-images + } + ctx.logger.info(`[SmartVision] Optimizing image for category: ${category}`); + const result = await Optimizer_1.ImageOptimizer.optimize(buffer, category); + return { + buffer: result.buffer, + mimetype: result.mimetype + }; + }); + ctx.logger.info('SmartVision Optimizer activated'); + }, + async deactivate(ctx) { + ctx.logger.info('SmartVision Optimizer deactivated'); + } +}; +exports.default = plugin; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/plugins/SmartVision/index.js.map b/plugins/SmartVision/index.js.map new file mode 100644 index 0000000..a973c6e --- /dev/null +++ b/plugins/SmartVision/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;AACA,mDAAqD;AACrD,oEAAuC;AAEvC,MAAM,MAAM,GAAmB;IAC3B,QAAQ,EAAE,uBAAe;IACzB,KAAK,CAAC,QAAQ,CAAC,GAAkB;QAC7B,yCAAyC;QACzC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,kBAAkB,EAAE,KAAK,EAAE,OAAY,EAAE,EAAE;YAC1D,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;YAE/C,oCAAoC;YACpC,MAAM,UAAU,GAAG,CAAC,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC;YAC3E,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,OAAO,IAAI,CAAC,CAAC,kBAAkB;YACnC,CAAC;YAED,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,gDAAgD,QAAQ,EAAE,CAAC,CAAC;YAE5E,MAAM,MAAM,GAAG,MAAM,0BAAc,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;YAE/D,OAAO;gBACH,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,QAAQ,EAAE,MAAM,CAAC,QAAQ;aAC5B,CAAC;QACN,CAAC,CAAC,CAAC;QAEH,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,iCAAiC,CAAC,CAAC;IACvD,CAAC;IACD,KAAK,CAAC,UAAU,CAAC,GAAkB;QAC/B,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;IACzD,CAAC;CACJ,CAAC;AAEF,kBAAe,MAAM,CAAC"} \ No newline at end of file diff --git a/plugins/SmartVision/index.ts b/plugins/SmartVision/index.ts new file mode 100644 index 0000000..12f97c5 --- /dev/null +++ b/plugins/SmartVision/index.ts @@ -0,0 +1,35 @@ +import { PluginInstance, PluginContext } from '../../backend/src/core/types'; +import { ImageOptimizer } from './backend/Optimizer'; +import manifest from './manifest.json'; + +const plugin: PluginInstance = { + manifest: manifest as any, + async activate(ctx: PluginContext): Promise { + // Register for the upload:transform hook + ctx.hooks.register('upload:transform', async (payload: any) => { + const { buffer, mimetype, category } = payload; + + // Only process common image formats + const imageTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/avif']; + if (!imageTypes.includes(mimetype)) { + return null; // Skip non-images + } + + ctx.logger.info(`[SmartVision] Optimizing image for category: ${category}`); + + const result = await ImageOptimizer.optimize(buffer, category); + + return { + buffer: result.buffer, + mimetype: result.mimetype + }; + }); + + ctx.logger.info('SmartVision Optimizer activated'); + }, + async deactivate(ctx: PluginContext): Promise { + ctx.logger.info('SmartVision Optimizer deactivated'); + } +}; + +export default plugin; diff --git a/plugins/SmartVision/manifest.json b/plugins/SmartVision/manifest.json new file mode 100644 index 0000000..267d598 --- /dev/null +++ b/plugins/SmartVision/manifest.json @@ -0,0 +1,19 @@ +{ + "name": "SmartVision", + "displayName": "SmartVision Optimizer", + "version": "1.0.0", + "description": "Otimização inteligente de imagens com WebP e Smart Crop 1:1.", + "author": "Clube67 Team", + "category": "integration", + "enabled": true, + "canDisable": true, + "dependencies": [ + "uploads" + ], + "hooks": { + "subscribes": [ + "upload:transform" + ], + "emits": [] + } +} \ No newline at end of file diff --git a/plugins/UnifiedStorageProvider/index.js b/plugins/UnifiedStorageProvider/index.js new file mode 100644 index 0000000..3d60088 --- /dev/null +++ b/plugins/UnifiedStorageProvider/index.js @@ -0,0 +1,388 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const client_s3_1 = require("@aws-sdk/client-s3"); +const lib_storage_1 = require("@aws-sdk/lib-storage"); +let storageProvider; +if (process.env.NODE_ENV === 'production') { + storageProvider = require('../../dist/core/StorageProvider').storageProvider; +} +else { + storageProvider = require('../../backend/src/core/StorageProvider').storageProvider; +} +const sharp_1 = __importDefault(require("sharp")); +const fs_1 = __importDefault(require("fs")); +const path_1 = __importDefault(require("path")); +const VALID_CATEGORIES = ['documents', 'exports', 'posts', 'cards', 'partners', 'whatsapp', 'media']; +// Mapa de extensão → mimetype para o proxy +const MIME_MAP = { + jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', + gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', + pdf: 'application/pdf', mp4: 'video/mp4', ogg: 'audio/ogg', + mp3: 'audio/mpeg', doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', +}; +class UnifiedStorageProviderPlugin { + constructor() { + this.s3 = null; + this.bucket = ''; + this.region = 'us-east-1'; + this.localFallbackDir = path_1.default.resolve(process.cwd(), 'storage', 'local_fallback'); + this.routesRegistered = false; + } + async activate(ctx) { + this.ctx = ctx; + const cfg = ctx.config.get('UnifiedStorageProvider') || {}; + if (!fs_1.default.existsSync(this.localFallbackDir)) { + fs_1.default.mkdirSync(this.localFallbackDir, { recursive: true }); + } + const accessKey = cfg.wasabiAccessKey || process.env.WASABI_ACCESS_KEY; + const secretKey = cfg.wasabiSecretKey || process.env.WASABI_SECRET_KEY; + this.region = (cfg.wasabiRegion || process.env.WASABI_REGION || 'us-east-1'); + this.bucket = (cfg.wasabiBucket || process.env.WASABI_BUCKET || ''); + const rawEndpoint = (cfg.wasabiEndpoint || process.env.WASABI_ENDPOINT || 's3.wasabisys.com'); + const endpoint = rawEndpoint.startsWith('http') ? rawEndpoint : `https://${rawEndpoint}`; + ctx.logger.info({ cfgKeys: Object.keys(cfg), hasKey: !!accessKey, hasBucket: !!this.bucket }, '[UnifiedStorage] activate debug'); + if (!accessKey || !secretKey || !this.bucket) { + ctx.logger.warn('UnifiedStorageProvider: credenciais ou bucket não configurados — configure via admin'); + } + else { + this.s3 = new client_s3_1.S3Client({ + region: this.region, + endpoint, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + storageProvider.register(this); + ctx.logger.info(`UnifiedStorageProvider ativo — bucket: ${this.bucket} endpoint: ${endpoint}`); + this.startSyncWorker(); + } + if (!this.routesRegistered) { + // ── Proxy: serve arquivos do Wasabi (ou fallback local) ──────────────── + ctx.app.get('/api/storage/view/*', async (req, res) => { + const filePath = req.params[0]; + if (!filePath) + return res.status(400).send('Path missing'); + try { + // 1. Fallback local + const localPath = path_1.default.join(this.localFallbackDir, filePath); + if (fs_1.default.existsSync(localPath)) { + const ext = filePath.split('.').pop()?.toLowerCase() ?? ''; + res.setHeader('Content-Type', MIME_MAP[ext] || 'application/octet-stream'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + res.setHeader('X-Storage-Source', 'local_fallback'); + return res.send(fs_1.default.readFileSync(localPath)); + } + // 2. Wasabi + if (!this.s3 || !this.bucket) { + return res.status(503).send('Storage não inicializado'); + } + const result = await this.s3.send(new client_s3_1.GetObjectCommand({ + Bucket: this.bucket, + Key: filePath, + })); + const ext = filePath.split('.').pop()?.toLowerCase() ?? ''; + res.setHeader('Content-Type', result.ContentType || MIME_MAP[ext] || 'application/octet-stream'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + res.setHeader('X-Storage-Source', 'wasabi'); + if (result.Body) { + const bytes = await result.Body.transformToByteArray(); + return res.send(Buffer.from(bytes)); + } + return res.status(404).send('Body vazio'); + } + catch { + return res.status(404).send('Arquivo não encontrado'); + } + }); + // ── POST /api/storage/wasabi/list-buckets ────────────────────────────── + // Lista buckets usando credenciais do body (sem exigir S3 pré-configurado) + ctx.app.post('/api/storage/wasabi/list-buckets', async (req, res) => { + const { accessKey, secretKey, region, endpoint } = req.body; + if (!accessKey || !secretKey) { + return res.status(400).json({ error: 'Access Key e Secret Key são obrigatórios' }); + } + try { + const ep = (endpoint || 's3.wasabisys.com').startsWith('http') + ? endpoint || 's3.wasabisys.com' + : `https://${endpoint || 's3.wasabisys.com'}`; + const s3t = new client_s3_1.S3Client({ + region: region || 'us-east-1', + endpoint: ep, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + const data = await s3t.send(new client_s3_1.ListBucketsCommand({})); + const buckets = (data.Buckets ?? []).map(b => ({ name: b.Name, createdAt: b.CreationDate })); + return res.json({ buckets }); + } + catch (err) { + return res.status(400).json({ error: err.message }); + } + }); + // ── POST /api/storage/wasabi/create-bucket ───────────────────────────── + ctx.app.post('/api/storage/wasabi/create-bucket', async (req, res) => { + const { accessKey, secretKey, region, endpoint, name } = req.body; + if (!accessKey || !secretKey) + return res.status(400).json({ error: 'Credenciais obrigatórias' }); + if (!name) + return res.status(400).json({ error: 'Campo "name" obrigatório' }); + const safeName = name.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'); + const effectiveRegion = region || 'us-east-1'; + const ep = (endpoint || 's3.wasabisys.com').startsWith('http') + ? endpoint || 's3.wasabisys.com' + : `https://${endpoint || 's3.wasabisys.com'}`; + try { + const s3t = new client_s3_1.S3Client({ + region: effectiveRegion, + endpoint: ep, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + const params = { Bucket: safeName }; + if (effectiveRegion !== 'us-east-1') { + params.CreateBucketConfiguration = { LocationConstraint: effectiveRegion }; + } + await s3t.send(new client_s3_1.CreateBucketCommand(params)); + ctx.logger.info(`[Wasabi] Bucket criado: ${safeName}`); + return res.status(201).json({ ok: true, name: safeName }); + } + catch (err) { + return res.status(500).json({ error: err.message }); + } + }); + this.routesRegistered = true; + } + setInterval(() => { storageProvider.updateHealth(); }, 30000); + } + async deactivate(ctx) { + this.s3 = null; + ctx.logger.info('UnifiedStorageProvider desativado'); + } + async checkHealth() { + return (await this.checkDetailedHealth()).healthy; + } + async checkDetailedHealth() { + if (!this.s3 || !this.bucket) { + return { + healthy: false, + configured: false, + reason: 'not_configured', + message: 'Credenciais Wasabi não configuradas. Acesse Admin > Plugins > UnifiedStorageProvider.', + }; + } + try { + await this.s3.send(new client_s3_1.HeadBucketCommand({ Bucket: this.bucket })); + return { healthy: true, configured: true, reason: 'ok', message: `Wasabi OK — bucket: ${this.bucket}` }; + } + catch (err) { + const code = err?.Code || err?.code || err?.name || ''; + const httpStatus = err?.$metadata?.httpStatusCode ?? 0; + if (code === 'InvalidAccessKeyId' || code === 'SignatureDoesNotMatch') { + return { + healthy: false, configured: true, reason: 'auth_error', + message: 'Chaves de acesso inválidas. Verifique Access Key e Secret Key no painel do Wasabi.', + }; + } + if (httpStatus === 403 || code === 'AccessDenied') { + // 403 pode ser pagamento atrasado ou permissão negada + return { + healthy: false, configured: true, reason: 'billing_error', + message: 'Acesso negado ao Wasabi (HTTP 403). Verifique se a conta está ativa e o pagamento em dia.', + }; + } + if (httpStatus === 404 || code === 'NoSuchBucket') { + return { + healthy: false, configured: true, reason: 'bucket_not_found', + message: `Bucket "${this.bucket}" não encontrado. Verifique o nome do bucket no painel Wasabi.`, + }; + } + if (code === 'ENOTFOUND' || code === 'ECONNREFUSED' || code === 'NetworkingError') { + return { + healthy: false, configured: true, reason: 'network_error', + message: 'Sem conectividade com o Wasabi. Verifique a rede e o endpoint configurado.', + }; + } + return { + healthy: false, configured: true, reason: 'unknown', + message: `Erro ao verificar Wasabi: ${err?.message ?? code}`, + }; + } + } + async deletePrefix(prefix) { + let deletedCount = 0; + // 1. Limpa do Wasabi + if (this.s3 && this.bucket) { + try { + let isTruncated = true; + let continuationToken; + while (isTruncated) { + const listParams = { + Bucket: this.bucket, + Prefix: prefix, + MaxKeys: 1000, + }; + if (continuationToken) { + listParams.ContinuationToken = continuationToken; + } + const listResult = await this.s3.send(new client_s3_1.ListObjectsV2Command(listParams)); + const objects = listResult.Contents ?? []; + if (objects.length > 0) { + const deleteParams = { + Bucket: this.bucket, + Delete: { + Objects: objects.map((obj) => ({ Key: obj.Key })), + Quiet: true, + }, + }; + await this.s3.send(new client_s3_1.DeleteObjectsCommand(deleteParams)); + deletedCount += objects.length; + this.ctx.logger.info(`[UnifiedStorageProvider] ${objects.length} objetos deletados do Wasabi com prefixo ${prefix}`); + } + isTruncated = listResult.IsTruncated ?? false; + continuationToken = listResult.NextContinuationToken; + } + } + catch (err) { + this.ctx.logger.error(`[UnifiedStorageProvider] Erro ao deletar prefixo ${prefix} do Wasabi: ${err.message}`); + } + } + // 2. Limpa do fallback local + const localPath = path_1.default.join(this.localFallbackDir, prefix); + if (fs_1.default.existsSync(localPath)) { + try { + const stat = fs_1.default.statSync(localPath); + if (stat.isDirectory()) { + fs_1.default.rmSync(localPath, { recursive: true, force: true }); + this.ctx.logger.info(`[UnifiedStorageProvider] Pasta local do fallback removida: ${localPath}`); + } + else { + fs_1.default.unlinkSync(localPath); + this.ctx.logger.info(`[UnifiedStorageProvider] Arquivo local do fallback removido: ${localPath}`); + } + } + catch (err) { + this.ctx.logger.error(`[UnifiedStorageProvider] Erro ao remover fallback local para prefixo ${prefix}: ${err.message}`); + } + } + return { deleted: deletedCount }; + } + async upload(payload) { + const { category, file } = payload; + if (!VALID_CATEGORIES.includes(category)) + throw new Error(`Categoria inválida: ${category}`); + let buffer = file.buffer; + let originalName = file.originalname; + let mimetype = file.mimetype; + // Otimiza imagens grandes (>150 KB) para WebP + if (mimetype.startsWith('image/') && !mimetype.includes('webp') && buffer.length > 150 * 1024) { + try { + buffer = await (0, sharp_1.default)(buffer).resize(1200, 1200, { fit: 'inside', withoutEnlargement: true }).webp({ quality: 75 }).toBuffer(); + mimetype = 'image/webp'; + originalName = originalName.replace(/\.[^.]+$/, '.webp'); + this.ctx.logger.debug(`[Optimizer] ${originalName} convertida para webp`); + } + catch (err) { + this.ctx.logger.warn(`[Optimizer] falha: ${err.message}`); + } + } + const safeBase = originalName.replace(/[^a-zA-Z0-9._-]/g, '_'); + const fileName = `${Date.now()}-${safeBase}`; + // Path no bucket: customPath (quando fornecido) > whatsapp/{t}/{i}/{file} > {cat}/{file} + const relativePath = payload.customPath + ?? ((category === 'whatsapp' && payload.usuarioSis && payload.sessionJID) + ? `whatsapp/${payload.usuarioSis}/${payload.sessionJID}/${fileName}` + : `${category}/${fileName}`); + if (!this.s3 || !this.bucket) { + return this.saveToLocalFallback(relativePath, buffer, originalName, category); + } + try { + await new lib_storage_1.Upload({ + client: this.s3, + params: { + Bucket: this.bucket, + Key: relativePath, + Body: buffer, + ContentType: mimetype, + }, + }).done(); + this.ctx.logger.debug(`[Wasabi] upload ok: ${relativePath}`); + return { provider: 'wasabi', path: relativePath }; + } + catch (err) { + this.ctx.logger.error(`[Wasabi] upload falhou (${err.message}) — salvando local`); + return this.saveToLocalFallback(relativePath, buffer, originalName, category); + } + } + async getBuffer(relativePath) { + // 1. Fallback local + const localPath = path_1.default.join(this.localFallbackDir, relativePath); + if (fs_1.default.existsSync(localPath)) { + return fs_1.default.readFileSync(localPath); + } + // 2. Wasabi + if (!this.s3 || !this.bucket) + return null; + try { + const result = await this.s3.send(new client_s3_1.GetObjectCommand({ Bucket: this.bucket, Key: relativePath })); + if (!result.Body) + return null; + const bytes = await result.Body.transformToByteArray(); + return Buffer.from(bytes); + } + catch { + return null; + } + } + /** Salva buffer no diretório de fallback local, criando subpastas se necessário */ + saveToLocalFallback(relativePath, buffer, originalName, _category) { + const dest = path_1.default.join(this.localFallbackDir, relativePath); + fs_1.default.mkdirSync(path_1.default.dirname(dest), { recursive: true }); + fs_1.default.writeFileSync(dest, buffer); + this.ctx.logger.debug(`[Fallback] salvo local: ${dest}`); + return { provider: 'local_fallback', path: relativePath }; + } + /** Sincroniza arquivos do fallback local para o Wasabi a cada 2 minutos */ + startSyncWorker() { + setInterval(async () => { + if (!this.s3 || !this.bucket) + return; + await this.syncDir(this.localFallbackDir); + }, 120000); + } + async syncDir(dir) { + if (!fs_1.default.existsSync(dir)) + return; + for (const entry of fs_1.default.readdirSync(dir)) { + const fullPath = path_1.default.join(dir, entry); + const stat = fs_1.default.statSync(fullPath); + if (stat.isDirectory()) { + await this.syncDir(fullPath); + continue; + } + const relativePath = path_1.default.relative(this.localFallbackDir, fullPath).replace(/\\/g, '/'); + try { + await new lib_storage_1.Upload({ + client: this.s3, + params: { + Bucket: this.bucket, + Key: relativePath, + Body: fs_1.default.readFileSync(fullPath), + }, + }).done(); + fs_1.default.unlinkSync(fullPath); + this.ctx.logger.info(`[SyncWorker] enviado e removido: ${relativePath}`); + } + catch (err) { + this.ctx.logger.warn(`[SyncWorker] falha em ${relativePath}: ${err.message}`); + } + } + } +} +const instance = new UnifiedStorageProviderPlugin(); +exports.default = instance; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/plugins/UnifiedStorageProvider/index.js.map b/plugins/UnifiedStorageProvider/index.js.map new file mode 100644 index 0000000..7b93e19 --- /dev/null +++ b/plugins/UnifiedStorageProvider/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;;;AAAA,kDAQ4B;AAC5B,sDAA8C;AAI9C,IAAI,eAAoB,CAAC;AACzB,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY,EAAE,CAAC;IACxC,eAAe,GAAG,OAAO,CAAC,iCAAiC,CAAC,CAAC,eAAe,CAAC;AACjF,CAAC;KAAM,CAAC;IACJ,eAAe,GAAG,OAAO,CAAC,wCAAwC,CAAC,CAAC,eAAe,CAAC;AACxF,CAAC;AACD,kDAA0B;AAC1B,4CAAoB;AACpB,gDAAwB;AAExB,MAAM,gBAAgB,GAAG,CAAC,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;AAErG,2CAA2C;AAC3C,MAAM,QAAQ,GAA2B;IACrC,GAAG,EAAE,YAAY,EAAE,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW;IACvD,GAAG,EAAE,WAAW,EAAG,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,eAAe;IAC3D,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW;IAC1D,GAAG,EAAE,YAAY,EAAE,GAAG,EAAE,oBAAoB;IAC5C,IAAI,EAAE,yEAAyE;IAC/E,GAAG,EAAE,0BAA0B;IAC/B,IAAI,EAAE,mEAAmE;CAC5E,CAAC;AAEF,MAAM,4BAA4B;IAAlC;QAEY,OAAE,GAAoB,IAAI,CAAC;QAC3B,WAAM,GAAW,EAAE,CAAC;QACpB,WAAM,GAAW,WAAW,CAAC;QAC7B,qBAAgB,GAAW,cAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAAC;QAEpF,qBAAgB,GAAG,KAAK,CAAC;IA2XrC,CAAC;IAzXG,KAAK,CAAC,QAAQ,CAAC,GAAkB;QAC7B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,MAAM,GAAG,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,wBAAwB,CAAC,IAAI,EAAE,CAAC;QAE3D,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,IAAI,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACxC,YAAE,CAAC,SAAS,CAAC,IAAI,CAAC,gBAAgB,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC7D,CAAC;QAED,MAAM,SAAS,GAAG,GAAG,CAAC,eAAe,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QACvE,MAAM,SAAS,GAAG,GAAG,CAAC,eAAe,IAAI,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QACvE,IAAI,CAAC,MAAM,GAAK,CAAC,GAAG,CAAC,YAAY,IAAM,OAAO,CAAC,GAAG,CAAC,aAAa,IAAM,WAAW,CAAW,CAAC;QAC7F,IAAI,CAAC,MAAM,GAAK,CAAC,GAAG,CAAC,YAAY,IAAM,OAAO,CAAC,GAAG,CAAC,aAAa,IAAM,EAAE,CAAW,CAAC;QAEpF,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,cAAc,IAAI,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,kBAAkB,CAAW,CAAC;QACxG,MAAM,QAAQ,GAAM,WAAW,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,WAAW,EAAE,CAAC;QAE5F,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,iCAAiC,CAAC,CAAC;QAEjI,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAC3C,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,sFAAsF,CAAC,CAAC;QAC5G,CAAC;aAAM,CAAC;YACJ,IAAI,CAAC,EAAE,GAAG,IAAI,oBAAQ,CAAC;gBACnB,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,QAAQ;gBACR,WAAW,EAAE,EAAE,WAAW,EAAE,SAAmB,EAAE,eAAe,EAAE,SAAmB,EAAE;gBACvF,cAAc,EAAE,IAAI;aACvB,CAAC,CAAC;YAEH,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;YAC/B,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,0CAA0C,IAAI,CAAC,MAAM,cAAc,QAAQ,EAAE,CAAC,CAAC;YAE/F,IAAI,CAAC,eAAe,EAAE,CAAC;QAC3B,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC;YACzB,0EAA0E;YAC1E,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,qBAAqB,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;gBAClD,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC/B,IAAI,CAAC,QAAQ;oBAAE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;gBAE3D,IAAI,CAAC;oBACD,oBAAoB;oBACpB,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC;oBAC7D,IAAI,YAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;wBAC3B,MAAM,GAAG,GAAI,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;wBAC5D,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC,CAAC;wBAC3E,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,0BAA0B,CAAC,CAAC;wBAC3D,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,CAAC;wBACpD,OAAO,GAAG,CAAC,IAAI,CAAC,YAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAC;oBAChD,CAAC;oBAED,YAAY;oBACZ,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;wBAC3B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;oBAC5D,CAAC;oBAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,4BAAgB,CAAC;wBACnD,MAAM,EAAE,IAAI,CAAC,MAAM;wBACnB,GAAG,EAAE,QAAQ;qBAChB,CAAC,CAAC,CAAC;oBAEJ,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;oBAC3D,GAAG,CAAC,SAAS,CAAC,cAAc,EAAE,MAAM,CAAC,WAAW,IAAI,QAAQ,CAAC,GAAG,CAAC,IAAI,0BAA0B,CAAC,CAAC;oBACjG,GAAG,CAAC,SAAS,CAAC,eAAe,EAAE,0BAA0B,CAAC,CAAC;oBAC3D,GAAG,CAAC,SAAS,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;oBAE5C,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;wBACd,MAAM,KAAK,GAAG,MAAO,MAAM,CAAC,IAAY,CAAC,oBAAoB,EAAE,CAAC;wBAChE,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;oBACxC,CAAC;oBACD,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;gBAC9C,CAAC;gBAAC,MAAM,CAAC;oBACL,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAC;gBAC1D,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,0EAA0E;YAC1E,2EAA2E;YAC3E,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,kCAAkC,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;gBAChE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC,IAA8B,CAAC;gBACtF,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE,CAAC;oBAC3B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0CAA0C,EAAE,CAAC,CAAC;gBACvF,CAAC;gBACD,IAAI,CAAC;oBACD,MAAM,EAAE,GAAI,CAAC,QAAQ,IAAI,kBAAkB,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;wBAC3D,CAAC,CAAC,QAAQ,IAAI,kBAAkB;wBAChC,CAAC,CAAC,WAAW,QAAQ,IAAI,kBAAkB,EAAE,CAAC;oBAClD,MAAM,GAAG,GAAG,IAAI,oBAAQ,CAAC;wBACrB,MAAM,EAAE,MAAM,IAAI,WAAW;wBAC7B,QAAQ,EAAE,EAAE;wBACZ,WAAW,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE;wBACnE,cAAc,EAAE,IAAI;qBACvB,CAAC,CAAC;oBACH,MAAM,IAAI,GAAM,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,8BAAkB,CAAC,EAAE,CAAC,CAAC,CAAC;oBAC3D,MAAM,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;oBAC7F,OAAO,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;gBACjC,CAAC;gBAAC,OAAO,GAAQ,EAAE,CAAC;oBAChB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;gBACxD,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,0EAA0E;YAC1E,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,mCAAmC,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;gBACjE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,IAA8B,CAAC;gBAC5F,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS;oBAAE,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;gBACjG,IAAI,CAAC,IAAI;oBAAqB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;gBAEjG,MAAM,QAAQ,GAAS,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;gBAC7E,MAAM,eAAe,GAAG,MAAM,IAAI,WAAW,CAAC;gBAC9C,MAAM,EAAE,GAAG,CAAC,QAAQ,IAAI,kBAAkB,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;oBAC1D,CAAC,CAAC,QAAQ,IAAI,kBAAkB;oBAChC,CAAC,CAAC,WAAW,QAAQ,IAAI,kBAAkB,EAAE,CAAC;gBAElD,IAAI,CAAC;oBACD,MAAM,GAAG,GAAG,IAAI,oBAAQ,CAAC;wBACrB,MAAM,EAAE,eAAe;wBACvB,QAAQ,EAAE,EAAE;wBACZ,WAAW,EAAE,EAAE,WAAW,EAAE,SAAS,EAAE,eAAe,EAAE,SAAS,EAAE;wBACnE,cAAc,EAAE,IAAI;qBACvB,CAAC,CAAC;oBACH,MAAM,MAAM,GAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;oBACzC,IAAI,eAAe,KAAK,WAAW,EAAE,CAAC;wBAClC,MAAM,CAAC,yBAAyB,GAAG,EAAE,kBAAkB,EAAE,eAAe,EAAE,CAAC;oBAC/E,CAAC;oBACD,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,+BAAmB,CAAC,MAAM,CAAC,CAAC,CAAC;oBAChD,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,2BAA2B,QAAQ,EAAE,CAAC,CAAC;oBACvD,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;gBAC9D,CAAC;gBAAC,OAAO,GAAQ,EAAE,CAAC;oBAChB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;gBACxD,CAAC;YACL,CAAC,CAAC,CAAC;YAEH,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC;QACjC,CAAC;QAED,WAAW,CAAC,GAAG,EAAE,GAAG,eAAe,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;IAClE,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAkB;QAC/B,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC;QACf,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;IACzD,CAAC;IAED,KAAK,CAAC,WAAW;QACb,OAAO,CAAC,MAAM,IAAI,CAAC,mBAAmB,EAAE,CAAC,CAAC,OAAO,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,mBAAmB;QACrB,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAC3B,OAAO;gBACH,OAAO,EAAE,KAAK;gBACd,UAAU,EAAE,KAAK;gBACjB,MAAM,EAAE,gBAAgB;gBACxB,OAAO,EAAE,uFAAuF;aACnG,CAAC;QACN,CAAC;QACD,IAAI,CAAC;YACD,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,6BAAiB,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;YACnE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,uBAAuB,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;QAC5G,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAChB,MAAM,IAAI,GAAW,GAAG,EAAE,IAAI,IAAI,GAAG,EAAE,IAAI,IAAI,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC;YAC/D,MAAM,UAAU,GAAW,GAAG,EAAE,SAAS,EAAE,cAAc,IAAI,CAAC,CAAC;YAE/D,IAAI,IAAI,KAAK,oBAAoB,IAAI,IAAI,KAAK,uBAAuB,EAAE,CAAC;gBACpE,OAAO;oBACH,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,YAAY;oBACtD,OAAO,EAAE,oFAAoF;iBAChG,CAAC;YACN,CAAC;YACD,IAAI,UAAU,KAAK,GAAG,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;gBAChD,sDAAsD;gBACtD,OAAO;oBACH,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe;oBACzD,OAAO,EAAE,2FAA2F;iBACvG,CAAC;YACN,CAAC;YACD,IAAI,UAAU,KAAK,GAAG,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;gBAChD,OAAO;oBACH,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,kBAAkB;oBAC5D,OAAO,EAAE,WAAW,IAAI,CAAC,MAAM,gEAAgE;iBAClG,CAAC;YACN,CAAC;YACD,IAAI,IAAI,KAAK,WAAW,IAAI,IAAI,KAAK,cAAc,IAAI,IAAI,KAAK,iBAAiB,EAAE,CAAC;gBAChF,OAAO;oBACH,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe;oBACzD,OAAO,EAAE,4EAA4E;iBACxF,CAAC;YACN,CAAC;YACD,OAAO;gBACH,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS;gBACnD,OAAO,EAAE,6BAA6B,GAAG,EAAE,OAAO,IAAI,IAAI,EAAE;aAC/D,CAAC;QACN,CAAC;IACL,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,MAAc;QAC7B,IAAI,YAAY,GAAG,CAAC,CAAC;QAErB,qBAAqB;QACrB,IAAI,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACzB,IAAI,CAAC;gBACD,IAAI,WAAW,GAAG,IAAI,CAAC;gBACvB,IAAI,iBAAqC,CAAC;gBAE1C,OAAO,WAAW,EAAE,CAAC;oBACjB,MAAM,UAAU,GAAQ;wBACpB,MAAM,EAAE,IAAI,CAAC,MAAM;wBACnB,MAAM,EAAE,MAAM;wBACd,OAAO,EAAE,IAAI;qBAChB,CAAC;oBACF,IAAI,iBAAiB,EAAE,CAAC;wBACpB,UAAU,CAAC,iBAAiB,GAAG,iBAAiB,CAAC;oBACrD,CAAC;oBAED,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,gCAAoB,CAAC,UAAU,CAAC,CAAC,CAAC;oBAC5E,MAAM,OAAO,GAAG,UAAU,CAAC,QAAQ,IAAI,EAAE,CAAC;oBAE1C,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;wBACrB,MAAM,YAAY,GAAG;4BACjB,MAAM,EAAE,IAAI,CAAC,MAAM;4BACnB,MAAM,EAAE;gCACJ,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,GAAI,EAAE,CAAC,CAAC;gCAClD,KAAK,EAAE,IAAI;6BACd;yBACJ,CAAC;wBACF,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,gCAAoB,CAAC,YAAY,CAAC,CAAC,CAAC;wBAC3D,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;wBAC/B,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,4BAA4B,OAAO,CAAC,MAAM,4CAA4C,MAAM,EAAE,CAAC,CAAC;oBACzH,CAAC;oBAED,WAAW,GAAG,UAAU,CAAC,WAAW,IAAI,KAAK,CAAC;oBAC9C,iBAAiB,GAAG,UAAU,CAAC,qBAAqB,CAAC;gBACzD,CAAC;YACL,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAChB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,oDAAoD,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YAClH,CAAC;QACL,CAAC;QAED,6BAA6B;QAC7B,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,CAAC;QAC3D,IAAI,YAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACD,MAAM,IAAI,GAAG,YAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;gBACpC,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;oBACrB,YAAE,CAAC,MAAM,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;oBACvD,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,8DAA8D,SAAS,EAAE,CAAC,CAAC;gBACpG,CAAC;qBAAM,CAAC;oBACJ,YAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;oBACzB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,gEAAgE,SAAS,EAAE,CAAC,CAAC;gBACtG,CAAC;YACL,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAChB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,wEAAwE,MAAM,KAAK,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YAC5H,CAAC;QACL,CAAC;QAED,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;IACrC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,OAA6B;QACtC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC;QACnC,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,uBAAuB,QAAQ,EAAE,CAAC,CAAC;QAE7F,IAAI,MAAM,GAAQ,IAAI,CAAC,MAAM,CAAC;QAC9B,IAAI,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACrC,IAAI,QAAQ,GAAM,IAAI,CAAC,QAAQ,CAAC;QAEhC,8CAA8C;QAC9C,IAAI,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,GAAG,GAAG,IAAI,EAAE,CAAC;YAC5F,IAAI,CAAC;gBACD,MAAM,GAAQ,MAAM,IAAA,eAAK,EAAC,MAAM,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,kBAAkB,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;gBACnI,QAAQ,GAAM,YAAY,CAAC;gBAC3B,YAAY,GAAG,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;gBACzD,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,YAAY,uBAAuB,CAAC,CAAC;YAC9E,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAChB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,sBAAsB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YAC9D,CAAC;QACL,CAAC;QAED,MAAM,QAAQ,GAAK,YAAY,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;QACjE,MAAM,QAAQ,GAAK,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,QAAQ,EAAE,CAAC;QAE/C,yFAAyF;QACzF,MAAM,YAAY,GAAG,OAAO,CAAC,UAAU;eAChC,CAAC,CAAC,QAAQ,KAAK,UAAU,IAAI,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,UAAU,CAAC;gBACrE,CAAC,CAAC,YAAY,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,UAAU,IAAI,QAAQ,EAAE;gBACpE,CAAC,CAAC,GAAG,QAAQ,IAAI,QAAQ,EAAE,CAAC,CAAC;QAErC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAC3B,OAAO,IAAI,CAAC,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;QAClF,CAAC;QAED,IAAI,CAAC;YACD,MAAM,IAAI,oBAAM,CAAC;gBACb,MAAM,EAAE,IAAI,CAAC,EAAE;gBACf,MAAM,EAAE;oBACJ,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,GAAG,EAAK,YAAY;oBACpB,IAAI,EAAI,MAAM;oBACd,WAAW,EAAE,QAAQ;iBACxB;aACJ,CAAC,CAAC,IAAI,EAAE,CAAC;YAEV,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,uBAAuB,YAAY,EAAE,CAAC,CAAC;YAC7D,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;QAEtD,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAChB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC;YAClF,OAAO,IAAI,CAAC,mBAAmB,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,QAAQ,CAAC,CAAC;QAClF,CAAC;IACL,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,YAAoB;QAChC,oBAAoB;QACpB,MAAM,SAAS,GAAG,cAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAC;QACjE,IAAI,YAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC3B,OAAO,YAAE,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;QACtC,CAAC;QACD,YAAY;QACZ,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAC1C,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,4BAAgB,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC;YACpG,IAAI,CAAC,MAAM,CAAC,IAAI;gBAAE,OAAO,IAAI,CAAC;YAC9B,MAAM,KAAK,GAAG,MAAO,MAAM,CAAC,IAAY,CAAC,oBAAoB,EAAE,CAAC;YAChE,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;QAAC,MAAM,CAAC;YACL,OAAO,IAAI,CAAC;QAChB,CAAC;IACL,CAAC;IAED,mFAAmF;IAC3E,mBAAmB,CAAC,YAAoB,EAAE,MAAc,EAAE,YAAoB,EAAE,SAAiB;QACrG,MAAM,IAAI,GAAG,cAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,YAAY,CAAC,CAAC;QAC5D,YAAE,CAAC,SAAS,CAAC,cAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,YAAE,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,IAAI,EAAE,CAAC,CAAC;QACzD,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAC9D,CAAC;IAED,2EAA2E;IACnE,eAAe;QACnB,WAAW,CAAC,KAAK,IAAI,EAAE;YACnB,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM;gBAAE,OAAO;YACrC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAC9C,CAAC,EAAE,MAAO,CAAC,CAAC;IAChB,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,GAAW;QAC7B,IAAI,CAAC,YAAE,CAAC,UAAU,CAAC,GAAG,CAAC;YAAE,OAAO;QAEhC,KAAK,MAAM,KAAK,IAAI,YAAE,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC;YACtC,MAAM,QAAQ,GAAG,cAAI,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YACvC,MAAM,IAAI,GAAO,YAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;YAEvC,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;gBACrB,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAC7B,SAAS;YACb,CAAC;YAED,MAAM,YAAY,GAAG,cAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;YAExF,IAAI,CAAC;gBACD,MAAM,IAAI,oBAAM,CAAC;oBACb,MAAM,EAAE,IAAI,CAAC,EAAG;oBAChB,MAAM,EAAE;wBACJ,MAAM,EAAE,IAAI,CAAC,MAAM;wBACnB,GAAG,EAAK,YAAY;wBACpB,IAAI,EAAI,YAAE,CAAC,YAAY,CAAC,QAAQ,CAAC;qBACpC;iBACJ,CAAC,CAAC,IAAI,EAAE,CAAC;gBAEV,YAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;gBACxB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,oCAAoC,YAAY,EAAE,CAAC,CAAC;YAC7E,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAChB,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,YAAY,KAAK,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YAClF,CAAC;QACL,CAAC;IACL,CAAC;CACJ;AAED,MAAM,QAAQ,GAAG,IAAI,4BAA4B,EAAE,CAAC;AACpD,kBAAe,QAAQ,CAAC"} \ No newline at end of file diff --git a/plugins/UnifiedStorageProvider/index.ts b/plugins/UnifiedStorageProvider/index.ts new file mode 100644 index 0000000..bf39be5 --- /dev/null +++ b/plugins/UnifiedStorageProvider/index.ts @@ -0,0 +1,426 @@ +import { + S3Client, + GetObjectCommand, + HeadBucketCommand, + ListObjectsV2Command, + ListBucketsCommand, + CreateBucketCommand, + DeleteObjectsCommand, +} from '@aws-sdk/client-s3'; +import { Upload } from '@aws-sdk/lib-storage'; +import { PluginInstance, PluginContext } from '../../backend/src/core/types'; +import type { StorageUploadPayload, StorageProviderInterface } from '../../backend/src/core/StorageProvider'; + +let storageProvider: any; +if (process.env.NODE_ENV === 'production') { + storageProvider = require('../../dist/core/StorageProvider').storageProvider; +} else { + storageProvider = require('../../backend/src/core/StorageProvider').storageProvider; +} +import sharp from 'sharp'; +import fs from 'fs'; +import path from 'path'; + +const VALID_CATEGORIES = ['documents', 'exports', 'posts', 'cards', 'partners', 'whatsapp', 'media']; + +// Mapa de extensão → mimetype para o proxy +const MIME_MAP: Record = { + jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', + gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', + pdf: 'application/pdf', mp4: 'video/mp4', ogg: 'audio/ogg', + mp3: 'audio/mpeg', doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', +}; + +class UnifiedStorageProviderPlugin implements PluginInstance, StorageProviderInterface { + manifest: any; + private s3: S3Client | null = null; + private bucket: string = ''; + private region: string = 'us-east-1'; + private localFallbackDir: string = path.resolve(process.cwd(), 'storage', 'local_fallback'); + private ctx!: PluginContext; + private routesRegistered = false; + + async activate(ctx: PluginContext): Promise { + this.ctx = ctx; + const cfg = ctx.config.get('UnifiedStorageProvider') || {}; + + if (!fs.existsSync(this.localFallbackDir)) { + fs.mkdirSync(this.localFallbackDir, { recursive: true }); + } + + const accessKey = cfg.wasabiAccessKey || process.env.WASABI_ACCESS_KEY; + const secretKey = cfg.wasabiSecretKey || process.env.WASABI_SECRET_KEY; + this.region = (cfg.wasabiRegion || process.env.WASABI_REGION || 'us-east-1') as string; + this.bucket = (cfg.wasabiBucket || process.env.WASABI_BUCKET || '') as string; + + const rawEndpoint = (cfg.wasabiEndpoint || process.env.WASABI_ENDPOINT || 's3.wasabisys.com') as string; + const endpoint = rawEndpoint.startsWith('http') ? rawEndpoint : `https://${rawEndpoint}`; + + ctx.logger.info({ cfgKeys: Object.keys(cfg), hasKey: !!accessKey, hasBucket: !!this.bucket }, '[UnifiedStorage] activate debug'); + + if (!accessKey || !secretKey || !this.bucket) { + ctx.logger.warn('UnifiedStorageProvider: credenciais ou bucket não configurados — configure via admin'); + } else { + this.s3 = new S3Client({ + region: this.region, + endpoint, + credentials: { accessKeyId: accessKey as string, secretAccessKey: secretKey as string }, + forcePathStyle: true, + }); + + storageProvider.register(this); + ctx.logger.info(`UnifiedStorageProvider ativo — bucket: ${this.bucket} endpoint: ${endpoint}`); + + this.startSyncWorker(); + } + + if (!this.routesRegistered) { + // ── Proxy: serve arquivos do Wasabi (ou fallback local) ──────────────── + ctx.app.get('/api/storage/view/*', async (req, res) => { + const filePath = req.params[0]; + if (!filePath) return res.status(400).send('Path missing'); + + try { + // 1. Fallback local + const localPath = path.join(this.localFallbackDir, filePath); + if (fs.existsSync(localPath)) { + const ext = filePath.split('.').pop()?.toLowerCase() ?? ''; + res.setHeader('Content-Type', MIME_MAP[ext] || 'application/octet-stream'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + res.setHeader('X-Storage-Source', 'local_fallback'); + return res.send(fs.readFileSync(localPath)); + } + + // 2. Wasabi + if (!this.s3 || !this.bucket) { + return res.status(503).send('Storage não inicializado'); + } + + const result = await this.s3.send(new GetObjectCommand({ + Bucket: this.bucket, + Key: filePath, + })); + + const ext = filePath.split('.').pop()?.toLowerCase() ?? ''; + res.setHeader('Content-Type', result.ContentType || MIME_MAP[ext] || 'application/octet-stream'); + res.setHeader('Cache-Control', 'public, max-age=31536000'); + res.setHeader('X-Storage-Source', 'wasabi'); + + if (result.Body) { + const bytes = await (result.Body as any).transformToByteArray(); + return res.send(Buffer.from(bytes)); + } + return res.status(404).send('Body vazio'); + } catch { + return res.status(404).send('Arquivo não encontrado'); + } + }); + + // ── POST /api/storage/wasabi/list-buckets ────────────────────────────── + // Lista buckets usando credenciais do body (sem exigir S3 pré-configurado) + ctx.app.post('/api/storage/wasabi/list-buckets', async (req, res) => { + const { accessKey, secretKey, region, endpoint } = req.body as Record; + if (!accessKey || !secretKey) { + return res.status(400).json({ error: 'Access Key e Secret Key são obrigatórios' }); + } + try { + const ep = (endpoint || 's3.wasabisys.com').startsWith('http') + ? endpoint || 's3.wasabisys.com' + : `https://${endpoint || 's3.wasabisys.com'}`; + const s3t = new S3Client({ + region: region || 'us-east-1', + endpoint: ep, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + const data = await s3t.send(new ListBucketsCommand({})); + const buckets = (data.Buckets ?? []).map(b => ({ name: b.Name, createdAt: b.CreationDate })); + return res.json({ buckets }); + } catch (err: any) { + return res.status(400).json({ error: err.message }); + } + }); + + // ── POST /api/storage/wasabi/create-bucket ───────────────────────────── + ctx.app.post('/api/storage/wasabi/create-bucket', async (req, res) => { + const { accessKey, secretKey, region, endpoint, name } = req.body as Record; + if (!accessKey || !secretKey) return res.status(400).json({ error: 'Credenciais obrigatórias' }); + if (!name) return res.status(400).json({ error: 'Campo "name" obrigatório' }); + + const safeName = name.trim().toLowerCase().replace(/[^a-z0-9-]/g, '-'); + const effectiveRegion = region || 'us-east-1'; + const ep = (endpoint || 's3.wasabisys.com').startsWith('http') + ? endpoint || 's3.wasabisys.com' + : `https://${endpoint || 's3.wasabisys.com'}`; + + try { + const s3t = new S3Client({ + region: effectiveRegion, + endpoint: ep, + credentials: { accessKeyId: accessKey, secretAccessKey: secretKey }, + forcePathStyle: true, + }); + const params: any = { Bucket: safeName }; + if (effectiveRegion !== 'us-east-1') { + params.CreateBucketConfiguration = { LocationConstraint: effectiveRegion }; + } + await s3t.send(new CreateBucketCommand(params)); + ctx.logger.info(`[Wasabi] Bucket criado: ${safeName}`); + return res.status(201).json({ ok: true, name: safeName }); + } catch (err: any) { + return res.status(500).json({ error: err.message }); + } + }); + + this.routesRegistered = true; + } + + setInterval(() => { storageProvider.updateHealth(); }, 30000); + } + + async deactivate(ctx: PluginContext): Promise { + this.s3 = null; + ctx.logger.info('UnifiedStorageProvider desativado'); + } + + async checkHealth(): Promise { + return (await this.checkDetailedHealth()).healthy; + } + + async checkDetailedHealth(): Promise { + if (!this.s3 || !this.bucket) { + return { + healthy: false, + configured: false, + reason: 'not_configured', + message: 'Credenciais Wasabi não configuradas. Acesse Admin > Plugins > UnifiedStorageProvider.', + }; + } + try { + await this.s3.send(new HeadBucketCommand({ Bucket: this.bucket })); + return { healthy: true, configured: true, reason: 'ok', message: `Wasabi OK — bucket: ${this.bucket}` }; + } catch (err: any) { + const code: string = err?.Code || err?.code || err?.name || ''; + const httpStatus: number = err?.$metadata?.httpStatusCode ?? 0; + + if (code === 'InvalidAccessKeyId' || code === 'SignatureDoesNotMatch') { + return { + healthy: false, configured: true, reason: 'auth_error', + message: 'Chaves de acesso inválidas. Verifique Access Key e Secret Key no painel do Wasabi.', + }; + } + if (httpStatus === 403 || code === 'AccessDenied') { + // 403 pode ser pagamento atrasado ou permissão negada + return { + healthy: false, configured: true, reason: 'billing_error', + message: 'Acesso negado ao Wasabi (HTTP 403). Verifique se a conta está ativa e o pagamento em dia.', + }; + } + if (httpStatus === 404 || code === 'NoSuchBucket') { + return { + healthy: false, configured: true, reason: 'bucket_not_found', + message: `Bucket "${this.bucket}" não encontrado. Verifique o nome do bucket no painel Wasabi.`, + }; + } + if (code === 'ENOTFOUND' || code === 'ECONNREFUSED' || code === 'NetworkingError') { + return { + healthy: false, configured: true, reason: 'network_error', + message: 'Sem conectividade com o Wasabi. Verifique a rede e o endpoint configurado.', + }; + } + return { + healthy: false, configured: true, reason: 'unknown', + message: `Erro ao verificar Wasabi: ${err?.message ?? code}`, + }; + } + } + + async deletePrefix(prefix: string): Promise<{ deleted: number }> { + let deletedCount = 0; + + // 1. Limpa do Wasabi + if (this.s3 && this.bucket) { + try { + let isTruncated = true; + let continuationToken: string | undefined; + + while (isTruncated) { + const listParams: any = { + Bucket: this.bucket, + Prefix: prefix, + MaxKeys: 1000, + }; + if (continuationToken) { + listParams.ContinuationToken = continuationToken; + } + + const listResult = await this.s3.send(new ListObjectsV2Command(listParams)); + const objects = listResult.Contents ?? []; + + if (objects.length > 0) { + const deleteParams = { + Bucket: this.bucket, + Delete: { + Objects: objects.map((obj) => ({ Key: obj.Key! })), + Quiet: true, + }, + }; + await this.s3.send(new DeleteObjectsCommand(deleteParams)); + deletedCount += objects.length; + this.ctx.logger.info(`[UnifiedStorageProvider] ${objects.length} objetos deletados do Wasabi com prefixo ${prefix}`); + } + + isTruncated = listResult.IsTruncated ?? false; + continuationToken = listResult.NextContinuationToken; + } + } catch (err: any) { + this.ctx.logger.error(`[UnifiedStorageProvider] Erro ao deletar prefixo ${prefix} do Wasabi: ${err.message}`); + } + } + + // 2. Limpa do fallback local + const localPath = path.join(this.localFallbackDir, prefix); + if (fs.existsSync(localPath)) { + try { + const stat = fs.statSync(localPath); + if (stat.isDirectory()) { + fs.rmSync(localPath, { recursive: true, force: true }); + this.ctx.logger.info(`[UnifiedStorageProvider] Pasta local do fallback removida: ${localPath}`); + } else { + fs.unlinkSync(localPath); + this.ctx.logger.info(`[UnifiedStorageProvider] Arquivo local do fallback removido: ${localPath}`); + } + } catch (err: any) { + this.ctx.logger.error(`[UnifiedStorageProvider] Erro ao remover fallback local para prefixo ${prefix}: ${err.message}`); + } + } + + return { deleted: deletedCount }; + } + + async upload(payload: StorageUploadPayload): Promise<{ provider: string; path: string }> { + const { category, file } = payload; + if (!VALID_CATEGORIES.includes(category)) throw new Error(`Categoria inválida: ${category}`); + + let buffer = file.buffer; + let originalName = file.originalname; + let mimetype = file.mimetype; + + // Otimiza imagens grandes (>150 KB) para WebP + if (mimetype.startsWith('image/') && !mimetype.includes('webp') && buffer.length > 150 * 1024) { + try { + buffer = await sharp(buffer).resize(1200, 1200, { fit: 'inside', withoutEnlargement: true }).webp({ quality: 75 }).toBuffer(); + mimetype = 'image/webp'; + originalName = originalName.replace(/\.[^.]+$/, '.webp'); + this.ctx.logger.debug(`[Optimizer] ${originalName} convertida para webp`); + } catch (err: any) { + this.ctx.logger.warn(`[Optimizer] falha: ${err.message}`); + } + } + + const safeBase = originalName.replace(/[^a-zA-Z0-9._-]/g, '_'); + const fileName = `${Date.now()}-${safeBase}`; + + // Path no bucket: customPath (quando fornecido) > whatsapp/{t}/{i}/{file} > {cat}/{file} + const relativePath = payload.customPath + ?? ((category === 'whatsapp' && payload.usuarioSis && payload.sessionJID) + ? `whatsapp/${payload.usuarioSis}/${payload.sessionJID}/${fileName}` + : `${category}/${fileName}`); + + if (!this.s3 || !this.bucket) { + return this.saveToLocalFallback(relativePath, buffer, originalName, category); + } + + try { + await new Upload({ + client: this.s3, + params: { + Bucket: this.bucket, + Key: relativePath, + Body: buffer, + ContentType: mimetype, + }, + }).done(); + + this.ctx.logger.debug(`[Wasabi] upload ok: ${relativePath}`); + return { provider: 'wasabi', path: relativePath }; + + } catch (err: any) { + this.ctx.logger.error(`[Wasabi] upload falhou (${err.message}) — salvando local`); + return this.saveToLocalFallback(relativePath, buffer, originalName, category); + } + } + + async getBuffer(relativePath: string): Promise { + // 1. Fallback local + const localPath = path.join(this.localFallbackDir, relativePath); + if (fs.existsSync(localPath)) { + return fs.readFileSync(localPath); + } + // 2. Wasabi + if (!this.s3 || !this.bucket) return null; + try { + const result = await this.s3.send(new GetObjectCommand({ Bucket: this.bucket, Key: relativePath })); + if (!result.Body) return null; + const bytes = await (result.Body as any).transformToByteArray(); + return Buffer.from(bytes); + } catch { + return null; + } + } + + /** Salva buffer no diretório de fallback local, criando subpastas se necessário */ + private saveToLocalFallback(relativePath: string, buffer: Buffer, originalName: string, _category: string): { provider: string; path: string } { + const dest = path.join(this.localFallbackDir, relativePath); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.writeFileSync(dest, buffer); + this.ctx.logger.debug(`[Fallback] salvo local: ${dest}`); + return { provider: 'local_fallback', path: relativePath }; + } + + /** Sincroniza arquivos do fallback local para o Wasabi a cada 2 minutos */ + private startSyncWorker(): void { + setInterval(async () => { + if (!this.s3 || !this.bucket) return; + await this.syncDir(this.localFallbackDir); + }, 120_000); + } + + private async syncDir(dir: string): Promise { + if (!fs.existsSync(dir)) return; + + for (const entry of fs.readdirSync(dir)) { + const fullPath = path.join(dir, entry); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + await this.syncDir(fullPath); + continue; + } + + const relativePath = path.relative(this.localFallbackDir, fullPath).replace(/\\/g, '/'); + + try { + await new Upload({ + client: this.s3!, + params: { + Bucket: this.bucket, + Key: relativePath, + Body: fs.readFileSync(fullPath), + }, + }).done(); + + fs.unlinkSync(fullPath); + this.ctx.logger.info(`[SyncWorker] enviado e removido: ${relativePath}`); + } catch (err: any) { + this.ctx.logger.warn(`[SyncWorker] falha em ${relativePath}: ${err.message}`); + } + } + } +} + +const instance = new UnifiedStorageProviderPlugin(); +export default instance; diff --git a/plugins/UnifiedStorageProvider/manifest.json b/plugins/UnifiedStorageProvider/manifest.json new file mode 100644 index 0000000..50d9af6 --- /dev/null +++ b/plugins/UnifiedStorageProvider/manifest.json @@ -0,0 +1,22 @@ +{ + "name": "UnifiedStorageProvider", + "displayName": "Wasabi Storage", + "version": "1.1.0", + "description": "Armazenamento de mídias do WhatsApp via Wasabi (S3 compatível)", + "author": "NewWhats", + "category": "core", + "enabled": true, + "canDisable": false, + "dependencies": [], + "backend": { + "routePrefix": "/api/storage", + "hasMigrations": false + }, + "configSchema": [ + { "key": "wasabiAccessKey", "label": "Access Key", "type": "password", "required": true, "group": "Credenciais Wasabi", "tooltip": "Chave de acesso gerada em Account → Access Keys no painel Wasabi." }, + { "key": "wasabiSecretKey", "label": "Secret Key", "type": "password", "required": true, "group": "Credenciais Wasabi", "tooltip": "Chave secreta correspondente à Access Key — exibida só uma vez no momento da criação." }, + { "key": "wasabiBucket", "label": "Bucket Ativo", "type": "text", "required": true, "group": "Bucket", "tooltip": "Nome do bucket onde as mídias do WhatsApp serão armazenadas." }, + { "key": "wasabiRegion", "label": "Region", "type": "text", "required": false, "group": "Avançado", "tooltip": "Região do bucket. Padrão: us-east-1. Exemplos: us-east-2, eu-central-1." }, + { "key": "wasabiEndpoint", "label": "Endpoint", "type": "text", "required": false, "group": "Avançado", "tooltip": "Endpoint S3. Padrão: s3.wasabisys.com. Só altere se usar região diferente." } + ] +} diff --git a/plugins/config.js b/plugins/config.js new file mode 100644 index 0000000..efcb9d5 --- /dev/null +++ b/plugins/config.js @@ -0,0 +1,43 @@ +'use strict' + +const { execute, queryOne, query } = require('../database/postgres') + +class PluginConfigStore { + constructor(pluginId) { + this.pluginId = pluginId + } + + async get(key) { + const row = await queryOne( + 'SELECT value FROM plugin_configs WHERE plugin_id = $1 AND key = $2', + [this.pluginId, key] + ) + return row ? row.value : null + } + + async set(key, value) { + const val = value === null ? null : String(value) + await execute( + `INSERT INTO plugin_configs (plugin_id, key, value) VALUES ($1, $2, $3) + ON CONFLICT (plugin_id, key) DO UPDATE SET value = EXCLUDED.value`, + [this.pluginId, key, val] + ) + } + + async getAll() { + const rows = await query( + 'SELECT key, value FROM plugin_configs WHERE plugin_id = $1', + [this.pluginId] + ) + return Object.fromEntries(rows.map((r) => [r.key, r.value])) + } + + async delete(key) { + await execute( + 'DELETE FROM plugin_configs WHERE plugin_id = $1 AND key = $2', + [this.pluginId, key] + ) + } +} + +module.exports = PluginConfigStore diff --git a/plugins/core-auth/backend/migrations/001_auth_tables.sql b/plugins/core-auth/backend/migrations/001_auth_tables.sql new file mode 100644 index 0000000..1a91b0a --- /dev/null +++ b/plugins/core-auth/backend/migrations/001_auth_tables.sql @@ -0,0 +1,47 @@ +-- core-auth migration: ensure users table has auth columns +-- This is idempotent — checks before altering + +-- Ensure password_hash column exists +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'password_hash'); +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE users ADD COLUMN password_hash VARCHAR(255) NOT NULL DEFAULT ""', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Ensure email column exists +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'email'); +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE users ADD COLUMN email VARCHAR(255) UNIQUE', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Ensure role column exists +SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND COLUMN_NAME = 'role'); +SET @sql = IF(@col_exists = 0, + 'ALTER TABLE users ADD COLUMN role VARCHAR(50) NOT NULL DEFAULT "user"', + 'SELECT 1'); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- Create audit_log table if not exists +CREATE TABLE IF NOT EXISTS audit_log ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NULL, + action VARCHAR(100) NOT NULL, + entity_type VARCHAR(100), + entity_id INT NULL, + details TEXT, + ip_address VARCHAR(45), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_audit_user (user_id), + INDEX idx_audit_action (action), + INDEX idx_audit_created (created_at) +) diff --git a/plugins/core-auth/backend/routes.js b/plugins/core-auth/backend/routes.js new file mode 100644 index 0000000..7ea96c7 --- /dev/null +++ b/plugins/core-auth/backend/routes.js @@ -0,0 +1,148 @@ +"use strict"; +// ============================================================ +// Plugin: core-auth — Routes +// ============================================================ +// Migrated from: backend/src/routes/auth.routes.ts +// backend/src/controllers/auth.controller.ts +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.createAuthRoutes = createAuthRoutes; +const express_1 = require("express"); +const bcryptjs_1 = __importDefault(require("bcryptjs")); +const jsonwebtoken_1 = __importDefault(require("jsonwebtoken")); +function createAuthRoutes(ctx) { + const router = (0, express_1.Router)(); + const { db, hooks } = ctx; + // ── POST /login ─────────────────────────────────────── + router.post('/login', async (req, res) => { + try { + const { email, password } = req.body; + if (!email || !password) { + return res.status(400).json({ error: 'Email e senha são obrigatórios' }); + } + const user = await db('users').where({ email }).first(); + if (!user) { + return res.status(401).json({ error: 'Credenciais inválidas' }); + } + const isValid = await bcryptjs_1.default.compare(password, user.password_hash); + if (!isValid) { + return res.status(401).json({ error: 'Credenciais inválidas' }); + } + const accessToken = jsonwebtoken_1.default.sign({ id: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET, { expiresIn: (process.env.JWT_EXPIRES_IN || '15m') }); + const refreshToken = jsonwebtoken_1.default.sign({ id: user.id, type: 'refresh' }, process.env.JWT_REFRESH_SECRET, { expiresIn: (process.env.JWT_REFRESH_EXPIRES_IN || '7d') }); + // Emit hook event + await hooks.emit('user:login', { userId: user.id, ip: req.ip }); + // Audit log + await db('audit_log').insert({ + user_id: user.id, + action: 'LOGIN', + entity_type: 'user', + entity_id: user.id, + details: 'Usuário logou no sistema', + ip_address: req.ip, + }).catch(() => { }); // non-critical + const { password_hash, ...safeUser } = user; + res.json({ user: safeUser, accessToken, refreshToken }); + } + catch (err) { + ctx.logger.error(`Login error: ${err.message}`); + res.status(500).json({ error: 'Erro interno no servidor' }); + } + }); + // ── POST /register ──────────────────────────────────── + router.post('/register', async (req, res) => { + try { + const { name, email, password, cpf, whatsapp, city, neighborhood, state } = req.body; + if (!name || !email || !password) { + return res.status(400).json({ error: 'Nome, email e senha são obrigatórios' }); + } + const existing = await db('users').where({ email }).first(); + if (existing) { + return res.status(409).json({ error: 'Email já cadastrado' }); + } + const password_hash = await bcryptjs_1.default.hash(password, 12); + const [id] = await db('users').insert({ + name, + email, + password_hash, + cpf: cpf || null, + whatsapp: whatsapp || null, + city: city || null, + neighborhood: neighborhood || null, + state: state || null, + role: 'user', + status: 'active', + }); + await hooks.emit('user:register', { userId: id, email }); + res.status(201).json({ id, name, email, role: 'user' }); + } + catch (err) { + ctx.logger.error(`Register error: ${err.message}`); + res.status(500).json({ error: 'Erro interno no servidor' }); + } + }); + // ── GET /me ─────────────────────────────────────────── + router.get('/me', async (req, res) => { + try { + const authHeader = req.headers.authorization; + if (!authHeader?.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Token não fornecido' }); + } + const token = authHeader.split(' ')[1]; + const payload = jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET); + const user = await db('users') + .where({ id: payload.id }) + .select('id', 'name', 'email', 'role', 'status', 'whatsapp', 'cpf', 'city', 'neighborhood', 'state', 'partner_id', 'created_at') + .first(); + if (!user) { + return res.status(404).json({ error: 'Usuário não encontrado' }); + } + let partnerConsent = false; + let partnerConsentAt = null; + let partnerConsentIp = null; + if (user?.partner_id) { + const partner = await db('partners') + .where({ id: user.partner_id }) + .select('data_consent', 'consent_at', 'consent_ip') + .first(); + partnerConsent = Boolean(partner?.data_consent); + partnerConsentAt = partner?.consent_at || null; + partnerConsentIp = partner?.consent_ip || null; + } + res.json({ ...user, partnerConsent, partnerConsentAt, partnerConsentIp }); + } + catch (err) { + if (err.name === 'TokenExpiredError') { + return res.status(401).json({ error: 'Token expirado' }); + } + res.status(401).json({ error: 'Token inválido' }); + } + }); + // ── POST /refresh ───────────────────────────────────── + router.post('/refresh', async (req, res) => { + try { + const { refreshToken } = req.body; + if (!refreshToken) { + return res.status(400).json({ error: 'Refresh token obrigatório' }); + } + const payload = jsonwebtoken_1.default.verify(refreshToken, process.env.JWT_REFRESH_SECRET); + if (payload.type !== 'refresh') { + return res.status(401).json({ error: 'Token inválido' }); + } + const user = await db('users').where({ id: payload.id }).first(); + if (!user) { + return res.status(404).json({ error: 'Usuário não encontrado' }); + } + const newAccessToken = jsonwebtoken_1.default.sign({ id: user.id, email: user.email, role: user.role }, process.env.JWT_SECRET, { expiresIn: (process.env.JWT_EXPIRES_IN || '15m') }); + await hooks.emit('token:refresh', { userId: user.id }); + res.json({ accessToken: newAccessToken }); + } + catch (err) { + res.status(401).json({ error: 'Refresh token inválido ou expirado' }); + } + }); + return router; +} +//# sourceMappingURL=routes.js.map \ No newline at end of file diff --git a/plugins/core-auth/backend/routes.js.map b/plugins/core-auth/backend/routes.js.map new file mode 100644 index 0000000..e6fbcee --- /dev/null +++ b/plugins/core-auth/backend/routes.js.map @@ -0,0 +1 @@ +{"version":3,"file":"routes.js","sourceRoot":"","sources":["routes.ts"],"names":[],"mappings":";AAAA,+DAA+D;AAC/D,6BAA6B;AAC7B,+DAA+D;AAC/D,mDAAmD;AACnD,4DAA4D;;;;;AAO5D,4CAwKC;AA7KD,qCAAoD;AACpD,wDAA8B;AAC9B,gEAA+B;AAG/B,SAAgB,gBAAgB,CAAC,GAAkB;IAC/C,MAAM,MAAM,GAAG,IAAA,gBAAM,GAAE,CAAC;IACxB,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC;IAE1B,yDAAyD;IACzD,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;QACxD,IAAI,CAAC;YACD,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;YAErC,IAAI,CAAC,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACtB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC,CAAC;YAC7E,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;YACxD,IAAI,CAAC,IAAI,EAAE,CAAC;gBACR,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;YACpE,CAAC;YAED,MAAM,OAAO,GAAG,MAAM,kBAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;YACnE,IAAI,CAAC,OAAO,EAAE,CAAC;gBACX,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;YACpE,CAAC;YAED,MAAM,WAAW,GAAG,sBAAG,CAAC,IAAI,CACxB,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EACnD,OAAO,CAAC,GAAG,CAAC,UAAW,EACvB,EAAE,SAAS,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,KAAK,CAAQ,EAAE,CAC9D,CAAC;YAEF,MAAM,YAAY,GAAG,sBAAG,CAAC,IAAI,CACzB,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAChC,OAAO,CAAC,GAAG,CAAC,kBAAmB,EAC/B,EAAE,SAAS,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,sBAAsB,IAAI,IAAI,CAAQ,EAAE,CACrE,CAAC;YAEF,kBAAkB;YAClB,MAAM,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;YAEhE,YAAY;YACZ,MAAM,EAAE,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC;gBACzB,OAAO,EAAE,IAAI,CAAC,EAAE;gBAChB,MAAM,EAAE,OAAO;gBACf,WAAW,EAAE,MAAM;gBACnB,SAAS,EAAE,IAAI,CAAC,EAAE;gBAClB,OAAO,EAAE,0BAA0B;gBACnC,UAAU,EAAE,GAAG,CAAC,EAAE;aACrB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe;YAEpC,MAAM,EAAE,aAAa,EAAE,GAAG,QAAQ,EAAE,GAAG,IAAI,CAAC;YAC5C,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,CAAC,CAAC;QAC5D,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAChB,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,gBAAgB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YAChD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;QAChE,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,yDAAyD;IACzD,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;QAC3D,IAAI,CAAC;YACD,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;YAErF,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC/B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,sCAAsC,EAAE,CAAC,CAAC;YACnF,CAAC;YAED,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;YAC5D,IAAI,QAAQ,EAAE,CAAC;gBACX,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;YAClE,CAAC;YAED,MAAM,aAAa,GAAG,MAAM,kBAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;YAEtD,MAAM,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;gBAClC,IAAI;gBACJ,KAAK;gBACL,aAAa;gBACb,GAAG,EAAE,GAAG,IAAI,IAAI;gBAChB,QAAQ,EAAE,QAAQ,IAAI,IAAI;gBAC1B,IAAI,EAAE,IAAI,IAAI,IAAI;gBAClB,YAAY,EAAE,YAAY,IAAI,IAAI;gBAClC,KAAK,EAAE,KAAK,IAAI,IAAI;gBACpB,IAAI,EAAE,MAAM;gBACZ,MAAM,EAAE,QAAQ;aACnB,CAAC,CAAC;YAEH,MAAM,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;YAEzD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QAC5D,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAChB,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,mBAAmB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;YACnD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,0BAA0B,EAAE,CAAC,CAAC;QAChE,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,yDAAyD;IACzD,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;QACpD,IAAI,CAAC;YACD,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;YAC7C,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBACrC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC,CAAC;YAClE,CAAC;YAED,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YACvC,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,GAAG,CAAC,UAAW,CAAQ,CAAC;YAElE,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC;iBACzB,KAAK,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC;iBACzB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY,CAAC;iBAC/H,KAAK,EAAE,CAAC;YAEb,IAAI,CAAC,IAAI,EAAE,CAAC;gBACR,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC,CAAC;YACrE,CAAC;YAED,IAAI,cAAc,GAAG,KAAK,CAAC;YAC3B,IAAI,gBAAgB,GAAG,IAAI,CAAC;YAC5B,IAAI,gBAAgB,GAAG,IAAI,CAAC;YAC5B,IAAI,IAAI,EAAE,UAAU,EAAE,CAAC;gBACnB,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC;qBAC/B,KAAK,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC;qBAC9B,MAAM,CAAC,cAAc,EAAE,YAAY,EAAE,YAAY,CAAC;qBAClD,KAAK,EAAE,CAAC;gBACb,cAAc,GAAG,OAAO,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;gBAChD,gBAAgB,GAAG,OAAO,EAAE,UAAU,IAAI,IAAI,CAAC;gBAC/C,gBAAgB,GAAG,OAAO,EAAE,UAAU,IAAI,IAAI,CAAC;YACnD,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,cAAc,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,CAAC,CAAC;QAC9E,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAChB,IAAI,GAAG,CAAC,IAAI,KAAK,mBAAmB,EAAE,CAAC;gBACnC,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;YAC7D,CAAC;YACD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;QACtD,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,yDAAyD;IACzD,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,EAAE;QAC1D,IAAI,CAAC;YACD,MAAM,EAAE,YAAY,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;YAClC,IAAI,CAAC,YAAY,EAAE,CAAC;gBAChB,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,CAAC;YACxE,CAAC;YAED,MAAM,OAAO,GAAG,sBAAG,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAmB,CAAQ,CAAC;YACjF,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;gBAC7B,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAC;YAC7D,CAAC;YAED,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;YACjE,IAAI,CAAC,IAAI,EAAE,CAAC;gBACR,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,wBAAwB,EAAE,CAAC,CAAC;YACrE,CAAC;YAED,MAAM,cAAc,GAAG,sBAAG,CAAC,IAAI,CAC3B,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EACnD,OAAO,CAAC,GAAG,CAAC,UAAW,EACvB,EAAE,SAAS,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,KAAK,CAAQ,EAAE,CAC9D,CAAC;YAEF,MAAM,KAAK,CAAC,IAAI,CAAC,eAAe,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;YAEvD,GAAG,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,cAAc,EAAE,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,GAAQ,EAAE,CAAC;YAChB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAC,CAAC;QAC1E,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,OAAO,MAAM,CAAC;AAClB,CAAC"} \ No newline at end of file diff --git a/plugins/core-auth/backend/routes.ts b/plugins/core-auth/backend/routes.ts new file mode 100644 index 0000000..1f612a4 --- /dev/null +++ b/plugins/core-auth/backend/routes.ts @@ -0,0 +1,180 @@ +// ============================================================ +// Plugin: core-auth — Routes +// ============================================================ +// Migrated from: backend/src/routes/auth.routes.ts +// backend/src/controllers/auth.controller.ts + +import { Router, Request, Response } from 'express'; +import bcrypt from 'bcryptjs'; +import jwt from 'jsonwebtoken'; +import { PluginContext } from '../../../backend/src/core/types'; + +export function createAuthRoutes(ctx: PluginContext): Router { + const router = Router(); + const { db, hooks } = ctx; + + // ── POST /login ─────────────────────────────────────── + router.post('/login', async (req: Request, res: Response) => { + try { + const { email, password } = req.body; + + if (!email || !password) { + return res.status(400).json({ error: 'Email e senha são obrigatórios' }); + } + + const user = await db('users').where({ email }).first(); + if (!user) { + return res.status(401).json({ error: 'Credenciais inválidas' }); + } + + const isValid = await bcrypt.compare(password, user.password_hash); + if (!isValid) { + return res.status(401).json({ error: 'Credenciais inválidas' }); + } + + const accessToken = jwt.sign( + { id: user.id, email: user.email, role: user.role }, + process.env.JWT_SECRET!, + { expiresIn: (process.env.JWT_EXPIRES_IN || '15m') as any } + ); + + const refreshToken = jwt.sign( + { id: user.id, type: 'refresh' }, + process.env.JWT_REFRESH_SECRET!, + { expiresIn: (process.env.JWT_REFRESH_EXPIRES_IN || '7d') as any } + ); + + // Emit hook event + await hooks.emit('user:login', { userId: user.id, ip: req.ip }); + + // Audit log + await db('audit_log').insert({ + user_id: user.id, + action: 'LOGIN', + entity_type: 'user', + entity_id: user.id, + details: 'Usuário logou no sistema', + ip_address: req.ip, + }).catch(() => { }); // non-critical + + const { password_hash, ...safeUser } = user; + res.json({ user: safeUser, accessToken, refreshToken }); + } catch (err: any) { + ctx.logger.error(`Login error: ${err.message}`); + res.status(500).json({ error: 'Erro interno no servidor' }); + } + }); + + // ── POST /register ──────────────────────────────────── + router.post('/register', async (req: Request, res: Response) => { + try { + const { name, email, password, cpf, whatsapp, city, neighborhood, state } = req.body; + + if (!name || !email || !password) { + return res.status(400).json({ error: 'Nome, email e senha são obrigatórios' }); + } + + const existing = await db('users').where({ email }).first(); + if (existing) { + return res.status(409).json({ error: 'Email já cadastrado' }); + } + + const password_hash = await bcrypt.hash(password, 12); + + const [id] = await db('users').insert({ + name, + email, + password_hash, + cpf: cpf || null, + whatsapp: whatsapp || null, + city: city || null, + neighborhood: neighborhood || null, + state: state || null, + role: 'user', + status: 'active', + }); + + await hooks.emit('user:register', { userId: id, email }); + + res.status(201).json({ id, name, email, role: 'user' }); + } catch (err: any) { + ctx.logger.error(`Register error: ${err.message}`); + res.status(500).json({ error: 'Erro interno no servidor' }); + } + }); + + // ── GET /me ─────────────────────────────────────────── + router.get('/me', async (req: Request, res: Response) => { + try { + const authHeader = req.headers.authorization; + if (!authHeader?.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Token não fornecido' }); + } + + const token = authHeader.split(' ')[1]; + const payload = jwt.verify(token, process.env.JWT_SECRET!) as any; + + const user = await db('users') + .where({ id: payload.id }) + .select('id', 'name', 'email', 'role', 'status', 'whatsapp', 'cpf', 'city', 'neighborhood', 'state', 'partner_id', 'created_at') + .first(); + + if (!user) { + return res.status(404).json({ error: 'Usuário não encontrado' }); + } + + let partnerConsent = false; + let partnerConsentAt = null; + let partnerConsentIp = null; + if (user?.partner_id) { + const partner = await db('partners') + .where({ id: user.partner_id }) + .select('data_consent', 'consent_at', 'consent_ip') + .first(); + partnerConsent = Boolean(partner?.data_consent); + partnerConsentAt = partner?.consent_at || null; + partnerConsentIp = partner?.consent_ip || null; + } + res.json({ ...user, partnerConsent, partnerConsentAt, partnerConsentIp }); + } catch (err: any) { + if (err.name === 'TokenExpiredError') { + return res.status(401).json({ error: 'Token expirado' }); + } + res.status(401).json({ error: 'Token inválido' }); + } + }); + + // ── POST /refresh ───────────────────────────────────── + router.post('/refresh', async (req: Request, res: Response) => { + try { + const { refreshToken } = req.body; + if (!refreshToken) { + return res.status(400).json({ error: 'Refresh token obrigatório' }); + } + + const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET!) as any; + if (payload.type !== 'refresh') { + return res.status(401).json({ error: 'Token inválido' }); + } + + const user = await db('users').where({ id: payload.id }).first(); + if (!user) { + return res.status(404).json({ error: 'Usuário não encontrado' }); + } + + const newAccessToken = jwt.sign( + { id: user.id, email: user.email, role: user.role }, + process.env.JWT_SECRET!, + { expiresIn: (process.env.JWT_EXPIRES_IN || '15m') as any } + ); + + await hooks.emit('token:refresh', { userId: user.id }); + + res.json({ accessToken: newAccessToken }); + } catch (err: any) { + res.status(401).json({ error: 'Refresh token inválido ou expirado' }); + } + }); + + return router; +} diff --git a/plugins/core-auth/index.js b/plugins/core-auth/index.js new file mode 100644 index 0000000..ee1e4c4 --- /dev/null +++ b/plugins/core-auth/index.js @@ -0,0 +1,23 @@ +"use strict"; +// ============================================================ +// Plugin: core-auth — Entry Point +// ============================================================ +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const routes_1 = require("./backend/routes"); +const manifest_json_1 = __importDefault(require("./manifest.json")); +const plugin = { + manifest: manifest_json_1.default, + async activate(ctx) { + const router = (0, routes_1.createAuthRoutes)(ctx); + ctx.app.use('/api/auth', router); + ctx.logger.info('Auth routes registered at /api/auth'); + }, + async deactivate(ctx) { + ctx.logger.info('Auth plugin deactivated'); + }, +}; +exports.default = plugin; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/plugins/core-auth/index.js.map b/plugins/core-auth/index.js.map new file mode 100644 index 0000000..dc38631 --- /dev/null +++ b/plugins/core-auth/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";AAAA,+DAA+D;AAC/D,kCAAkC;AAClC,+DAA+D;;;;;AAG/D,6CAAoD;AACpD,oEAAuC;AAEvC,MAAM,MAAM,GAAmB;IAC3B,QAAQ,EAAE,uBAAe;IAEzB,KAAK,CAAC,QAAQ,CAAC,GAAkB;QAC7B,MAAM,MAAM,GAAG,IAAA,yBAAgB,EAAC,GAAG,CAAC,CAAC;QACrC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QACjC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;IAC3D,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,GAAkB;QAC/B,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAC/C,CAAC;CACJ,CAAC;AAEF,kBAAe,MAAM,CAAC"} \ No newline at end of file diff --git a/plugins/core-auth/index.ts b/plugins/core-auth/index.ts new file mode 100644 index 0000000..9d2e49b --- /dev/null +++ b/plugins/core-auth/index.ts @@ -0,0 +1,23 @@ +// ============================================================ +// Plugin: core-auth — Entry Point +// ============================================================ + +import { PluginInstance, PluginContext } from '../../backend/src/core/types'; +import { createAuthRoutes } from './backend/routes'; +import manifest from './manifest.json'; + +const plugin: PluginInstance = { + manifest: manifest as any, + + async activate(ctx: PluginContext): Promise { + const router = createAuthRoutes(ctx); + ctx.app.use('/api/auth', router); + ctx.logger.info('Auth routes registered at /api/auth'); + }, + + async deactivate(ctx: PluginContext): Promise { + ctx.logger.info('Auth plugin deactivated'); + }, +}; + +export default plugin; diff --git a/plugins/core-auth/manifest.json b/plugins/core-auth/manifest.json new file mode 100644 index 0000000..57b2d25 --- /dev/null +++ b/plugins/core-auth/manifest.json @@ -0,0 +1,46 @@ +{ + "name": "core-auth", + "displayName": "Autenticação", + "version": "1.0.0", + "description": "Login, JWT, refresh tokens, sessões, Google OAuth e RBAC.", + "author": "Clube67", + "category": "core", + "enabled": true, + "canDisable": false, + "dependencies": [], + "backend": { + "routePrefix": "/api/auth", + "hasMigrations": true, + "globalMiddleware": [ + "authenticate" + ] + }, + "frontend": { + "menuItems": [], + "pages": [ + { + "path": "/login", + "component": "LoginPage", + "roles": [ + "*" + ] + }, + { + "path": "/register", + "component": "RegisterPage", + "roles": [ + "*" + ] + } + ] + }, + "hooks": { + "subscribes": [], + "emits": [ + "user:login", + "user:logout", + "user:register", + "token:refresh" + ] + } +} diff --git a/plugins/exemplo-plugin/index.js b/plugins/exemplo-plugin/index.js new file mode 100644 index 0000000..3f95e25 --- /dev/null +++ b/plugins/exemplo-plugin/index.js @@ -0,0 +1,35 @@ +const { Router } = require('express'); + +const router = Router(); + +router.get('/status', (_req, res) => { + res.json({ + plugin: 'exemplo-plugin', + version: '1.0.0', + status: 'active', + timestamp: new Date().toISOString(), + }); +}); + +const hooks = { + 'benefit.created': [ + async (data) => { + console.log('[ExemploPlugin] Benefício criado:', data?.title); + }, + ], + 'user.registered': [ + async (data) => { + console.log('[ExemploPlugin] Usuário registrado:', data?.email); + }, + ], +}; + +async function activate() { + console.log('[ExemploPlugin] Plugin ativado'); +} + +async function deactivate() { + console.log('[ExemploPlugin] Plugin desativado'); +} + +module.exports = { router, hooks, activate, deactivate }; diff --git a/plugins/exemplo-plugin/manifest.json b/plugins/exemplo-plugin/manifest.json new file mode 100644 index 0000000..2852ccc --- /dev/null +++ b/plugins/exemplo-plugin/manifest.json @@ -0,0 +1,13 @@ +{ + "name": "exemplo-plugin", + "slug": "exemplo-plugin", + "version": "1.0.0", + "description": "Plugin de exemplo para o Clube de Benefícios", + "author": "Clube67", + "entryPoint": "index.js", + "dependencies": [], + "hooks": [ + "benefit.created", + "user.registered" + ] +} \ No newline at end of file diff --git a/plugins/ext-api/backend/apikey-auth.js b/plugins/ext-api/backend/apikey-auth.js new file mode 100644 index 0000000..7fe01a0 --- /dev/null +++ b/plugins/ext-api/backend/apikey-auth.js @@ -0,0 +1,103 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.buildApiKeyAuth = buildApiKeyAuth; +exports.resolveApiKey = resolveApiKey; +// ─── Cache de API key (evita Prisma query em cada requisição) ──────────────── +// Chaves são imutáveis durante a vida da sessão — TTL de 5 min é suficiente. +const KEY_CACHE_TTL = 5 * 60000; +const keyCache = new Map(); +setInterval(() => { + const now = Date.now(); + for (const [k, v] of keyCache.entries()) { + if (now - v.cachedAt > KEY_CACHE_TTL) + keyCache.delete(k); + } +}, KEY_CACHE_TTL); +// ─── Rate limiter simples em memória (por chave) ───────────────────────────── +const RATE_WINDOW_MS = 60000; +const RATE_MAX = 120; +const rateMap = new Map(); +function checkRateLimit(key) { + const now = Date.now(); + let entry = rateMap.get(key); + if (!entry || now > entry.resetAt) { + entry = { count: 1, resetAt: now + RATE_WINDOW_MS }; + rateMap.set(key, entry); + return true; + } + entry.count++; + return entry.count <= RATE_MAX; +} +// Limpeza periódica para não vazar memória +setInterval(() => { + const now = Date.now(); + for (const [k, v] of rateMap.entries()) { + if (now > v.resetAt) + rateMap.delete(k); + } +}, RATE_WINDOW_MS * 2); +// ─── Factory ───────────────────────────────────────────────────────────────── +function buildApiKeyAuth(prisma) { + return async function apiKeyAuth(req, res, next) { + const key = req.headers['x-nw-key']?.trim(); + if (!key) { + res.status(401).json({ error: 'Header x-nw-key ausente' }); + return; + } + if (!checkRateLimit(key)) { + res.status(429).json({ error: 'Rate limit excedido (120 req/min por chave)' }); + return; + } + // Verifica cache antes de ir ao banco + const cached = keyCache.get(key); + if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) { + if (cached.expiresAt && cached.expiresAt < new Date()) { + res.status(401).json({ error: 'Chave expirada' }); + return; + } + req.extTenantId = cached.tenantId; + next(); + return; + } + const apiKey = await prisma.apiKey.findUnique({ + where: { key }, + select: { tenantId: true, isActive: true, expiresAt: true }, + }); + if (!apiKey || !apiKey.isActive) { + res.status(401).json({ error: 'Chave inválida ou inativa' }); + return; + } + if (apiKey.expiresAt && apiKey.expiresAt < new Date()) { + res.status(401).json({ error: 'Chave expirada' }); + return; + } + keyCache.set(key, { tenantId: apiKey.tenantId, expiresAt: apiKey.expiresAt, cachedAt: Date.now() }); + req.extTenantId = apiKey.tenantId; + next(); + }; +} +/** + * Versão standalone que valida a chave sem passar pelo Express. + * Usada no handshake do WS (upgrade request). + */ +async function resolveApiKey(prisma, key) { + if (!key) + return null; + const cached = keyCache.get(key); + if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) { + if (cached.expiresAt && cached.expiresAt < new Date()) + return null; + return cached.tenantId; + } + const apiKey = await prisma.apiKey.findUnique({ + where: { key }, + select: { tenantId: true, isActive: true, expiresAt: true }, + }); + if (!apiKey || !apiKey.isActive) + return null; + if (apiKey.expiresAt && apiKey.expiresAt < new Date()) + return null; + keyCache.set(key, { tenantId: apiKey.tenantId, expiresAt: apiKey.expiresAt, cachedAt: Date.now() }); + return apiKey.tenantId; +} +//# sourceMappingURL=apikey-auth.js.map \ No newline at end of file diff --git a/plugins/ext-api/backend/apikey-auth.js.map b/plugins/ext-api/backend/apikey-auth.js.map new file mode 100644 index 0000000..79d624b --- /dev/null +++ b/plugins/ext-api/backend/apikey-auth.js.map @@ -0,0 +1 @@ +{"version":3,"file":"apikey-auth.js","sourceRoot":"","sources":["apikey-auth.ts"],"names":[],"mappings":";;AA+DA,0CA6CC;AAMD,sCAkBC;AAxHD,gFAAgF;AAChF,6EAA6E;AAC7E,MAAM,aAAa,GAAG,CAAC,GAAG,KAAM,CAAA;AAEhC,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAyB,CAAA;AAEjD,WAAW,CAAC,GAAG,EAAE;IACf,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;QACxC,IAAI,GAAG,GAAG,CAAC,CAAC,QAAQ,GAAG,aAAa;YAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IAC1D,CAAC;AACH,CAAC,EAAE,aAAa,CAAC,CAAA;AAEjB,gFAAgF;AAChF,MAAM,cAAc,GAAG,KAAM,CAAA;AAC7B,MAAM,QAAQ,GAAS,GAAG,CAAA;AAG1B,MAAM,OAAO,GAAG,IAAI,GAAG,EAAqB,CAAA;AAE5C,SAAS,cAAc,CAAC,GAAW;IACjC,MAAM,GAAG,GAAI,IAAI,CAAC,GAAG,EAAE,CAAA;IACvB,IAAI,KAAK,GAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAC7B,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;QAClC,KAAK,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,GAAG,GAAG,cAAc,EAAE,CAAA;QACnD,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;QACvB,OAAO,IAAI,CAAA;IACb,CAAC;IACD,KAAK,CAAC,KAAK,EAAE,CAAA;IACb,OAAO,KAAK,CAAC,KAAK,IAAI,QAAQ,CAAA;AAChC,CAAC;AAED,2CAA2C;AAC3C,WAAW,CAAC,GAAG,EAAE;IACf,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IACtB,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QACvC,IAAI,GAAG,GAAG,CAAC,CAAC,OAAO;YAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;IACxC,CAAC;AACH,CAAC,EAAE,cAAc,GAAG,CAAC,CAAC,CAAA;AAYtB,gFAAgF;AAChF,SAAgB,eAAe,CAAC,MAAoB;IAClD,OAAO,KAAK,UAAU,UAAU,CAAC,GAAY,EAAE,GAAa,EAAE,IAAkB;QAC9E,MAAM,GAAG,GAAI,GAAG,CAAC,OAAO,CAAC,UAAU,CAAwB,EAAE,IAAI,EAAE,CAAA;QAEnE,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC,CAAA;YAC1D,OAAM;QACR,CAAC;QAED,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,6CAA6C,EAAE,CAAC,CAAA;YAC9E,OAAM;QACR,CAAC;QAED,sCAAsC;QACtC,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAChC,IAAI,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,QAAQ,GAAG,aAAa,EAAE,CAAC;YAC3D,IAAI,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;gBACtD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAA;gBACjD,OAAM;YACR,CAAC;YACD,GAAG,CAAC,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAA;YACjC,IAAI,EAAE,CAAA;YACN,OAAM;QACR,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC;YAC5C,KAAK,EAAE,EAAE,GAAG,EAAE;YACd,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;SAC5D,CAAC,CAAA;QAEF,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YAChC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,CAAC,CAAA;YAC5D,OAAM;QACR,CAAC;QAED,IAAI,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;YACtD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC,CAAA;YACjD,OAAM;QACR,CAAC;QAED,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;QACnG,GAAG,CAAC,WAAW,GAAG,MAAM,CAAC,QAAQ,CAAA;QACjC,IAAI,EAAE,CAAA;IACR,CAAC,CAAA;AACH,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,aAAa,CACjC,MAAoB,EACpB,GAAW;IAEX,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAA;IACrB,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IAChC,IAAI,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,QAAQ,GAAG,aAAa,EAAE,CAAC;QAC3D,IAAI,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE;YAAE,OAAO,IAAI,CAAA;QAClE,OAAO,MAAM,CAAC,QAAQ,CAAA;IACxB,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC;QAC5C,KAAK,EAAE,EAAE,GAAG,EAAE;QACd,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE;KAC5D,CAAC,CAAA;IACF,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAA;IAC5C,IAAI,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE;QAAE,OAAO,IAAI,CAAA;IAClE,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAA;IACnG,OAAO,MAAM,CAAC,QAAQ,CAAA;AACxB,CAAC"} \ No newline at end of file diff --git a/plugins/ext-api/backend/apikey-auth.ts b/plugins/ext-api/backend/apikey-auth.ts new file mode 100644 index 0000000..a38a4c0 --- /dev/null +++ b/plugins/ext-api/backend/apikey-auth.ts @@ -0,0 +1,133 @@ +/** + * Middleware de autenticação para a External API. + * + * Aceita a integration_key via: + * - Header: x-nw-key: nw_... + * + * Resolve a chave para tenantId e injeta em req.extTenantId. + * Rate-limiting básico: max 120 req/min por chave (janela deslizante simples em memória). + */ +import type { Request, Response, NextFunction } from 'express' +import type { PrismaClient } from '@prisma/client' + +// ─── Cache de API key (evita Prisma query em cada requisição) ──────────────── +// Chaves são imutáveis durante a vida da sessão — TTL de 5 min é suficiente. +const KEY_CACHE_TTL = 5 * 60_000 +interface KeyCacheEntry { tenantId: string; expiresAt: Date | null; cachedAt: number } +const keyCache = new Map() + +setInterval(() => { + const now = Date.now() + for (const [k, v] of keyCache.entries()) { + if (now - v.cachedAt > KEY_CACHE_TTL) keyCache.delete(k) + } +}, KEY_CACHE_TTL) + +// ─── Rate limiter simples em memória (por chave) ───────────────────────────── +const RATE_WINDOW_MS = 60_000 +const RATE_MAX = 120 + +interface RateEntry { count: number; resetAt: number } +const rateMap = new Map() + +function checkRateLimit(key: string): boolean { + const now = Date.now() + let entry = rateMap.get(key) + if (!entry || now > entry.resetAt) { + entry = { count: 1, resetAt: now + RATE_WINDOW_MS } + rateMap.set(key, entry) + return true + } + entry.count++ + return entry.count <= RATE_MAX +} + +// Limpeza periódica para não vazar memória +setInterval(() => { + const now = Date.now() + for (const [k, v] of rateMap.entries()) { + if (now > v.resetAt) rateMap.delete(k) + } +}, RATE_WINDOW_MS * 2) + +// ─── Extend Express Request ─────────────────────────────────────────────────── +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Express { + interface Request { + extTenantId?: string + } + } +} + +// ─── Factory ───────────────────────────────────────────────────────────────── +export function buildApiKeyAuth(prisma: PrismaClient) { + return async function apiKeyAuth(req: Request, res: Response, next: NextFunction) { + const key = (req.headers['x-nw-key'] as string | undefined)?.trim() + + if (!key) { + res.status(401).json({ error: 'Header x-nw-key ausente' }) + return + } + + if (!checkRateLimit(key)) { + res.status(429).json({ error: 'Rate limit excedido (120 req/min por chave)' }) + return + } + + // Verifica cache antes de ir ao banco + const cached = keyCache.get(key) + if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) { + if (cached.expiresAt && cached.expiresAt < new Date()) { + res.status(401).json({ error: 'Chave expirada' }) + return + } + req.extTenantId = cached.tenantId + next() + return + } + + const apiKey = await prisma.apiKey.findUnique({ + where: { key }, + select: { tenantId: true, isActive: true, expiresAt: true }, + }) + + if (!apiKey || !apiKey.isActive) { + res.status(401).json({ error: 'Chave inválida ou inativa' }) + return + } + + if (apiKey.expiresAt && apiKey.expiresAt < new Date()) { + res.status(401).json({ error: 'Chave expirada' }) + return + } + + keyCache.set(key, { tenantId: apiKey.tenantId, expiresAt: apiKey.expiresAt, cachedAt: Date.now() }) + req.extTenantId = apiKey.tenantId + next() + } +} + +/** + * Versão standalone que valida a chave sem passar pelo Express. + * Usada no handshake do WS (upgrade request). + */ +export async function resolveApiKey( + prisma: PrismaClient, + key: string, +): Promise { + if (!key) return null + const cached = keyCache.get(key) + if (cached && Date.now() - cached.cachedAt < KEY_CACHE_TTL) { + if (cached.expiresAt && cached.expiresAt < new Date()) return null + return cached.tenantId + } + const apiKey = await prisma.apiKey.findUnique({ + where: { key }, + select: { tenantId: true, isActive: true, expiresAt: true }, + }) + if (!apiKey || !apiKey.isActive) return null + if (apiKey.expiresAt && apiKey.expiresAt < new Date()) return null + keyCache.set(key, { tenantId: apiKey.tenantId, expiresAt: apiKey.expiresAt, cachedAt: Date.now() }) + return apiKey.tenantId +} diff --git a/plugins/ext-api/backend/backend/src/config/env.js b/plugins/ext-api/backend/backend/src/config/env.js new file mode 100644 index 0000000..e6c2ea2 --- /dev/null +++ b/plugins/ext-api/backend/backend/src/config/env.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.env = void 0; +const zod_1 = require("zod"); +require("dotenv/config"); +const envSchema = zod_1.z.object({ + PORT: zod_1.z.string().default('8008'), + NODE_ENV: zod_1.z.enum(['development', 'production', 'test']).default('development'), + JWT_SECRET: zod_1.z.string().min(16), + DATABASE_URL: zod_1.z.string().url(), + DRAGONFLY_URL: zod_1.z.string().default('redis://localhost:6379'), + DRAGONFLY_TTL_SECONDS: zod_1.z.string().default('86400'), + NATS_URL: zod_1.z.string().default('nats://localhost:4222'), + TEMPORAL_ADDRESS: zod_1.z.string().default('localhost:7233'), + TEMPORAL_NAMESPACE: zod_1.z.string().default('default'), + TEMPORAL_TASK_QUEUE: zod_1.z.string().default('newwhats-queue'), + BAILEYS_SESSIONS_PATH: zod_1.z.string().default('./sessions'), + FRONTEND_URL: zod_1.z.string().default('http://localhost:3000'), +}); +const parsed = envSchema.safeParse(process.env); +if (!parsed.success) { + console.error('❌ Variáveis de ambiente inválidas:'); + console.error(parsed.error.format()); + process.exit(1); +} +exports.env = parsed.data; diff --git a/plugins/ext-api/backend/backend/src/config/logger.js b/plugins/ext-api/backend/backend/src/config/logger.js new file mode 100644 index 0000000..95c3a9e --- /dev/null +++ b/plugins/ext-api/backend/backend/src/config/logger.js @@ -0,0 +1,14 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.logger = void 0; +const pino_1 = __importDefault(require("pino")); +const env_1 = require("./env"); +exports.logger = (0, pino_1.default)({ + level: env_1.env.NODE_ENV === 'production' ? 'info' : 'debug', + transport: env_1.env.NODE_ENV !== 'production' + ? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard' } } + : undefined, +}); diff --git a/plugins/ext-api/backend/backend/src/core/StorageProvider.js b/plugins/ext-api/backend/backend/src/core/StorageProvider.js new file mode 100644 index 0000000..a02b80e --- /dev/null +++ b/plugins/ext-api/backend/backend/src/core/StorageProvider.js @@ -0,0 +1,50 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.storageProvider = void 0; +const logger_1 = require("../config/logger"); +/** + * Singleton registry para o storage provider ativo. + * O plugin UnifiedStorageProvider se registra aqui durante o activate(). + * O plugin uploads chama uploadFile() para enviar ao storage registrado. + */ +class StorageProviderRegistry { + constructor() { + this.provider = null; + this.healthy = false; + } + register(provider) { + this.provider = provider; + logger_1.logger.info('[StorageProvider] Provider registrado'); + } + isRegistered() { + return this.provider !== null; + } + isHealthy() { + return this.healthy; + } + async uploadFile(payload) { + if (!this.provider) { + throw new Error('Nenhum storage provider registrado. Ative o plugin UnifiedStorageProvider.'); + } + return this.provider.upload(payload); + } + /** Atualiza o status de saúde — chamado periodicamente pelo UnifiedStorageProvider */ + async updateHealth() { + if (!this.provider) { + this.healthy = false; + return; + } + try { + this.healthy = await this.provider.checkHealth(); + } + catch { + this.healthy = false; + } + } + unregister() { + this.provider = null; + this.healthy = false; + logger_1.logger.info('[StorageProvider] Provider removido'); + } +} +exports.storageProvider = new StorageProviderRegistry(); diff --git a/plugins/ext-api/backend/backend/src/core/hook-bus.js b/plugins/ext-api/backend/backend/src/core/hook-bus.js new file mode 100644 index 0000000..3ad6421 --- /dev/null +++ b/plugins/ext-api/backend/backend/src/core/hook-bus.js @@ -0,0 +1,49 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.hookBus = exports.HookBus = void 0; +const logger_1 = require("../config/logger"); +/** + * Event bus entre plugins. + * emit() executa todos os handlers registrados e retorna array de resultados. + * O plugin uploads usa o último resultado não-nulo retornado pelo SmartVision. + */ +class HookBus { + constructor() { + this.handlers = new Map(); + } + register(event, handler) { + if (!this.handlers.has(event)) { + this.handlers.set(event, []); + } + this.handlers.get(event).push(handler); + logger_1.logger.debug({ event }, '[HookBus] Handler registrado'); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async emit(event, data) { + const handlers = this.handlers.get(event); + if (!handlers || handlers.length === 0) + return []; + const results = []; + for (const handler of handlers) { + try { + const result = await handler(data); + results.push(result); + } + catch (err) { + logger_1.logger.error({ err, event }, '[HookBus] Erro em handler'); + results.push(null); + } + } + return results; + } + /** Remove todos os handlers de um evento (usado no deactivate de plugins) */ + removeAll(event) { + this.handlers.delete(event); + } + /** Lista eventos registrados — útil para debug */ + listEvents() { + return Array.from(this.handlers.keys()); + } +} +exports.HookBus = HookBus; +exports.hookBus = new HookBus(); diff --git a/plugins/ext-api/backend/backend/src/core/plugin-config.js b/plugins/ext-api/backend/backend/src/core/plugin-config.js new file mode 100644 index 0000000..dc092fa --- /dev/null +++ b/plugins/ext-api/backend/backend/src/core/plugin-config.js @@ -0,0 +1,51 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.pluginConfig = exports.PluginConfigStore = void 0; +const dragonfly_1 = require("../infra/cache/dragonfly"); +const logger_1 = require("../config/logger"); +const KEY_PREFIX = 'plugin:config:'; +// Config de plugins não expira — TTL longo (1 ano) +const TTL_SECONDS = 365 * 24 * 3600; +/** + * Config store para plugins, backed por DragonflyDB. + * get() usa cache in-memory para evitar round-trips no hot-path. + * set() persiste no Dragonfly e invalida o cache local. + */ +class PluginConfigStore { + constructor() { + this.cache = new Map(); + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + /** Lê config de um plugin. Retorna {} se não houver config salva. */ + get(pluginName) { + return this.cache.get(pluginName) ?? {}; + } + /** Persiste config no Dragonfly e atualiza cache local. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async set(pluginName, config) { + this.cache.set(pluginName, config); + await dragonfly_1.dragonfly.setJson(`${KEY_PREFIX}${pluginName}`, config, TTL_SECONDS); + logger_1.logger.debug({ pluginName }, '[PluginConfig] Config salva'); + } + /** Carrega todas as configs do Dragonfly para o cache local no boot. */ + async loadAll(pluginNames) { + await Promise.all(pluginNames.map(async (name) => { + const stored = await dragonfly_1.dragonfly.getJson(`${KEY_PREFIX}${name}`); + if (stored) { + this.cache.set(name, stored); + logger_1.logger.debug({ name }, '[PluginConfig] Config carregada do Dragonfly'); + } + })); + } + /** Retorna config de todos os plugins (para a API admin). */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + getAllCached() { + const result = {}; + for (const [name, config] of this.cache.entries()) { + result[name] = config; + } + return result; + } +} +exports.PluginConfigStore = PluginConfigStore; +exports.pluginConfig = new PluginConfigStore(); diff --git a/plugins/ext-api/backend/backend/src/core/types.js b/plugins/ext-api/backend/backend/src/core/types.js new file mode 100644 index 0000000..c8ad2e5 --- /dev/null +++ b/plugins/ext-api/backend/backend/src/core/types.js @@ -0,0 +1,2 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); diff --git a/plugins/ext-api/backend/backend/src/infra/cache/dragonfly.js b/plugins/ext-api/backend/backend/src/infra/cache/dragonfly.js new file mode 100644 index 0000000..9ea435b --- /dev/null +++ b/plugins/ext-api/backend/backend/src/infra/cache/dragonfly.js @@ -0,0 +1,54 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.dragonfly = void 0; +const ioredis_1 = __importDefault(require("ioredis")); +const env_1 = require("../../config/env"); +const logger_1 = require("../../config/logger"); +class DragonflyClient { + constructor() { + this.ttl = parseInt(env_1.env.DRAGONFLY_TTL_SECONDS, 10); + this.client = new ioredis_1.default(env_1.env.DRAGONFLY_URL, { + maxRetriesPerRequest: 3, + enableReadyCheck: true, + lazyConnect: true, + }); + this.client.on('connect', () => logger_1.logger.info('DragonflyDB conectado')); + this.client.on('error', (err) => logger_1.logger.error(err, 'DragonflyDB erro')); + } + async connect() { + await this.client.connect(); + } + async get(key) { + return this.client.get(key); + } + async getJson(key) { + const raw = await this.client.get(key); + if (!raw) + return null; + return JSON.parse(raw); + } + async set(key, value, ttlSeconds) { + await this.client.set(key, value, 'EX', ttlSeconds ?? this.ttl); + } + async setJson(key, value, ttlSeconds) { + await this.client.set(key, JSON.stringify(value), 'EX', ttlSeconds ?? this.ttl); + } + async del(key) { + await this.client.del(key); + } + async exists(key) { + const count = await this.client.exists(key); + return count > 0; + } + /** Publica em um canal pub/sub (usado para broadcasting de QR/status) */ + async publish(channel, message) { + await this.client.publish(channel, message); + } + raw() { + return this.client; + } +} +exports.dragonfly = new DragonflyClient(); diff --git a/plugins/ext-api/backend/backend/src/infra/database/prisma.js b/plugins/ext-api/backend/backend/src/infra/database/prisma.js new file mode 100644 index 0000000..17038f6 --- /dev/null +++ b/plugins/ext-api/backend/backend/src/infra/database/prisma.js @@ -0,0 +1,13 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.prisma = void 0; +const client_1 = require("@prisma/client"); +exports.prisma = global.__prisma ?? + new client_1.PrismaClient({ + log: process.env.NODE_ENV !== 'production' + ? [{ level: 'warn', emit: 'stdout' }, { level: 'error', emit: 'stdout' }] + : [], + }); +if (process.env.NODE_ENV !== 'production') { + global.__prisma = exports.prisma; +} diff --git a/plugins/ext-api/backend/backend/src/modules/chatbot/chatbot.repository.js b/plugins/ext-api/backend/backend/src/modules/chatbot/chatbot.repository.js new file mode 100644 index 0000000..5a2b8f6 --- /dev/null +++ b/plugins/ext-api/backend/backend/src/modules/chatbot/chatbot.repository.js @@ -0,0 +1,67 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.chatBotState = exports.botRepo = exports.credentialRepo = void 0; +/** + * chatbot.repository.ts — CRUD para AICredential e AIBot. + */ +const prisma_1 = require("../../infra/database/prisma"); +// ─── Credentials ───────────────────────────────────────────────────────────── +exports.credentialRepo = { + findAll: (tenantId, instanceId) => prisma_1.prisma.aICredential.findMany({ + where: { tenantId, instanceId }, + select: { id: true, name: true, provider: true, createdAt: true }, + orderBy: { createdAt: 'desc' }, + }), + create: (data) => prisma_1.prisma.aICredential.create({ + data: { + tenantId: data.tenantId, + instanceId: data.instanceId, + name: data.name, + provider: data.provider ?? 'GEMINI', + apiKey: data.apiKey, + }, + select: { id: true, name: true, provider: true, createdAt: true }, + }), + delete: (id, tenantId) => prisma_1.prisma.aICredential.deleteMany({ where: { id, tenantId } }), +}; +// ─── Bots ───────────────────────────────────────────────────────────────────── +exports.botRepo = { + findAll: (tenantId, instanceId) => prisma_1.prisma.aIBot.findMany({ + where: { tenantId, instanceId }, + include: { credential: { select: { id: true, name: true, provider: true } } }, + orderBy: { createdAt: 'desc' }, + }), + findEnabled: (tenantId, instanceId) => prisma_1.prisma.aIBot.findFirst({ + where: { tenantId, instanceId, enabled: true }, + include: { credential: true }, + }), + findById: (id, tenantId) => prisma_1.prisma.aIBot.findFirst({ + where: { id, tenantId }, + include: { credential: true }, + }), + create: (data) => prisma_1.prisma.aIBot.create({ + data: { + tenantId: data.tenantId, + instanceId: data.instanceId, + credentialId: data.credentialId, + name: data.name, + systemPrompt: data.systemPrompt, + model: data.model ?? 'gemini-1.5-flash', + enabled: data.enabled ?? false, + triggerMode: data.triggerMode ?? 'ALL', + keywords: data.keywords ?? [], + }, + }), + update: (id, tenantId, data) => prisma_1.prisma.aIBot.updateMany({ where: { id, tenantId }, data }), + delete: (id, tenantId) => prisma_1.prisma.aIBot.deleteMany({ where: { id, tenantId } }), +}; +// ─── Chat bot state ─────────────────────────────────────────────────────────── +exports.chatBotState = { + pauseBot: (chatId) => prisma_1.prisma.chat.update({ where: { id: chatId }, data: { botPaused: true } }), + resumeBot: (chatId) => prisma_1.prisma.chat.update({ where: { id: chatId }, data: { botPaused: false } }), + updateSummary: (chatId, botSummary) => prisma_1.prisma.chat.update({ where: { id: chatId }, data: { botSummary } }), + getState: (chatId) => prisma_1.prisma.chat.findUnique({ + where: { id: chatId }, + select: { botPaused: true, botSummary: true }, + }), +}; diff --git a/plugins/ext-api/backend/backend/src/modules/chatbot/chatbot.service.js b/plugins/ext-api/backend/backend/src/modules/chatbot/chatbot.service.js new file mode 100644 index 0000000..6553a2e --- /dev/null +++ b/plugins/ext-api/backend/backend/src/modules/chatbot/chatbot.service.js @@ -0,0 +1,164 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ChatbotService = void 0; +/** + * chatbot.service.ts — Motor de IA Multi-Agente com Gemini Flash. + * + * Padrão Cérebro (instrucoes.md §5): + * - Não envia histórico completo para economizar tokens + * - Mantém resumo de 2 frases por chat (botSummary no banco) + * - Micro-prompt de intenção retorna 1 token: "1" = resolver, "2" = escalar + */ +const generative_ai_1 = require("@google/generative-ai"); +const prisma_1 = require("../../infra/database/prisma"); +const logger_1 = require("../../config/logger"); +const chatbot_repository_1 = require("./chatbot.repository"); +// Cache de clientes Gemini por apiKey (evita recriar para cada mensagem) +const geminiClients = new Map(); +function getGeminiClient(apiKey) { + if (!geminiClients.has(apiKey)) { + geminiClients.set(apiKey, new generative_ai_1.GoogleGenerativeAI(apiKey)); + } + return geminiClients.get(apiKey); +} +function getModel(apiKey, modelName) { + return getGeminiClient(apiKey).getGenerativeModel({ model: modelName }); +} +// ─── Micro-prompt: classificação de intenção (1 token) ─────────────────────── +async function classifyIntent(userMsg, summary, systemPrompt, model) { + const prompt = [ + `Você é um classificador de intenção para um chatbot de atendimento.`, + `Prompt do atendente: ${systemPrompt}`, + summary ? `Contexto da conversa até agora: ${summary}` : '', + `Nova mensagem do cliente: "${userMsg}"`, + `Responda APENAS com o número:`, + `1 = Consigo responder essa pergunta dentro do meu papel`, + `2 = Precisa de um agente humano`, + ].filter(Boolean).join('\n'); + try { + const result = await model.generateContent({ + contents: [{ role: 'user', parts: [{ text: prompt }] }], + generationConfig: { maxOutputTokens: 2, temperature: 0 }, + }); + const text = result.response.text().trim(); + return text.startsWith('2') ? 'escalate' : 'resolve'; + } + catch (err) { + logger_1.logger.error({ err }, '[Chatbot] Erro na classificação de intenção — assumindo resolve'); + return 'resolve'; + } +} +// ─── Geração de resposta ────────────────────────────────────────────────────── +async function generateResponse(userMsg, summary, systemPrompt, model) { + const prompt = [ + systemPrompt, + summary ? `\nContexto da conversa até agora:\n${summary}` : '', + `\nMensagem do cliente: "${userMsg}"`, + `\nResponda de forma natural, breve e direta. Não use markdown.`, + ].filter(Boolean).join('\n'); + const result = await model.generateContent({ + contents: [{ role: 'user', parts: [{ text: prompt }] }], + generationConfig: { maxOutputTokens: 300, temperature: 0.7 }, + }); + return result.response.text().trim(); +} +// ─── Atualização do Cérebro (resumo de 2 frases) ───────────────────────────── +async function updateSummary(currentSummary, userMsg, botMsg, model) { + const prompt = [ + currentSummary + ? `Resumo atual da conversa: ${currentSummary}` + : 'Esta é a primeira troca da conversa.', + `Nova troca:`, + `Cliente: "${userMsg}"`, + `Assistente: "${botMsg}"`, + `Atualize o resumo em MÁXIMO 2 frases curtas, capturando o essencial da conversa inteira.`, + `Responda apenas com o resumo, sem introdução.`, + ].join('\n'); + try { + const result = await model.generateContent({ + contents: [{ role: 'user', parts: [{ text: prompt }] }], + generationConfig: { maxOutputTokens: 80, temperature: 0.3 }, + }); + return result.response.text().trim(); + } + catch { + // Em caso de erro, mantém o resumo anterior + return currentSummary ?? ''; + } +} +// ─── Interface pública ──────────────────────────────────────────────────────── +class ChatbotService { + constructor(io) { + this.io = io; + } + /** + * Ponto de entrada para cada mensagem recebida. + * Chamado pelo MessageHandler após persistir a mensagem. + */ + async handleIncoming(opts) { + const { tenantId, instanceId, chatId, jid, text, sock } = opts; + // 1. Verifica se há bot ativo para esta instância + const bot = await chatbot_repository_1.botRepo.findEnabled(tenantId, instanceId); + if (!bot) + return; + // 2. Verifica o estado do chat (human takeover) + const chatState = await chatbot_repository_1.chatBotState.getState(chatId); + if (!chatState || chatState.botPaused) + return; + // 3. Aplica modo de gatilho + if (bot.triggerMode === 'KEYWORD') { + const lowerText = text.toLowerCase(); + const hasKeyword = bot.keywords.some((kw) => lowerText.includes(kw.toLowerCase())); + if (!hasKeyword) + return; + } + try { + const model = getModel(bot.credential.apiKey, bot.model); + const summary = chatState.botSummary ?? null; + // 4. Micro-prompt: intenção + const intent = await classifyIntent(text, summary, bot.systemPrompt, model); + if (intent === 'escalate') { + // Pausa o bot e notifica via Socket.IO + await chatbot_repository_1.chatBotState.pauseBot(chatId); + this.io.to(`chat:${chatId}`).emit('bot:escalated', { + chatId, + message: 'Bot pausado — atendente humano necessário', + }); + logger_1.logger.info({ chatId, jid }, '[Chatbot] Escalação para humano'); + return; + } + // 5. Gera resposta + const responseText = await generateResponse(text, summary, bot.systemPrompt, model); + // 6. Envia via Baileys + const sent = await sock.sendMessage(jid, { text: responseText }); + // 7. Persiste mensagem do bot no banco + const chat = await prisma_1.prisma.chat.findUnique({ where: { id: chatId } }); + if (chat) { + const botMsg = await prisma_1.prisma.message.create({ + data: { + tenantId, + instanceId, + chatId, + remoteJid: jid, + messageId: sent?.key.id ?? `bot-${Date.now()}`, + fromMe: true, + type: 'TEXT', + body: responseText, + status: 'SENT', + timestamp: new Date(), + }, + }); + // Notifica frontend + this.io.to(`chat:${chatId}`).emit('message:new', botMsg); + } + // 8. Atualiza Cérebro + const newSummary = await updateSummary(summary, text, responseText, model); + await chatbot_repository_1.chatBotState.updateSummary(chatId, newSummary); + logger_1.logger.info({ chatId, jid, intent }, '[Chatbot] Resposta enviada'); + } + catch (err) { + logger_1.logger.error({ err, chatId }, '[Chatbot] Erro ao processar mensagem'); + } + } +} +exports.ChatbotService = ChatbotService; diff --git a/plugins/ext-api/backend/backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.js b/plugins/ext-api/backend/backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.js new file mode 100644 index 0000000..ba7485d --- /dev/null +++ b/plugins/ext-api/backend/backend/src/modules/whatsapp/connection/WhatsAppConnectionManager.js @@ -0,0 +1,1113 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.WhatsAppConnectionManager = void 0; +/** + * WhatsAppConnectionManager — Orquestra conexões Baileys para todas as instâncias. + * + * Esta é a classe central do módulo WhatsApp. Ela gerencia: + * 1. Ciclo de vida das conexões (connect, disconnect, reconnect) + * 2. Registro de TODOS os event listeners do Baileys + * 3. Reconexão automática com backoff exponencial + * 4. Watchdog para detectar conexões mortas silenciosamente + * 5. Sincronização de chats, contatos e histórico + * 6. Mapeamento LID → phone JID (via chats.phoneNumberShare) + * 7. Emissão de eventos Socket.IO para o frontend + * + * ARQUITETURA: + * - Uma instância de WhatsAppConnectionManager por servidor (singleton prático) + * - Cada instância WhatsApp (número conectado) tem seu próprio WASocket + * - Os Maps internos indexam por instanceId para acesso O(1) + * - Multi-tenant: tenantByInstance mapeia instância → tenant para isolamento + * + * EVENTOS BAILEYS REGISTRADOS: + * - creds.update → salva credenciais (auth state) + * - connection.update → gerencia QR, conexão, desconexão, reconexão + * - messages.upsert → delega para MessageHandler (tempo real + histórico) + * - messages.update → atualiza status (sent→delivered→read) + * - contacts.upsert/update → delega para ContactHandler + * - chats.upsert/update → sincroniza lista de chats no banco + * - chats.phoneNumberShare → captura mapeamento LID → phone JID + * - groups.upsert/update → sincroniza grupos (subject, metadata) + * - messaging-history.set → history sync (contatos, chats, mensagens em lote) + * + * @see MessageHandler — processa mensagens individuais + * @see ContactHandler — gerencia contatos e avatares + */ +const path_1 = __importDefault(require("path")); +const promises_1 = __importDefault(require("fs/promises")); +const promises_2 = __importDefault(require("dns/promises")); +const baileys_1 = require("@whiskeysockets/baileys"); +const qrcode_1 = __importDefault(require("qrcode")); +const env_1 = require("../../../config/env"); +const logger_1 = require("../../../config/logger"); +const dragonfly_1 = require("../../../infra/cache/dragonfly"); +const prisma_1 = require("../../../infra/database/prisma"); +const client_1 = require("@prisma/client"); +const MessageHandler_1 = require("../handlers/MessageHandler"); +const ContactHandler_1 = require("../handlers/ContactHandler"); +const chatbot_service_1 = require("../../chatbot/chatbot.service"); +const hook_bus_1 = require("../../../core/hook-bus"); +// ─── Chaves de cache no DragonflyDB (Redis-compatible) ────────────────────── +const CACHE_KEY_STATUS = (instanceId) => `instance:${instanceId}:status`; +const CACHE_KEY_QR = (instanceId) => `instance:${instanceId}:qr`; +// ─── Configuração de reconexão automática ─────────────────────────────────── +// Backoff exponencial: 3s → 6s → 12s → 24s → 48s → ... → max 5min +// Nunca desiste, exceto loggedOut (usuário deslogou) ou forbidden (ban) +const RECONNECT_BASE_DELAY_MS = 3000; // delay inicial +const RECONNECT_MAX_DELAY_MS = 300000; // teto geral: 5 minutos +// Teto MENOR para erros transientes de rede (DNS, ECONN, ETIMEDOUT). +// Esses erros costumam resolver em segundos; um teto de 5min deixa a +// sessão "dormindo" num setTimeout longo enquanto a rede já voltou. +const RECONNECT_NETWORK_MAX_MS = 60000; // teto rede: 60s +const RECONNECT_MAX_ATTEMPTS = Infinity; +// ─── Network probe — acorda reconexões pendentes quando a rede volta ──────── +// Enquanto uma instância está aguardando backoff, um DNS lookup leve é +// disparado em intervalo fixo. Se o lookup funciona E existe um retry +// agendado para daqui a mais de PROBE_ADVANCE_THRESHOLD_MS, cancela o timer +// e dispara retry imediato. Zero tráfego para o WhatsApp — só DNS. +const NETWORK_PROBE_INTERVAL_MS = 30000; // a cada 30s +const PROBE_ADVANCE_THRESHOLD_MS = 15000; // só adianta se faltar > 15s +const PROBE_DNS_HOST = 'web.whatsapp.com'; +// ─── Watchdog: detecta conexão morta silenciosamente ──────────────────────── +// Conservador por design: reconexões frequentes sinalizam "cliente instável" +// para o WhatsApp e aumentam risco de banimento do número. Por isso: +// - Timeout alto (10 min): conversas calmas são normais, não disparam +// - Intervalo baixo de verificação (3 min): menos overhead +// - Ping leve antes de matar: tenta reviver sem derrubar o socket +// - Cool-down entre kills: evita loop de reconexão se a rede oscila +const WATCHDOG_INTERVAL_MS = 180000; // verifica a cada 3 min +const WATCHDOG_TIMEOUT_MS = 600000; // 10 min sem atividade antes de agir +const WATCHDOG_PING_GRACE_MS = 30000; // aguarda 30s após ping antes de matar +const WATCHDOG_COOLDOWN_MS = 900000; // 15 min entre force-kills consecutivos +class WhatsAppConnectionManager { + constructor(io) { + // ─── Maps indexados por instanceId (UUID) ────────────────────────────── + /** Socket Baileys ativo por instância */ + this.sockets = new Map(); + /** Handler de contatos por instância */ + this.contactHandlers = new Map(); + /** Handler de mensagens por instância */ + this.msgHandlers = new Map(); + /** Contador de tentativas de reconexão (reseta ao conectar com sucesso) */ + this.reconnectAttempts = new Map(); + /** Timer de reconexão pendente (clearTimeout para cancelar) */ + this.reconnectTimers = new Map(); + /** + * Timestamp absoluto (ms) em que o retry pendente vai disparar. + * Usado pelo network probe para decidir se vale adiantar um retry + * que ainda está dormindo num backoff longo. + */ + this.reconnectScheduledAt = new Map(); + /** Timer global do network probe (singleton) */ + this.networkProbeTimer = null; + /** Timer do watchdog (clearInterval para parar) */ + this.watchdogTimers = new Map(); + /** Timestamp da última atividade (qualquer evento Baileys) */ + this.lastActivity = new Map(); + /** Timestamp do último force-kill do watchdog (cool-down anti-loop) */ + this.lastWatchdogKill = new Map(); + /** Timestamp do último ping do watchdog (aguarda resposta antes de matar) */ + this.lastWatchdogPing = new Map(); + /** Mapeamento instanceId → tenantId (necessário para reconexões) */ + this.tenantByInstance = new Map(); + this.io = io; + this.chatbotService = new chatbot_service_1.ChatbotService(io); + this.startNetworkProbe(); + } + // ═══════════════════════════════════════════════════════════════════════════ + // CONEXÃO PRINCIPAL + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Conecta (ou reconecta) uma instância WhatsApp. + * + * Fluxo: + * 1. Carrega auth state do disco (useMultiFileAuthState) + * 2. Cria socket Baileys com configuração padrão + * 3. Registra TODOS os event listeners + * 4. Baileys inicia handshake com WhatsApp Web + * 5. Se precisar de QR: emite evento 'qr' via Socket.IO + * 6. Se já autenticado: connection 'open' dispara sync + * + * IMPORTANTE: Este método é idempotente — se a instância já está + * conectada, retorna sem fazer nada (evita sockets duplicados). + */ + async connect(instanceId, tenantId) { + if (this.sockets.has(instanceId)) { + // Socket já existe — re-emite o status atual para sincronizar o frontend + // (acontece quando o backend reconectou em background antes do frontend abrir) + const cachedStatus = await dragonfly_1.dragonfly.get(CACHE_KEY_STATUS(instanceId)); + const currentStatus = cachedStatus ?? client_1.InstanceStatus.CONNECTED; + logger_1.logger.info({ instanceId, currentStatus }, 'Instância já conectada — re-emitindo status para o frontend'); + this.io.to(`tenant:${tenantId}`).emit('instance:status', { + instanceId, + status: currentStatus, + }); + return; + } + // Limpa timers pendentes de reconexão anterior + this.clearReconnectTimer(instanceId); + this.tenantByInstance.set(instanceId, tenantId); + // ── Auth state: credenciais persistidas em disco ────────────────────── + // Cada instância tem seu diretório em BAILEYS_SESSIONS_PATH/{instanceId}/ + // Contém: creds.json, app-state-sync-key-*, pre-key-*, etc. + const sessionDir = path_1.default.resolve(env_1.env.BAILEYS_SESSIONS_PATH, instanceId); + try { + await promises_1.default.mkdir(sessionDir, { recursive: true }); + } + catch (err) { + logger_1.logger.error({ err, instanceId, sessionDir }, 'Falha ao criar diretório de sessão'); + await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.DISCONNECTED); + this.io.to(`tenant:${tenantId}`).emit('instance:status', { instanceId, status: client_1.InstanceStatus.DISCONNECTED }); + return; + } + const { state, saveCreds } = await (0, baileys_1.useMultiFileAuthState)(sessionDir); + // fetchLatestBaileysVersion faz requisição HTTP ao WhatsApp — pode falhar + // se não houver internet. Usa versão embutida como fallback seguro. + let version = [2, 3000, 1023504162]; + try { + const fetched = await (0, baileys_1.fetchLatestBaileysVersion)(); + version = fetched.version; + } + catch (err) { + logger_1.logger.warn({ instanceId }, 'fetchLatestBaileysVersion falhou — usando versão embutida como fallback'); + } + logger_1.logger.info({ instanceId, version }, 'Iniciando socket Baileys'); + // ── Cria socket Baileys ────────────────────────────────────────────── + const sock = (0, baileys_1.makeWASocket)({ + version, + auth: { + creds: state.creds, + // Cache de chaves Signal para performance (evita leituras de disco) + keys: (0, baileys_1.makeCacheableSignalKeyStore)(state.keys, logger_1.logger), + }, + printQRInTerminal: false, + qrTimeout: 60000, + logger: logger_1.logger.child({ instanceId }), + browser: ['NewWhats', 'Chrome', '124.0.0'], + syncFullHistory: true, // Solicita histórico completo ao telefone + markOnlineOnConnect: true, // Marca como "online" ao conectar + // getMessage: callback para reenvio de mensagens (retry) + // O Baileys chama quando precisa reenviar uma mensagem que falhou + getMessage: async (key) => { + if (!key.id) + return undefined; + const msg = await prisma_1.prisma.message.findUnique({ + where: { instanceId_messageId: { instanceId, messageId: key.id } }, + }); + if (!msg?.body) + return undefined; + return { conversation: msg.body }; + }, + }); + this.sockets.set(instanceId, sock); + this.touchActivity(instanceId); + // ═════════════════════════════════════════════════════════════════════ + // REGISTRO DE EVENT LISTENERS DO BAILEYS + // ═════════════════════════════════════════════════════════════════════ + // ─── Credenciais: salvar ao atualizar ──────────────────────────────── + // O Baileys atualiza credenciais periodicamente (rotação de chaves Signal) + sock.ev.on('creds.update', saveCreds); + // ─── Estado de conexão ─────────────────────────────────────────────── + // Eventos: qr (novo QR gerado), open (conectado), close (desconectado), + // connecting (tentando conectar) + sock.ev.on('connection.update', (update) => { + this.touchActivity(instanceId); + this.baileysLog(tenantId, instanceId, 'connection.update', { + connection: update.connection, + qr: update.qr ? '(qr gerado)' : undefined, + lastDisconnect: update.lastDisconnect + ? { statusCode: update.lastDisconnect.error?.output?.statusCode } + : undefined, + }); + this.handleConnectionUpdate(instanceId, tenantId, update, sock); + }); + // ─── Mensagens recebidas (tempo real + histórico) ──────────────────── + // type 'notify': mensagem em tempo real → processMessage completo + // type 'append': histórico sync → handleHistory (bulk, sem socket) + const msgHandler = new MessageHandler_1.MessageHandler(sock, instanceId, tenantId, this.io, this.chatbotService); + this.msgHandlers.set(instanceId, msgHandler); + sock.ws.on('CB:message', (node) => { + const msgId = node?.attrs?.id; + const peerRecipientPn = node?.attrs?.peer_recipient_pn; + if (msgId && peerRecipientPn) { + msgHandler.setPeerRecipientHint(msgId, peerRecipientPn); + } + }); + sock.ev.on('messages.upsert', (m) => { + this.touchActivity(instanceId); + // Log de debug para monitoramento via página /baileys + for (const msg of m.messages ?? []) { + const text = msg.message?.conversation ?? + msg.message?.extendedTextMessage?.text ?? + msg.message?.imageMessage?.caption ?? + msg.message?.videoMessage?.caption ?? + null; + this.baileysLog(tenantId, instanceId, 'message', { + fromMe: msg.key?.fromMe, + remoteJid: msg.key?.remoteJid, + body: text, + type: Object.keys(msg.message ?? {}).find(k => k !== 'messageContextInfo') ?? 'unknown', + status: msg.status, + }); + // Log de monitoramento para mensagens LID + // Ajuda a verificar se key.senderPn está presente para resolução + if (msg.key?.remoteJid?.endsWith('@lid')) { + logger_1.logger.debug({ + instanceId, + remoteJid: msg.key.remoteJid, + senderPn: msg.key.senderPn, + pushName: msg.pushName, + }, '[LID] Mensagem @lid recebida — será resolvida pelo MessageHandler'); + } + } + // A persistência do mapeamento LID→phone vive dentro do + // MessageHandler.processMessage (no upsert do Contact), porque + // updateMany aqui afetava 0 rows em contatos inexistentes — bug + // silencioso que descartava o mapeamento na primeira mensagem LID. + // Delega processamento completo para o MessageHandler + msgHandler.handle(m); + }); + // ─── Status de mensagens (sent → delivered → read) ─────────────────── + // O WhatsApp envia atualizações de status conforme a mensagem é entregue + // e lida. Mapeamos os códigos numéricos para strings legíveis. + sock.ev.on('messages.update', (updates) => { + this.touchActivity(instanceId); + for (const { key, update } of updates) { + if (!key.id || !update.status) + continue; + const statusMap = { + 0: 'PENDING', 1: 'PENDING', 2: 'SENT', 3: 'DELIVERED', 4: 'READ', 5: 'READ', + }; + const status = statusMap[update.status]; + if (!status) + continue; + // Atualiza no banco e emite para o frontend (ticks azuis/cinza) + prisma_1.prisma.message.updateMany({ + where: { instanceId, messageId: key.id }, + data: { status: status }, + }).then((r) => { + if (r.count === 0) + return; + this.io.to(`tenant:${tenantId}`).emit('message:status', { + messageId: key.id, + status, + }); + hook_bus_1.hookBus.emit('ext:message.update', { + instanceId, + tenantId, + messageId: key.id, + status, + timestamp: Date.now(), + }).catch(() => { }); + }).catch(() => { }); + } + }); + // ─── Contatos ──────────────────────────────────────────────────────── + // contacts.upsert: lista completa de contatos (conexão inicial/sync) + // contacts.update: atualizações parciais (nome alterado, etc) + const contactHandler = new ContactHandler_1.ContactHandler(instanceId, tenantId, sock, this.io); + this.contactHandlers.set(instanceId, contactHandler); + sock.ev.on('contacts.upsert', (contacts) => { + this.touchActivity(instanceId); + this.baileysLog(tenantId, instanceId, 'contacts.upsert', { count: contacts.length }); + logger_1.logger.info({ instanceId, count: contacts.length }, '[contacts.upsert] Recebido'); + contactHandler.upsert(contacts); + }); + sock.ev.on('contacts.update', (contacts) => { + this.touchActivity(instanceId); + this.baileysLog(tenantId, instanceId, 'contacts.update', { count: contacts.length }); + contactHandler.update(contacts); + }); + // ─── LID → Phone mapping (chats.phoneNumberShare) ──────────────────── + // Evento emitido quando um contato envia uma mensagem de protocolo + // SHARE_PHONE_NUMBER, revelando o telefone real por trás do LID. + // + // Este é um dos mecanismos para construir o mapeamento LID → phone. + // Outros mecanismos: + // - key.senderPn nas mensagens (MessageHandler.resolveLidToPhoneJid) + // - campo `lid` nos contatos do history sync (ContactHandler.upsert) + // + // @see MessageHandler.resolveLidToPhoneJid() — consome este mapeamento + sock.ev.on('chats.phoneNumberShare', ({ lid, jid: phoneJid }) => { + this.touchActivity(instanceId); + logger_1.logger.info({ instanceId, lid, phoneJid }, '[LID] phoneNumberShare recebido'); + if (lid && phoneJid) { + // Salva o mapeamento LID no contato que já existe com o phone JID + prisma_1.prisma.contact.updateMany({ + where: { tenantId, instanceId, jid: phoneJid }, + data: { lidJid: lid }, + }).catch(() => { }); + // Tenta corrigir contatos que foram criados com JID = LID + // (pode acontecer se o contato chegou antes do phoneNumberShare) + prisma_1.prisma.contact.updateMany({ + where: { tenantId, instanceId, jid: lid }, + data: { jid: phoneJid, phone: phoneJid.split('@')[0], lidJid: lid }, + }).catch(() => { }); + } + }); + // ─── Chats ─────────────────────────────────────────────────────────── + // chats.upsert: novos chats (ou chats que voltaram da lista) + // chats.update: atualizações (unread count, pin, archive, etc) + sock.ev.on('chats.upsert', (chats) => { + this.touchActivity(instanceId); + this.baileysLog(tenantId, instanceId, 'chats.upsert', { count: chats.length }); + this.syncChats(instanceId, tenantId, chats).catch(() => { }); + }); + sock.ev.on('chats.update', (chats) => { + this.touchActivity(instanceId); + this.baileysLog(tenantId, instanceId, 'chats.update', { count: chats.length }); + this.handleChatsUpdate(instanceId, tenantId, chats).catch(() => { }); + }); + // ─── Grupos ────────────────────────────────────────────────────────── + // groups.upsert: grupo criado ou entrou em novo grupo + // groups.update: subject (nome) ou metadata alterados + // Convertemos para o formato de chat e sincronizamos via syncChats() + sock.ev.on('groups.upsert', (groups) => { + this.touchActivity(instanceId); + this.baileysLog(tenantId, instanceId, 'groups.upsert', { count: groups.length }); + const chatsFromGroups = groups.map((g) => ({ + id: g.id, + name: g.subject, + conversationTimestamp: g.creation, + unreadCount: 0, + pinned: false, + archived: false, + })); + this.syncChats(instanceId, tenantId, chatsFromGroups).catch(() => { }); + }); + sock.ev.on('groups.update', (updates) => { + this.touchActivity(instanceId); + this.baileysLog(tenantId, instanceId, 'groups.update', { count: updates.length }); + const chatsFromUpdates = updates + .filter((g) => g.id && g.subject) + .map((g) => ({ id: g.id, name: g.subject })); + this.syncChats(instanceId, tenantId, chatsFromUpdates).catch(() => { }); + }); + // ─── Histórico inicial (messaging-history.set) ─────────────────────── + // Evento principal de sincronização: o telefone envia chats, contatos + // e mensagens em lotes. Pode disparar múltiplas vezes. + // Quando isLatest=true, o sync terminou → dispara reconciliação de nomes. + sock.ev.on('messaging-history.set', async ({ chats: histChats, contacts: histContacts, messages: histMessages, isLatest }) => { + this.touchActivity(instanceId); + logger_1.logger.info({ + instanceId, + chats: histChats.length, + contacts: histContacts.length, + messages: histMessages.length, + isLatest, + }, '[History] messaging-history.set recebido'); + this.baileysLog(tenantId, instanceId, 'messaging-history.set', { + chats: histChats.length, + contacts: histContacts.length, + messages: histMessages.length, + isLatest, + }); + // Processa na ordem: contatos → chats → mensagens + // (contatos primeiro para que o vínculo Chat→Contact funcione) + if (histContacts.length > 0) { + await contactHandler.upsert(histContacts).catch(() => { }); + } + if (histChats.length > 0) { + await this.syncChats(instanceId, tenantId, histChats).catch(() => { }); + } + if (histMessages.length > 0) { + await msgHandler.handleHistory(histMessages).catch(() => { }); + } + // Quando isLatest=true, o sync do histórico terminou completamente. + // Inicia reconciliação: busca contatos sem nome e preenche via pushName + // das mensagens recebidas (o history sync não traz pushName nos contatos). + if (isLatest) { + logger_1.logger.info({ instanceId }, '[History] Sync completo (isLatest=true) — iniciando reconciliação de nomes'); + contactHandler.reconcileNamesFromMessages().catch(() => { }); + } + }); + // ── Guard: emite DISCONNECTED se o Baileys lançar erro síncrono ───────── + // makeWASocket pode lançar em casos raros (OpenSSL, auth corrompida, etc.) + // Não há como capturar erros async do Baileys aqui além dos event listeners, + // mas garantimos que o socket foi registrado antes de prosseguir. + if (!this.sockets.has(instanceId)) { + logger_1.logger.error({ instanceId }, 'Socket Baileys não foi registrado após inicialização — marcando como DISCONNECTED'); + await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.DISCONNECTED); + this.io.to(`tenant:${tenantId}`).emit('instance:status', { instanceId, status: client_1.InstanceStatus.DISCONNECTED }); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // HANDLER DE ATUALIZAÇÃO DE CONEXÃO + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Processa eventos de mudança de estado da conexão Baileys. + * + * Estados possíveis: + * - qr: QR Code gerado → emite para o frontend escanear + * - open: conectado com sucesso → inicia sync, watchdog, etc. + * - close: desconectado → decide se reconecta ou para + * - connecting: tentando conectar → atualiza status + * + * DECISÃO DE RECONEXÃO: + * - loggedOut (401) → sessão expirada, limpa arquivos, exige novo QR + * - forbidden (403) → conta banida, para completamente + * - Qualquer outro código → reconecta com backoff exponencial + */ + async handleConnectionUpdate(instanceId, tenantId, update, sock) { + const { connection, lastDisconnect, qr } = update; + // ── QR Code emitido ─────────────────────────────────────────────────── + if (qr) { + logger_1.logger.info({ instanceId }, 'QR Code gerado — aguardando escaneamento'); + const qrBase64 = await qrcode_1.default.toDataURL(qr); + // Salva no cache com TTL de 60s (QR expira rapidamente) + await dragonfly_1.dragonfly.set(CACHE_KEY_QR(instanceId), qrBase64, 60); + await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.QR_PENDING); + // Emite para a sala da instância (página de conexão) E para o tenant (dashboard) + this.io.to(`instance:${instanceId}`).emit('qr', { instanceId, qrBase64 }); + this.io.to(`tenant:${tenantId}`).emit('instance:status', { + instanceId, + status: client_1.InstanceStatus.QR_PENDING, + qrBase64, + }); + // ext: bridge — satélites externos recebem via WS /api/ext/v1/stream + hook_bus_1.hookBus.emit('ext:session.qr', { instanceId, tenantId, qrBase64, timestamp: Date.now() }).catch(() => { }); + } + // ── Conectado com sucesso ───────────────────────────────────────────── + if (connection === 'open') { + logger_1.logger.info({ instanceId }, 'WhatsApp conectado com sucesso'); + // Reseta contadores de reconexão + this.reconnectAttempts.set(instanceId, 0); + this.clearReconnectTimer(instanceId); + // Inicia watchdog para detectar conexão morta + this.startWatchdog(instanceId); + await dragonfly_1.dragonfly.del(CACHE_KEY_QR(instanceId)); + await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.CONNECTED); + // Salva o número de telefone da sessão no banco + // (sock.user.id vem no formato "5511999999999:1@s.whatsapp.net") + const phoneJid = sock.user?.id; + if (phoneJid) { + const phone = phoneJid.split(':')[0].split('@')[0]; + await prisma_1.prisma.instance.update({ where: { id: instanceId }, data: { phone } }) + .catch(() => { }); + } + // Pós-conexão: sincroniza avatar da instância e busca avatares ausentes + const handler = this.contactHandlers.get(instanceId); + if (handler) { + // Avatar da própria instância (foto de perfil do número) + handler.syncInstanceAvatar(instanceId) + .then((avatar) => { + if (avatar) { + this.io.to(`tenant:${tenantId}`).emit('instance:avatar', { instanceId, avatar }); + } + }) + .catch(() => { }); + // Busca avatares ausentes após 5s (dá tempo para o sync inicial) + setTimeout(() => handler.syncMissingAvatars().catch(() => { }), 5000); + // Renova avatares próximos de expirar após 30s (não compete com syncMissing) + setTimeout(() => handler.syncExpiredAvatars().catch(() => { }), 30000); + } + // Sync incremental: verifica gap de mensagens durante downtime + this.syncMissedMessages(instanceId, tenantId).catch(() => { }); + this.io.to(`tenant:${tenantId}`).emit('instance:status', { + instanceId, + status: client_1.InstanceStatus.CONNECTED, + }); + hook_bus_1.hookBus.emit('ext:session.status', { instanceId, tenantId, status: 'connected', timestamp: Date.now() }).catch(() => { }); + } + // ── Desconectado ────────────────────────────────────────────────────── + if (connection === 'close') { + const rawErr = lastDisconnect?.error; + const error = rawErr?.output; + const statusCode = error?.statusCode; + const isNetworkErr = this.isTransientNetworkError(rawErr); + logger_1.logger.warn({ instanceId, statusCode, isNetworkErr }, 'Conexao encerrada'); + // Limpa referências da instância desconectada + this.sockets.delete(instanceId); + this.contactHandlers.delete(instanceId); + this.msgHandlers.delete(instanceId); + this.stopWatchdog(instanceId); + // Decide se deve reconectar baseado no código de erro + const shouldReconnect = statusCode !== baileys_1.DisconnectReason.loggedOut && + statusCode !== baileys_1.DisconnectReason.forbidden; + if (shouldReconnect) { + const attempts = (this.reconnectAttempts.get(instanceId) ?? 0) + 1; + this.reconnectAttempts.set(instanceId, attempts); + // Backoff exponencial: 3s, 6s, 12s, 24s, 48s… + // Teto menor (60s) para erros de rede transientes: DNS/ECONN se + // resolvem em segundos, não faz sentido dormir 5min. Teto geral + // (5min) fica para erros de servidor/protocolo do WhatsApp, onde + // paciência é melhor do que pressa. + const maxDelay = isNetworkErr ? RECONNECT_NETWORK_MAX_MS : RECONNECT_MAX_DELAY_MS; + const delay = Math.min(RECONNECT_BASE_DELAY_MS * Math.pow(2, attempts - 1), maxDelay); + const scheduledAt = Date.now() + delay; + const nextAt = new Date(scheduledAt).toISOString(); + logger_1.logger.info({ instanceId, attempts, delayMs: delay, nextAt, isNetworkErr }, '[Reconexao] Agendando reconexao com backoff exponencial'); + await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.CONNECTING); + this.io.to(`tenant:${tenantId}`).emit('instance:status', { + instanceId, + status: client_1.InstanceStatus.CONNECTING, + reconnectIn: delay, + attempt: attempts, + }); + this.scheduleReconnect(instanceId, tenantId, delay); + } + else { + // loggedOut → limpa sessão e exige novo QR + if (statusCode === baileys_1.DisconnectReason.loggedOut) { + logger_1.logger.warn({ instanceId }, 'Sessao desconectada pelo usuario — limpando sessao'); + await this.deleteSessionFiles(instanceId); + } + // forbidden → conta banida, apenas atualiza status + await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.DISCONNECTED); + this.io.to(`tenant:${tenantId}`).emit('instance:status', { + instanceId, + status: client_1.InstanceStatus.DISCONNECTED, + reason: baileys_1.DisconnectReason[statusCode] ?? statusCode, + }); + hook_bus_1.hookBus.emit('ext:session.status', { instanceId, tenantId, status: 'disconnected', timestamp: Date.now() }).catch(() => { }); + } + } + // ── Conectando ──────────────────────────────────────────────────────── + if (connection === 'connecting') { + await this.updateInstanceStatus(instanceId, client_1.InstanceStatus.CONNECTING); + this.io.to(`tenant:${tenantId}`).emit('instance:status', { + instanceId, + status: client_1.InstanceStatus.CONNECTING, + }); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // SYNC INCREMENTAL — Mensagens perdidas durante downtime + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Verifica e notifica sobre gap de mensagens durante downtime. + * + * Consulta a última mensagem no banco e calcula quanto tempo o servidor + * ficou offline. O Baileys com syncFullHistory=true já solicita as + * mensagens pendentes automaticamente — este método apenas: + * 1. Loga o gap para visibilidade (monitoramento) + * 2. Emite 'instance:syncing' para o frontend mostrar indicador de sync + */ + async syncMissedMessages(instanceId, tenantId) { + try { + const lastMsg = await prisma_1.prisma.message.findFirst({ + where: { instanceId }, + orderBy: { timestamp: 'desc' }, + select: { timestamp: true, messageId: true }, + }); + if (!lastMsg) { + logger_1.logger.info({ instanceId }, '[Sync] Nenhuma mensagem no DB — history sync completo via Baileys'); + return; + } + const gap = Date.now() - lastMsg.timestamp.getTime(); + const gapMinutes = Math.round(gap / 60000); + logger_1.logger.info({ instanceId, lastMsgAt: lastMsg.timestamp, gapMinutes }, '[Sync] Reconectado — gap de mensagens detectado'); + this.baileysLog(tenantId, instanceId, 'sync:gap', { + lastMsgAt: lastMsg.timestamp.toISOString(), + gapMinutes, + }); + // Notifica o frontend que está sincronizando + this.io.to(`tenant:${tenantId}`).emit('instance:syncing', { + instanceId, + gapMinutes, + since: lastMsg.timestamp.toISOString(), + }); + } + catch (err) { + logger_1.logger.error({ err, instanceId }, '[Sync] Erro ao verificar gap de mensagens'); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // WATCHDOG — Detecta conexão morta silenciosamente + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Registra timestamp da última atividade para o watchdog. + * Chamado por TODOS os event listeners do Baileys. + */ + touchActivity(instanceId) { + this.lastActivity.set(instanceId, Date.now()); + } + /** + * Inicia o watchdog para uma instância. + * + * Fluxo conservador em 3 etapas — escolhido pra minimizar reconexões: + * 1. Se inativo > TIMEOUT (10 min): envia um presence update como + * ping leve e marca timestamp. Se o socket estiver vivo, Baileys + * responde e touchActivity é chamado no próximo evento. + * 2. Se continua inativo GRACE (+30s) após o ping: confirma que a + * conexão está de fato morta. Procede pro kill. + * 3. Antes de matar: verifica cool-down (15 min desde último kill) + * pra não entrar em loop quando a rede está oscilando. Se estiver + * no cool-down, só loga e espera o próximo ciclo. + * + * Esta sequência dá ~10-11 min de folga antes do primeiro kill, e no + * mínimo 15 min entre kills consecutivos — 7-8× mais conservador que + * a versão anterior (2 min sem verificação de estado). + */ + startWatchdog(instanceId) { + this.stopWatchdog(instanceId); + const timer = setInterval(async () => { + const now = Date.now(); + const lastTs = this.lastActivity.get(instanceId) ?? 0; + const elapsed = now - lastTs; + const tenantId = this.tenantByInstance.get(instanceId); + if (elapsed <= WATCHDOG_TIMEOUT_MS) { + // Conexão com atividade recente — reseta estado de ping/cool-down + this.lastWatchdogPing.delete(instanceId); + return; + } + const sock = this.sockets.get(instanceId); + // Caso 1: socket sumiu sem connection.close — reconecta imediatamente + if (!sock) { + if (tenantId) { + logger_1.logger.warn({ instanceId }, '[Watchdog] Socket ausente — reagendando conexao'); + this.stopWatchdog(instanceId); + this.connect(instanceId, tenantId).catch(() => { }); + } + return; + } + const pingedAt = this.lastWatchdogPing.get(instanceId) ?? 0; + const sincePing = now - pingedAt; + // Etapa 1: envia ping leve (presence update) se ainda não pingou + // neste ciclo de inatividade. Não conta como atividade — só estímulo. + if (pingedAt === 0 || sincePing > WATCHDOG_TIMEOUT_MS) { + try { + await sock.sendPresenceUpdate('available'); + this.lastWatchdogPing.set(instanceId, now); + logger_1.logger.debug({ instanceId, elapsedMs: elapsed }, '[Watchdog] Ping leve enviado — aguardando resposta'); + } + catch (err) { + // Ping falhou → socket já está morto, pula pra etapa de kill + logger_1.logger.warn({ instanceId, err }, '[Watchdog] Ping falhou — socket morto'); + this.forceKillSocket(instanceId, sock, tenantId, elapsed); + } + return; + } + // Etapa 2: ping já foi enviado, mas ainda sem atividade após GRACE. + // Confirma que o socket está zumbi e força reconexão. + if (sincePing > WATCHDOG_PING_GRACE_MS) { + this.forceKillSocket(instanceId, sock, tenantId, elapsed); + } + }, WATCHDOG_INTERVAL_MS); + this.watchdogTimers.set(instanceId, timer); + } + /** + * Força o fechamento do socket, respeitando cool-down para evitar loop. + * Só mata se não houve outro kill nos últimos WATCHDOG_COOLDOWN_MS. + */ + forceKillSocket(instanceId, sock, tenantId, elapsedMs) { + const now = Date.now(); + const lastKill = this.lastWatchdogKill.get(instanceId) ?? 0; + const sinceKill = now - lastKill; + if (sinceKill < WATCHDOG_COOLDOWN_MS) { + logger_1.logger.warn({ instanceId, elapsedMs, sinceLastKillMs: sinceKill }, '[Watchdog] Em cool-down — adiando kill para proteger numero de ban'); + return; + } + logger_1.logger.warn({ instanceId, elapsedMs }, '[Watchdog] Conexao morta confirmada — forcando reconexao'); + this.lastWatchdogKill.set(instanceId, now); + this.lastWatchdogPing.delete(instanceId); + try { + sock.end(new Error('Watchdog timeout')); + } + catch { /* ignore */ } + // Se o sock.end não disparar connection.close (raro), reconecta manualmente + if (!this.sockets.has(instanceId) && tenantId) { + this.stopWatchdog(instanceId); + this.connect(instanceId, tenantId).catch(() => { }); + } + } + /** Para o watchdog de uma instância */ + stopWatchdog(instanceId) { + const timer = this.watchdogTimers.get(instanceId); + if (timer) { + clearInterval(timer); + this.watchdogTimers.delete(instanceId); + } + // Limpa estado de ping pendente — novo ciclo começa limpo. + // NÃO limpa lastWatchdogKill aqui: o cool-down deve sobreviver a + // stop/start para bloquear reconexões em cascata. + this.lastWatchdogPing.delete(instanceId); + } + // ═══════════════════════════════════════════════════════════════════════════ + // TIMER DE RECONEXÃO + // ═══════════════════════════════════════════════════════════════════════════ + /** Cancela timer de reconexão pendente (evita reconexão duplicada) */ + clearReconnectTimer(instanceId) { + const timer = this.reconnectTimers.get(instanceId); + if (timer) { + clearTimeout(timer); + this.reconnectTimers.delete(instanceId); + } + this.reconnectScheduledAt.delete(instanceId); + } + /** + * Agenda uma reconexão após `delay` ms. + * + * Centraliza o setTimeout + tracking do scheduledAt (usado pelo network + * probe pra saber se vale adiantar). Cancela qualquer timer anterior. + */ + scheduleReconnect(instanceId, tenantId, delay) { + this.clearReconnectTimer(instanceId); + const scheduledAt = Date.now() + delay; + this.reconnectScheduledAt.set(instanceId, scheduledAt); + const timer = setTimeout(() => { + this.reconnectScheduledAt.delete(instanceId); + this.connect(instanceId, tenantId); + }, delay); + this.reconnectTimers.set(instanceId, timer); + } + /** + * Classifica um erro de conexão como "rede transiente". + * + * Erros de DNS/TCP costumam resolver em segundos. Erros de status do + * WhatsApp (loggedOut, conflict, restartRequired) são categoria diferente + * e devem usar o backoff longo tradicional. + */ + isTransientNetworkError(err) { + if (!err) + return false; + const code = err.code || err.cause?.code || err.errno; + if (code && typeof code === 'string') { + const transient = [ + 'EAI_AGAIN', // DNS retry + 'ENOTFOUND', // DNS não resolveu + 'ECONNRESET', + 'ECONNREFUSED', + 'ETIMEDOUT', + 'EHOSTUNREACH', + 'ENETUNREACH', + 'ENETDOWN', + 'EPIPE', + ]; + if (transient.includes(code)) + return true; + } + // Fallback: mensagem contém nome do host — é quase certo que é DNS/WS + const msg = (err.message || '').toString(); + if (/getaddrinfo|web\.whatsapp\.com|WebSocket Error/i.test(msg)) + return true; + return false; + } + // ═══════════════════════════════════════════════════════════════════════════ + // NETWORK PROBE — Acorda reconexões quando a rede volta + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Inicia o network probe global. + * + * A cada NETWORK_PROBE_INTERVAL_MS, testa se o DNS resolve + * `web.whatsapp.com`. Se resolver E houver alguma instância com retry + * agendado para daqui a mais de PROBE_ADVANCE_THRESHOLD_MS, cancela o + * timer dessa instância e dispara o retry imediatamente. + * + * Por quê DNS em vez de HTTP? Zero bytes para o WhatsApp. DNS falha com + * EAI_AGAIN quando a rede está caída, e resolve rápido quando volta — + * é um sinal confiável de disponibilidade de rede sem tocar no WA. + */ + startNetworkProbe() { + if (this.networkProbeTimer) + return; + this.networkProbeTimer = setInterval(async () => { + // Curto-circuito: se não há retries pendentes, nem faz o DNS. + if (this.reconnectScheduledAt.size === 0) + return; + try { + await promises_2.default.lookup(PROBE_DNS_HOST); + } + catch { + return; // Rede ainda caída — não adianta nada, espera o tick seguinte. + } + // DNS OK → adianta qualquer retry que ainda falte > threshold + const now = Date.now(); + for (const [instanceId, scheduledAt] of this.reconnectScheduledAt) { + const remaining = scheduledAt - now; + if (remaining <= PROBE_ADVANCE_THRESHOLD_MS) + continue; + const tenantId = this.tenantByInstance.get(instanceId); + if (!tenantId) + continue; + logger_1.logger.info({ instanceId, remainingMs: remaining }, '[NetworkProbe] Rede detectada — adiantando retry pendente'); + this.clearReconnectTimer(instanceId); + // Dispara sem await — probe roda em interval e não deve bloquear + this.connect(instanceId, tenantId).catch((err) => logger_1.logger.error({ err, instanceId }, '[NetworkProbe] Falha ao adiantar retry')); + } + }, NETWORK_PROBE_INTERVAL_MS); + // Não segura o processo vivo só por causa do probe + this.networkProbeTimer.unref?.(); + } + // ═══════════════════════════════════════════════════════════════════════════ + // DESCONEXÃO MANUAL + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Desconecta uma instância manualmente (logout). + * + * Diferente de uma desconexão por erro — aqui o usuário está + * intencionalmente desconectando. Limpa todos os timers e + * faz logout no WhatsApp (invalidando a sessão). + */ + async disconnect(instanceId) { + const sock = this.sockets.get(instanceId); + if (!sock) + return; + this.clearReconnectTimer(instanceId); + this.stopWatchdog(instanceId); + this.reconnectAttempts.delete(instanceId); + // Logout manual zera o estado de cool-down: reconexão futura é um + // novo ciclo e não deve herdar o bloqueio do watchdog. + this.lastWatchdogKill.delete(instanceId); + this.lastActivity.delete(instanceId); + await sock.logout(); + this.sockets.delete(instanceId); + this.contactHandlers.delete(instanceId); + this.msgHandlers.delete(instanceId); + logger_1.logger.info({ instanceId }, 'Instancia desconectada manualmente'); + } + // ═══════════════════════════════════════════════════════════════════════════ + // ACESSORES PÚBLICOS + // ═══════════════════════════════════════════════════════════════════════════ + /** Retorna o socket Baileys ativo para uma instância (usado pelas rotas de envio) */ + getSocket(instanceId) { + return this.sockets.get(instanceId); + } + /** Retorna o Socket.IO server (usado pelas rotas para emitir eventos) */ + getIO() { + return this.io; + } + // ═══════════════════════════════════════════════════════════════════════════ + // HELPERS INTERNOS + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Atualiza o status da instância no banco E no cache. + * Dupla escrita para que o frontend possa consultar status tanto + * via API (banco) quanto via polling rápido (cache DragonflyDB). + */ + async updateInstanceStatus(instanceId, status) { + await Promise.all([ + prisma_1.prisma.instance.update({ where: { id: instanceId }, data: { status } }) + .catch((err) => { if (err?.code !== 'P2025') + throw err; }), + dragonfly_1.dragonfly.set(CACHE_KEY_STATUS(instanceId), status), + ]); + } + /** Remove arquivos de sessão do disco (após loggedOut) */ + async deleteSessionFiles(instanceId) { + const sessionDir = path_1.default.resolve(env_1.env.BAILEYS_SESSIONS_PATH, instanceId); + await promises_1.default.rm(sessionDir, { recursive: true, force: true }); + } + // ═══════════════════════════════════════════════════════════════════════════ + // SINCRONIZAÇÃO DE CHATS + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Sincroniza lista de chats no banco. + * + * Chamado por: chats.upsert, chats.update, groups.upsert, groups.update, + * messaging-history.set + * + * Para cada chat: + * - Cria ou atualiza no banco com JID, nome (grupo), timestamps + * - Vincula ao Contact existente (se houver) + * - Sincroniza estado de pin/archive/unread do WhatsApp + * + * Otimização: busca todos os contatos existentes em uma única query + * e usa Map para lookup O(1) por JID. + */ + async syncChats(instanceId, tenantId, chats) { + if (chats.length === 0) + return; + // Busca todos os contatos existentes para os JIDs (1 query) + const jids = chats.map((c) => c.id).filter(Boolean); + const existingContacts = await prisma_1.prisma.contact.findMany({ + where: { tenantId, instanceId, jid: { in: jids } }, + select: { id: true, jid: true }, + }); + const contactIdByJid = new Map(existingContacts.map((c) => [c.jid, c.id])); + // Pré-carrega mapeamento LID → phone JID para chats @lid. + // ContactHandler.upsert já rodou antes (messaging-history.set processa + // contacts → chats → messages nessa ordem), então lidJid já está populado. + const lidJids = jids.filter((j) => j.endsWith('@lid')); + const lidToPhoneJid = new Map(); // @lid → @s.whatsapp.net + const lidToContactId = new Map(); // @lid → contactId + if (lidJids.length > 0) { + const resolvedByLid = await prisma_1.prisma.contact.findMany({ + where: { + tenantId, + instanceId, + lidJid: { in: lidJids }, + NOT: { jid: { endsWith: '@lid' } }, // só aceita se resolveu para phone JID + }, + select: { id: true, jid: true, lidJid: true }, + }); + for (const rc of resolvedByLid) { + if (rc.lidJid) { + lidToPhoneJid.set(rc.lidJid, rc.jid); + lidToContactId.set(rc.lidJid, rc.id); + } + } + } + for (const chat of chats) { + const rawJid = chat.id; + if (!rawJid) + continue; + // Descarta JIDs de sistema: broadcasts, newsletters e conta PSA (0@s.whatsapp.net) + if (rawJid.includes('@broadcast') || rawJid.endsWith('@newsletter') || rawJid.startsWith('0@')) + continue; + // ── Resolução LID → phone JID ───────────────────────────────────── + // Chats @lid são artefatos do protocolo WhatsApp multi-device: + // o JID real do contato é @s.whatsapp.net. + // Se já temos o mapeamento (ContactHandler rodou antes), usa o phone JID. + // Se não temos resolução, descarta — seria filtrado pelo ChatRepository. + let jid = rawJid; + if (rawJid.endsWith('@lid')) { + const phoneJid = lidToPhoneJid.get(rawJid); + if (phoneJid) { + jid = phoneJid; + // Garante que o contactId mapeado seja usado abaixo + const resolvedContactId = lidToContactId.get(rawJid) ?? contactIdByJid.get(jid); + if (resolvedContactId) + contactIdByJid.set(jid, resolvedContactId); + } + else { + // Sem resolução disponível: chat será criado via handleHistory quando + // a mensagem chegar com senderPn resolvível. + logger_1.logger.debug({ rawJid, instanceId }, '[syncChats] @lid sem resolução — descartando'); + continue; + } + } + try { + // Converte timestamp Unix (segundos) para Date + const lastAt = chat.conversationTimestamp + ? new Date(Number(chat.conversationTimestamp) * 1000) + : undefined; + const isGroup = jid.endsWith('@g.us'); + // Nome do chat: apenas grupos têm nome (subject) — individuais usam Contact.name + const groupName = isGroup ? (chat.name ?? null) : null; + const contactId = contactIdByJid.get(jid) ?? null; + await prisma_1.prisma.chat.upsert({ + where: { tenantId_instanceId_jid: { tenantId, instanceId, jid } }, + create: { + tenantId, + instanceId, + jid, + name: groupName, + unreadCount: chat.unreadCount ?? 0, + lastMessageAt: lastAt ?? null, + isPinned: !!chat.pinned, + isArchived: chat.archived ?? false, + ...(contactId && { contactId }), + }, + update: { + ...(groupName && { name: groupName }), + ...(contactId && { contactId }), + // Só aplica campos que vieram explicitamente no evento. + // chats.upsert pode ser parcial (nova mensagem, metadata) — se + // pinned/archived/unreadCount estiverem ausentes, não sobrescreve + // o valor salvo no banco. + ...(chat.unreadCount !== undefined && { unreadCount: chat.unreadCount }), + ...(lastAt && { lastMessageAt: lastAt }), + ...(chat.pinned !== undefined && { isPinned: !!chat.pinned }), + ...(chat.archived !== undefined && { isArchived: !!chat.archived }), + }, + }); + } + catch (err) { + logger_1.logger.error({ err, jid }, '[WhatsApp] Erro ao sincronizar chat'); + } + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // HANDLER DE ATUALIZAÇÕES PARCIAIS DE CHAT (chats.update) + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Processa atualizações parciais de chats (pin, archive, unread). + * + * Diferente de syncChats (usado para bulk/history), este método: + * 1. Só atualiza campos presentes na atualização (sem sobrescrever com defaults) + * 2. Emite chat:upsert via Socket.IO para atualizar o frontend em tempo real + * + * O evento chats.update do Baileys traz objetos parciais — apenas os campos + * que mudaram. Por isso não podemos usar ?? 0 para unreadCount, pois isso + * sobrescreveria o valor correto com zero. + */ + async handleChatsUpdate(instanceId, tenantId, updates) { + for (const update of updates) { + const jid = update.id; + if (!jid || jid.endsWith('@lid')) + continue; + try { + // Monta patch com apenas os campos presentes na atualização + const patch = {}; + if (update.pinned !== undefined) + patch.isPinned = !!update.pinned; + if (update.archived !== undefined) + patch.isArchived = !!update.archived; + if (update.unreadCount !== undefined) + patch.unreadCount = update.unreadCount; + if (Object.keys(patch).length === 0) + continue; + const chat = await prisma_1.prisma.chat.update({ + where: { tenantId_instanceId_jid: { tenantId, instanceId, jid } }, + data: patch, + include: { + contact: { + select: { + name: true, verifiedName: true, notify: true, + phone: true, avatarUrl: true, + scoreReputacao: true, flagRestricao: true, + }, + }, + }, + }).catch(() => null); + if (!chat) + continue; + // Emite para o frontend atualizar em tempo real + const c = chat.contact; + const isGroup = jid.endsWith('@g.us'); + const displayName = isGroup + ? (chat.name ?? null) + : (c?.name ?? c?.verifiedName ?? c?.notify ?? null); + this.io.to(`tenant:${tenantId}`).emit('chat:upsert', { + id: chat.id, + tenantId: chat.tenantId, + instanceId: chat.instanceId, + jid: chat.jid, + name: chat.name ?? null, + unreadCount: chat.unreadCount, + lastMessageAt: chat.lastMessageAt, + isPinned: chat.isPinned, + isArchived: chat.isArchived, + botPaused: chat.botPaused, + contact: c + ? { + name: displayName, + phone: c.phone, + avatarUrl: c.avatarUrl, + scoreReputacao: c.scoreReputacao, + flagRestricao: c.flagRestricao, + } + : isGroup + ? { name: displayName, phone: null, avatarUrl: null, scoreReputacao: 0, flagRestricao: false } + : null, + lastMessage: null, + }); + } + catch (err) { + logger_1.logger.error({ err, jid }, '[WhatsApp] Erro ao aplicar chats.update'); + } + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // DEBUG LOG (página /baileys no frontend) + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Emite evento de debug para a página /baileys do frontend. + * + * Permite monitorar em tempo real todos os eventos Baileys + * sem precisar acessar os logs do servidor. + */ + baileysLog(tenantId, instanceId, event, data) { + this.io.to(`tenant:${tenantId}`).emit('baileys:log', { + timestamp: new Date().toISOString(), + sessionId: instanceId, + event, + data, + }); + } +} +exports.WhatsAppConnectionManager = WhatsAppConnectionManager; diff --git a/plugins/ext-api/backend/backend/src/modules/whatsapp/handlers/ContactHandler.js b/plugins/ext-api/backend/backend/src/modules/whatsapp/handlers/ContactHandler.js new file mode 100644 index 0000000..39dec1d --- /dev/null +++ b/plugins/ext-api/backend/backend/src/modules/whatsapp/handlers/ContactHandler.js @@ -0,0 +1,494 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ContactHandler = void 0; +const prisma_1 = require("../../../infra/database/prisma"); +const logger_1 = require("../../../config/logger"); +// ─── Configuração de rate-limit para busca de avatares ────────────────────── +// O WhatsApp bloqueia (ban temporário) se muitas requisições profilePictureUrl +// forem feitas em curto período. Estas constantes controlam o throttling. +/** Quantos avatares buscar em paralelo por lote */ +const AVATAR_BATCH_SIZE = 5; +/** Delay entre lotes de avatares (~1 req/s por JID → abaixo do threshold do WA) */ +const AVATAR_DELAY_MS = 900; +/** Máximo de avatares a buscar no sync pós-conexão (evita overload em contas grandes) */ +const MAX_MISSING_AVATAR_SYNC = 80; +class ContactHandler { + constructor( + /** UUID da instância WhatsApp */ + instanceId, + /** UUID do tenant (isolamento multi-tenant) */ + tenantId, + /** Socket Baileys — usado para profilePictureUrl e resyncAppState */ + sock, + /** Socket.IO — emite eventos de avatar/reconciliação para o frontend */ + io) { + this.instanceId = instanceId; + this.tenantId = tenantId; + this.sock = sock; + this.io = io; + /** Fila de JIDs aguardando busca de avatar */ + this.avatarQueue = []; + /** Flag de mutex simples — evita processamento concorrente da fila */ + this.processingAvatars = false; + } + // ═══════════════════════════════════════════════════════════════════════════ + // RESOLUÇÃO DE NOME + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Regra de ouro para resolução de nome de contato: + * name (agenda do telefone) > verifiedName (conta business) > notify (pushname) + * + * Retorna undefined se nenhum campo estiver disponível. + */ + resolveName(c) { + return c.name ?? c.verifiedName ?? c.notify ?? undefined; + } + // ═══════════════════════════════════════════════════════════════════════════ + // UPSERT EM MASSA (contacts.upsert + messaging-history.set) + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Cria ou atualiza contatos em massa. + * + * Dispara em dois cenários: + * 1. Evento contacts.upsert do Baileys (conexão inicial ou reconexão) + * 2. Campo contacts[] do messaging-history.set (history sync) + * + * Para cada contato: + * - Cria ou atualiza no banco com nome resolvido + * - Vincula chats órfãos (Chat sem contactId) ao contato + * - Enfileira busca de avatar se imgUrl não disponível + * - Captura mapeamento LID → phone JID (campo `lid` do history sync) + * + * TRATAMENTO DE LID: + * O Baileys pode enviar contatos com c.id no formato @lid. + * Quando o contato do history sync inclui campo `jid` (telefone real), + * usamos esse JID como chave primária e salvamos o LID em lidJid. + * Quando não há campo `jid`, o contato é criado com o LID como JID + * (será corrigido quando phoneNumberShare ou senderPn revelar o telefone). + */ + async upsert(contacts) { + /** JIDs que precisam buscar avatar via profilePictureUrl */ + const needFetchAvatar = []; + /** Contador de avatares que já vieram prontos do Baileys (imgUrl direto) */ + let directAvatarCount = 0; + for (const c of contacts) { + if (!c.id) + continue; + const isLid = c.id.endsWith('@lid'); + const isBroadcast = c.id.includes('@broadcast'); + if (isBroadcast) + continue; + // imgUrl pode vir do Baileys com valores especiais: + // - URL string: avatar disponível (salva direto) + // - 'changed': avatar foi alterado (precisa buscar novamente) + // - null/undefined: não disponível (precisa buscar via API) + const imgUrl = c.imgUrl; + // LID mapping: o history sync do Baileys traz campo `lid` nos contatos + // que associa o LID ao contato com JID real (@s.whatsapp.net) + // Ex: { id: "5511999999999@s.whatsapp.net", lid: "123456789@lid" } + const lidFromContact = c.lid; + try { + // Para contatos @lid que possuem campo `jid` (vem do history sync), + // usa o JID real como chave primária. Caso contrário, usa c.id como está. + // Isso evita criar contatos duplicados (um com LID, outro com phone). + const contactJid = isLid && c.jid ? c.jid : c.id; + const contact = await prisma_1.prisma.contact.upsert({ + where: { + tenantId_instanceId_jid: { + tenantId: this.tenantId, + instanceId: this.instanceId, + jid: contactJid, + }, + }, + create: { + tenantId: this.tenantId, + instanceId: this.instanceId, + jid: contactJid, + name: this.resolveName(c) ?? null, + verifiedName: c.verifiedName ?? null, + notify: c.notify ?? null, + phone: contactJid.endsWith('@lid') ? null : contactJid.split('@')[0], + ...(imgUrl && imgUrl !== 'changed' ? { avatarUrl: imgUrl } : {}), + // Salva mapeamento LID: + // Se o contato É um LID (c.id termina em @lid): salva c.id como lidJid + // Se o contato TEM campo lid (history sync): salva lid como lidJid + ...(isLid ? { lidJid: c.id } : lidFromContact ? { lidJid: lidFromContact } : {}), + }, + update: { + name: this.resolveName(c) ?? null, + verifiedName: c.verifiedName ?? null, + notify: c.notify ?? null, + ...(imgUrl && imgUrl !== 'changed' ? { avatarUrl: imgUrl } : {}), + ...(isLid ? { lidJid: c.id } : lidFromContact ? { lidJid: lidFromContact } : {}), + }, + }); + // Garante vínculo Chat ↔ Contact para chats órfãos + // (chats criados antes do contato existir ficam com contactId=null) + await prisma_1.prisma.chat.updateMany({ + where: { + tenantId: this.tenantId, + instanceId: this.instanceId, + jid: c.id, + contactId: null, + }, + data: { contactId: contact.id }, + }); + // ── Lógica de avatar ────────────────────────────────────────────── + // JIDs @lid não suportam profilePictureUrl (retorna 404 no WA) + if (!isLid) { + if (imgUrl && imgUrl !== 'changed') { + // Avatar veio pronto do Baileys — notifica frontend imediatamente + directAvatarCount++; + this.io?.to(`tenant:${this.tenantId}`).emit('contact:avatar', { + instanceId: this.instanceId, + jid: c.id, + avatarUrl: imgUrl, + }); + } + else if (imgUrl === 'changed' || imgUrl === undefined) { + // Precisa buscar via API — adiciona à fila com rate-limit + needFetchAvatar.push(c.id); + } + } + } + catch (err) { + logger_1.logger.error({ err, jid: c.id }, '[ContactHandler] Erro ao upsert contato'); + } + } + logger_1.logger.debug({ + instanceId: this.instanceId, + direct: directAvatarCount, + toFetch: needFetchAvatar.length, + }, '[ContactHandler] Avatares: diretos vs a buscar via API'); + // Enfileira busca de avatares com cap de 50 por batch + // (randomiza para priorizar contatos diferentes a cada sync) + if (this.sock && needFetchAvatar.length > 0) { + const toQueue = needFetchAvatar.length > 50 + ? needFetchAvatar.sort(() => Math.random() - 0.5).slice(0, 50) + : needFetchAvatar; + this.enqueueAvatars(toQueue); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // UPDATE PARCIAL (contacts.update) + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Atualiza campos parciais de contatos existentes. + * + * Dispara no evento contacts.update do Baileys — diferente do upsert, + * aqui só atualizamos os campos que foram explicitamente alterados. + * Não cria contatos novos (se não existir, ignora silenciosamente). + */ + async update(contacts) { + for (const c of contacts) { + if (!c.id) + continue; + const resolvedName = this.resolveName(c); + // Se nenhum campo relevante mudou, pula + if (!resolvedName && c.verifiedName === undefined && c.notify === undefined) + continue; + try { + await prisma_1.prisma.contact.updateMany({ + where: { + tenantId: this.tenantId, + instanceId: this.instanceId, + jid: c.id, + }, + data: { + ...(resolvedName !== undefined && { name: resolvedName }), + ...(c.verifiedName !== undefined && { verifiedName: c.verifiedName }), + ...(c.notify !== undefined && { notify: c.notify }), + }, + }); + } + catch (err) { + logger_1.logger.error({ err, jid: c.id }, '[ContactHandler] Erro ao atualizar contato'); + } + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // AVATAR DA INSTÂNCIA (foto de perfil do número da sessão) + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Busca e salva a foto de perfil do número WhatsApp da instância. + * Chamado pelo WhatsAppConnectionManager após connection 'open'. + */ + async syncInstanceAvatar(instanceId) { + if (!this.sock?.user?.id) + return null; + try { + const url = await this.sock.profilePictureUrl(this.sock.user.id, 'image'); + if (url) { + await prisma_1.prisma.instance.update({ where: { id: instanceId }, data: { avatar: url } }); + logger_1.logger.info({ instanceId }, '[ContactHandler] Avatar da instância atualizado'); + return url; + } + } + catch { + // Avatar privado ou não disponível — normal para contas com privacidade restrita + } + return null; + } + // ═══════════════════════════════════════════════════════════════════════════ + // RECONCILIAÇÃO DE NOMES VIA PUSHNAME + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Reconcilia nomes de contatos após sync do histórico. + * + * PROBLEMA: O history sync do Baileys (messaging-history.set) traz contatos + * sem nome — apenas contas business (~22 de ~3000) vêm com verifiedName. + * Os pushNames só aparecem nas mensagens, não nos contatos do sync. + * + * SOLUÇÃO: Após o sync completo (isLatest=true), busca contatos com + * name=null E notify=null, e preenche usando o pushName da mensagem + * recebida mais recente para cada JID. + * + * FILTROS: Ignora @lid, @broadcast, @newsletter, @g.us (grupos usam subject) + * + * Emite evento `contacts:reconciled` para o frontend re-buscar a lista de chats. + */ + async reconcileNamesFromMessages() { + try { + // Busca contatos "sem nome" — candidatos à reconciliação + const unnamed = await prisma_1.prisma.contact.findMany({ + where: { + tenantId: this.tenantId, + instanceId: this.instanceId, + name: null, + notify: null, + NOT: [ + { jid: { endsWith: '@lid' } }, + { jid: { contains: '@broadcast' } }, + { jid: { endsWith: '@newsletter' } }, + { jid: { endsWith: '@g.us' } }, + ], + }, + select: { id: true, jid: true }, + }); + if (unnamed.length === 0) { + logger_1.logger.info({ instanceId: this.instanceId }, '[ContactHandler] Reconciliação: todos os contatos já possuem nome'); + return; + } + logger_1.logger.info({ instanceId: this.instanceId, count: unnamed.length }, '[ContactHandler] Reconciliação: buscando pushNames nas mensagens'); + let updated = 0; + for (const contact of unnamed) { + // Busca a mensagem recebida mais recente com pushName para este JID + const msg = await prisma_1.prisma.message.findFirst({ + where: { + instanceId: this.instanceId, + remoteJid: contact.jid, + fromMe: false, + pushName: { not: null }, + }, + orderBy: { timestamp: 'desc' }, + select: { pushName: true }, + }); + if (msg?.pushName) { + await prisma_1.prisma.contact.update({ + where: { id: contact.id }, + data: { name: msg.pushName, notify: msg.pushName }, + }); + updated++; + } + } + logger_1.logger.info({ instanceId: this.instanceId, total: unnamed.length, updated }, '[ContactHandler] Reconciliação de nomes concluída'); + // Notifica o frontend para atualizar a lista de chats + if (updated > 0) { + this.io?.to(`tenant:${this.tenantId}`).emit('contacts:reconciled', { + instanceId: this.instanceId, + count: updated, + }); + } + } + catch (err) { + logger_1.logger.error({ err }, '[ContactHandler] Erro na reconciliação de nomes'); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // FORCE CONTACT SYNC (resyncAppState) + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Força re-sync dos contatos via app-state do Baileys. + * + * NOTA: Este método existe mas NÃO é chamado automaticamente. + * Em reconexões, o Baileys não re-envia contacts.upsert. Este método + * força o resync das app-state collections que contêm contatos. + * + * PROBLEMA CONHECIDO: a collection 'regular_low' pode falhar com + * "tried remove, but no previous op" — erro de estado corrompido + * no Baileys. Por isso foi removido do trigger automático pós-conexão. + * + * Pode ser chamado manualmente via API se necessário. + */ + async forceContactSync() { + if (!this.sock) + return; + try { + logger_1.logger.info({ instanceId: this.instanceId }, '[ContactHandler] Forçando resync de contatos via app-state'); + await this.sock.resyncAppState(['critical_unblock_low', 'regular_low', 'regular'], false); + logger_1.logger.info({ instanceId: this.instanceId }, '[ContactHandler] resyncAppState concluído'); + } + catch (err) { + logger_1.logger.warn({ err, instanceId: this.instanceId }, '[ContactHandler] Erro no resyncAppState (não-fatal)'); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // SYNC PÓS-CONEXÃO: AVATARES AUSENTES + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Busca avatares ausentes para chats ativos após reconexão. + * + * Chamado pelo WhatsAppConnectionManager 5s após connection 'open'. + * Cobre o caso em que contacts.upsert não disparou (history sync timeout) + * ou contatos vieram sem imgUrl. + * + * Prioriza chats não-arquivados ordenados por lastMessageAt (mais recentes primeiro). + * Limitado a MAX_MISSING_AVATAR_SYNC (80) para não sobrecarregar a API do WA. + */ + async syncMissingAvatars() { + try { + const missing = await prisma_1.prisma.chat.findMany({ + where: { + tenantId: this.tenantId, + instanceId: this.instanceId, + isArchived: false, + NOT: [ + { jid: { endsWith: '@lid' } }, + { jid: { contains: '@broadcast' } }, + ], + contact: { avatarUrl: null }, + }, + select: { jid: true }, + orderBy: { lastMessageAt: { sort: 'desc', nulls: 'last' } }, + take: MAX_MISSING_AVATAR_SYNC, + }); + if (missing.length === 0) + return; + const jids = missing.map((c) => c.jid); + logger_1.logger.info({ instanceId: this.instanceId, count: jids.length }, '[ContactHandler] Sync pós-conexão: buscando avatares ausentes'); + this.enqueueAvatars(jids); + } + catch (err) { + logger_1.logger.error({ err }, '[ContactHandler] Erro no sync de avatares ausentes'); + } + } + /** + * Re-faz profilePictureUrl para contatos cuja URL CDN do WhatsApp expira + * em menos de 48 horas ou já expirou. + * + * URLs do WA têm parâmetro `oe=` que indica a expiração. + * Sem renovação, quebra silenciosamente no browser após o TTL. + */ + async syncExpiredAvatars() { + try { + const contacts = await prisma_1.prisma.contact.findMany({ + where: { + tenantId: this.tenantId, + instanceId: this.instanceId, + avatarUrl: { not: null }, + NOT: [ + { jid: { endsWith: '@lid' } }, + { jid: { contains: '@broadcast' } }, + ], + }, + select: { jid: true, avatarUrl: true }, + orderBy: { updatedAt: 'asc' }, + take: 200, + }); + const threshold = Math.floor(Date.now() / 1000) + 48 * 3600; // now + 48h + const expiring = contacts + .filter(c => { + const m = c.avatarUrl.match(/[?&]oe=([0-9a-f]+)/i); + if (!m) + return false; + return parseInt(m[1], 16) < threshold; + }) + .map(c => c.jid); + if (expiring.length === 0) + return; + logger_1.logger.info({ instanceId: this.instanceId, count: expiring.length }, '[ContactHandler] Renovando avatares próximos de expirar'); + // Limpa as URLs expiradas antes de re-buscar, para que o browser + // não use URLs inválidas enquanto a fila processa + await prisma_1.prisma.contact.updateMany({ + where: { + tenantId: this.tenantId, + instanceId: this.instanceId, + jid: { in: expiring }, + }, + data: { avatarUrl: null }, + }); + this.enqueueAvatars(expiring.slice(0, MAX_MISSING_AVATAR_SYNC)); + } + catch (err) { + logger_1.logger.error({ err }, '[ContactHandler] Erro no sync de avatares expirados'); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // FILA DE AVATARES COM RATE-LIMIT + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Adiciona JIDs à fila de busca de avatares (com deduplicação). + * + * A fila é processada em lotes de AVATAR_BATCH_SIZE com delay de + * AVATAR_DELAY_MS entre cada lote para respeitar o rate-limit do WhatsApp. + * + * Se a fila já está sendo processada, os novos JIDs são adicionados + * e serão processados na próxima iteração do loop. + */ + enqueueAvatars(jids) { + const inQueue = new Set(this.avatarQueue); + for (const jid of jids) { + if (!inQueue.has(jid)) + this.avatarQueue.push(jid); + } + this.processAvatarQueue(); + } + /** + * Processa a fila de avatares com rate-limit. + * + * Usa Promise.allSettled() para que falhas individuais (foto privada, + * JID inválido) não interrompam o lote. Cada avatar encontrado é: + * 1. Salvo no campo avatarUrl do contato no banco + * 2. Emitido via Socket.IO para atualizar o frontend em tempo real + */ + async processAvatarQueue() { + // Mutex simples: apenas uma execução por vez + if (this.processingAvatars || this.avatarQueue.length === 0) + return; + this.processingAvatars = true; + logger_1.logger.info({ count: this.avatarQueue.length, instanceId: this.instanceId }, '[ContactHandler] Iniciando sync de avatares'); + while (this.avatarQueue.length > 0 && this.sock) { + // Pega o próximo lote (remove da fila) + const batch = this.avatarQueue.splice(0, AVATAR_BATCH_SIZE); + await Promise.allSettled(batch.map(async (jid) => { + try { + const url = await this.sock.profilePictureUrl(jid, 'image'); + if (url) { + // Persiste no banco + await prisma_1.prisma.contact.updateMany({ + where: { tenantId: this.tenantId, instanceId: this.instanceId, jid }, + data: { avatarUrl: url }, + }); + // Notifica o frontend para atualizar o avatar na UI + this.io?.to(`tenant:${this.tenantId}`).emit('contact:avatar', { + instanceId: this.instanceId, + jid, + avatarUrl: url, + }); + } + } + catch { + // Foto privada ou JID inválido — ignora silenciosamente + // (não loga para evitar spam com milhares de contatos) + } + })); + // Delay entre lotes para respeitar rate-limit do WhatsApp + if (this.avatarQueue.length > 0) { + await new Promise((r) => setTimeout(r, AVATAR_DELAY_MS * AVATAR_BATCH_SIZE)); + } + } + this.processingAvatars = false; + logger_1.logger.info({ instanceId: this.instanceId }, '[ContactHandler] Sync de avatares concluído'); + } +} +exports.ContactHandler = ContactHandler; diff --git a/plugins/ext-api/backend/backend/src/modules/whatsapp/handlers/MessageHandler.js b/plugins/ext-api/backend/backend/src/modules/whatsapp/handlers/MessageHandler.js new file mode 100644 index 0000000..b4b69a8 --- /dev/null +++ b/plugins/ext-api/backend/backend/src/modules/whatsapp/handlers/MessageHandler.js @@ -0,0 +1,758 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.MessageHandler = void 0; +/** + * MessageHandler — Processa mensagens recebidas do Baileys (tempo real e histórico). + * + * Responsabilidades: + * 1. Receber eventos `messages.upsert` do Baileys (tipo 'notify' ou 'append') + * 2. Resolver JIDs LID → telefone (@lid → @s.whatsapp.net) + * 3. Criar/atualizar Contact, Chat e Message no banco + * 4. Emitir eventos Socket.IO para o frontend (message:new, chat:upsert) + * 5. Download assíncrono de mídia (imagens, vídeos, docs) + * 6. Disparar chatbot para mensagens recebidas com texto + * + * CONTEXTO LID (Linked ID): + * A partir de 2024, o WhatsApp passou a usar LID como identificador primário + * nas mensagens peer-to-peer. O remoteJid chega no formato "123456@lid" em vez + * de "5511999999999@s.whatsapp.net". O telefone real está disponível em: + * - key.senderPn: campo do protocolo WA contendo o JID com telefone + * - key.participantPn: idem, usado em contextos de grupo + * - DB lookup: tabela contacts.lidJid (mapeamento persistido anteriormente) + * + * @see WhatsAppConnectionManager — registra os event listeners do Baileys + * @see ContactHandler — gerencia contatos e avatares + */ +const path_1 = __importDefault(require("path")); +const promises_1 = __importDefault(require("fs/promises")); +const baileys_1 = require("@whiskeysockets/baileys"); +const client_1 = require("@prisma/client"); +const prisma_1 = require("../../../infra/database/prisma"); +const logger_1 = require("../../../config/logger"); +const whatsapp_1 = require("../../../shared/utils/whatsapp"); +const StorageProvider_1 = require("../../../core/StorageProvider"); +const hook_bus_1 = require("../../../core/hook-bus"); +/** Diretório raiz onde os arquivos de mídia são salvos (organizados por instanceId) */ +const MEDIA_DIR = path_1.default.resolve('./media'); +/** + * Tipos de mensagem do protocolo WhatsApp que NÃO representam conteúdo real para o usuário. + * Mensagens com estes contentTypes são descartadas silenciosamente — sem criar Chat nem Message. + */ +const PROTOCOL_CONTENT_TYPES = new Set([ + 'senderKeyDistributionMessage', // distribuição de chaves de grupo (protocolo) + 'protocolMessage', // revogações, timers de desaparecimento (protocolo) + 'ephemeralMessage', // wrapper de mensagem efêmera (o conteúdo real está dentro) + 'deviceSentMessage', // eco de mensagem enviada de outro dispositivo (protocolo) + 'messageContextInfo', // só metadados, sem conteúdo + 'appStateSyncKeyShare', // sync de estado do app (protocolo) + 'appStateFatalExceptionNotification', // notificação interna (protocolo) + 'keepInChatMessage', // manter no chat (protocolo) +]); +class MessageHandler { + constructor( + /** Socket Baileys ativo — usado para download de mídia e reupload */ + sock, + /** UUID da instância WhatsApp (1 instância = 1 número conectado) */ + instanceId, + /** UUID do tenant (empresa/cliente) — isolamento multi-tenant */ + tenantId, + /** Socket.IO server — emite eventos em tempo real para o frontend */ + io, + /** Serviço de chatbot (opcional) — processa mensagens recebidas com IA */ + chatbotService) { + this.sock = sock; + this.instanceId = instanceId; + this.tenantId = tenantId; + this.io = io; + this.chatbotService = chatbotService; + /** + * Cache de dicas de `peer_recipient_pn` extraídas das stanzas brutas. + * + * Mapa: msgId → phone JID do destinatário (ex: "556799138694@s.whatsapp.net"). + * + * Contexto: quando o usuário envia uma mensagem pelo celular (fromMe=true) + * para um contato que usa LID, o WhatsApp sincroniza a mensagem pra nossa + * sessão com remoteJid=@lid. Nesse caso, `key.senderPn` descreve o DONO da + * sessão (não o destinatário) — inútil pra resolução. A ÚNICA pista do + * telefone real é o atributo `peer_recipient_pn` na stanza bruta, que o + * Baileys NÃO expõe em `decode-wa-message.js`. + * + * Por isso, o WhatsAppConnectionManager registra um listener extra em + * `sock.ws.on('CB:message')` que extrai esse atributo e popula este mapa + * ANTES do Baileys emitir `messages.upsert`. Depois, `resolveLidToPhoneJid` + * consome a dica e remove a entrada (mapa efêmero, não cresce). + */ + this.peerRecipientHints = new Map(); + } + /** + * Registra uma dica de destinatário extraída da stanza bruta. + * + * Chamado pelo listener `sock.ws.on('CB:message')` no + * WhatsAppConnectionManager. Entradas expiram em 60s como garantia + * contra vazamento caso o `messages.upsert` correspondente nunca chegue. + */ + setPeerRecipientHint(msgId, peerRecipientPn) { + this.peerRecipientHints.set(msgId, peerRecipientPn); + setTimeout(() => this.peerRecipientHints.delete(msgId), 60000).unref(); + } + /** + * Ponto de entrada principal — chamado pelo event listener `messages.upsert`. + * + * @param type - 'notify' = mensagem em tempo real; 'append' = histórico sendo sincronizado + * + * Diferença entre os modos: + * - notify: processa completo (salva, emite socket, dispara chatbot, baixa mídia) + * - append: apenas salva no banco em lote (bulk insert), sem emitir eventos + */ + async handle({ messages, type }) { + if (type === 'notify') { + for (const msg of messages) { + try { + await this.processMessage(msg); + } + catch (err) { + logger_1.logger.error({ err, msgId: msg.key.id }, 'Erro ao processar mensagem'); + } + } + } + else if (type === 'append') { + await this.handleHistory(messages); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // HISTÓRICO — Salva mensagens em lote durante sync inicial (sem socket/chatbot) + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Processa mensagens históricas (history sync / append). + * + * Estratégia: agrupa mensagens por JID e faz 1 upsert de Contact + Chat + createMany + * por grupo, minimizando round-trips ao banco. + * + * Mensagens @lid são resolvidas para o JID real via senderPn ou DB lookup. + * Se não for possível resolver, a mensagem é descartada silenciosamente + * (será capturada quando o mapeamento LID for descoberto via phoneNumberShare). + */ + async handleHistory(messages) { + if (messages.length === 0) + return; + // JID próprio da sessão (ex: "5511999@s.whatsapp.net") — usado para filtrar self-chat + const ownJid = (0, whatsapp_1.normalizeJid)(this.sock.user?.id ?? ''); + // Agrupa por JID para minimizar queries (1 chat upsert + 1 createMany por JID) + const byJid = new Map(); + for (const msg of messages) { + let jidRaw = msg.key.remoteJid; + if (!jidRaw || jidRaw.includes('@broadcast') || jidRaw.endsWith('@newsletter') || jidRaw.startsWith('0@') || !msg.message) + continue; + // Ignora mensagens de/para o próprio número da sessão (self-chat / notas pessoais) + const normalizedRaw = (0, whatsapp_1.normalizeJid)(jidRaw); + if (ownJid && normalizedRaw === ownJid) + continue; + // ── Resolução LID → phone JID para mensagens históricas ────────── + // O history sync do WhatsApp pode enviar mensagens com remoteJid @lid. + // Precisamos converter para @s.whatsapp.net antes de persistir, + // caso contrário o chat ficaria duplicado (um com LID, outro com phone). + if (jidRaw.endsWith('@lid')) { + // Tenta key.senderPn (disponível quando o protocolo WA inclui sender_pn) + const senderPn = msg.key.senderPn || msg.key.participantPn; + if (senderPn) { + jidRaw = (0, whatsapp_1.normalizeJid)(senderPn); + if (!jidRaw.endsWith('@s.whatsapp.net')) + continue; + } + else { + // Fallback: busca mapeamento LID → phone já salvo no banco + // (populado por phoneNumberShare ou processMessage anterior) + const contact = await prisma_1.prisma.contact.findFirst({ + where: { tenantId: this.tenantId, instanceId: this.instanceId, lidJid: jidRaw }, + select: { jid: true }, + }); + if (contact?.jid && !contact.jid.endsWith('@lid')) { + jidRaw = contact.jid; + } + else { + // Sem resolução possível, ou contato ainda armazenado com JID @lid + // (o mapeamento phone ainda não chegou via phoneNumberShare / senderPn). + continue; + } + } + } + const jid = (0, whatsapp_1.normalizeJid)(jidRaw); + const list = byJid.get(jid) ?? []; + list.push(msg); + byJid.set(jid, list); + } + let totalSaved = 0; + for (const [jid, msgs] of byJid) { + try { + // Calcula o timestamp mais recente para atualizar lastMessageAt do chat + const latestTs = msgs.reduce((max, m) => Math.max(max, Number(m.messageTimestamp ?? 0)), 0); + const lastAt = latestTs ? new Date(latestTs * 1000) : null; + // 1. Garantir que o Contato existe (cria sem nome — será preenchido pela reconciliação) + const contact = await prisma_1.prisma.contact.upsert({ + where: { tenantId_instanceId_jid: { tenantId: this.tenantId, instanceId: this.instanceId, jid } }, + create: { + tenantId: this.tenantId, + instanceId: this.instanceId, + jid, + phone: jid.endsWith('@g.us') ? null : jid.split('@')[0], + }, + update: {}, + }); + // 2. Garantir que o Chat existe e está vinculado ao Contact + const chat = await prisma_1.prisma.chat.upsert({ + where: { tenantId_instanceId_jid: { tenantId: this.tenantId, instanceId: this.instanceId, jid } }, + create: { + tenantId: this.tenantId, + instanceId: this.instanceId, + jid, + contactId: contact.id, + lastMessageAt: lastAt, + unreadCount: 0 + }, + update: { + contactId: contact.id, + ...(lastAt && { lastMessageAt: lastAt }) + }, + }); + // 3. Prepara registros para bulk insert (createMany com skipDuplicates) + const rows = msgs.flatMap((msg) => { + const { key, message, messageTimestamp } = msg; + if (!message || !key.id) + return []; + const contentType = (0, baileys_1.getContentType)(message); + if (!contentType) + return []; + // Descarta tipos de protocolo/sistema que não representam conteúdo real + if (PROTOCOL_CONTENT_TYPES.has(contentType)) + return []; + const msgType = this.mapContentType(contentType); + if (msgType === client_1.MessageType.UNSUPPORTED) + return []; + const ts = messageTimestamp ? new Date(Number(messageTimestamp) * 1000) : new Date(); + // Extrai o corpo textual — tenta vários campos em ordem de prioridade + const body = message.conversation ?? + message.extendedTextMessage?.text ?? + message.imageMessage?.caption ?? + message.videoMessage?.caption ?? + message.documentMessage?.caption ?? + null; + const isGroup = jid.endsWith('@g.us'); + return [{ + tenantId: this.tenantId, + instanceId: this.instanceId, + chatId: chat.id, + remoteJid: jid, + messageId: key.id, + fromMe: key.fromMe ?? false, + type: msgType, + body, + // pushName: nome público do remetente (só confiável em msgs recebidas) + pushName: (key.fromMe ? null : msg.pushName) ?? null, + // senderJid: em grupos, identifica quem enviou (JID do participante) + senderJid: isGroup ? (key.participant ?? null) : null, + status: 'READ', // históricas são consideradas já lidas + timestamp: ts, + }]; + }); + if (rows.length > 0) { + const { count } = await prisma_1.prisma.message.createMany({ data: rows, skipDuplicates: true }); + totalSaved += count; + } + } + catch (err) { + logger_1.logger.error({ err, jid }, '[History] Erro ao salvar mensagens do chat'); + } + } + if (totalSaved > 0) { + logger_1.logger.info({ totalSaved, chats: byJid.size, instanceId: this.instanceId }, '[History] Mensagens históricas salvas'); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // TEMPO REAL — Processa uma única mensagem (notify) + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Processa uma mensagem recebida em tempo real. + * + * Fluxo completo: + * 1. Filtra broadcasts, newsletters + * 2. Resolve LID → telefone (se @lid) + * 3. Cria/atualiza Contact com pushName (se aplicável) + * 4. Cria/atualiza Chat vinculado ao Contact + * 5. Persiste a Message no banco + * 6. Emite socket events (message:new, chat:upsert) + * 7. Agenda download de mídia (assíncrono) + * 8. Dispara chatbot (assíncrono) + */ + async processMessage(msg) { + const { key, message, messageTimestamp, pushName } = msg; + if (!message || !key.remoteJid) + return; + // Ignora mensagens de status broadcast, newsletters e conta PSA do WhatsApp + if (key.remoteJid === 'status@broadcast') + return; + if (key.remoteJid.endsWith('@newsletter')) + return; + if (key.remoteJid.startsWith('0@')) + return; + // ── Resolução LID → telefone ────────────────────────────────────────── + // WhatsApp moderno usa LID (Linked ID) como remoteJid em vez de @s.whatsapp.net. + // O telefone real está em key.senderPn (quando disponível) ou no mapeamento DB. + // + // SEM esta resolução, mensagens de contatos que usam LID seriam descartadas + // e o chat apareceria com a foto/número errado no frontend (bug crítico). + // + // Prioridade de resolução: + // 1. key.senderPn — campo do protocolo WA (mais confiável, disponível ~95% dos casos) + // 2. key.participantPn — idem, usado em contextos de grupo + // 3. DB lookup — contacts.lidJid (mapeamento salvo por processamento anterior ou phoneNumberShare) + /** + * Guarda o JID @lid original quando a resolução é bem-sucedida, para + * persistir em `contacts.lidJid` mais abaixo (após o upsert do Contact). + * Tentar salvar ANTES do upsert via `updateMany` afeta 0 rows para + * contatos novos — bug silencioso que fazia o mapeamento ser perdido. + */ + let originalLidJid = null; + if (key.remoteJid.endsWith('@lid')) { + const phoneJid = await this.resolveLidToPhoneJid(key.remoteJid, key); + if (!phoneJid) { + // Sem resolução: não há como saber o número real do contato. + // A mensagem será perdida, mas o mapeamento será capturado no futuro + // via chats.phoneNumberShare e as próximas mensagens serão processadas. + logger_1.logger.warn({ remoteJid: key.remoteJid, instanceId: this.instanceId, fromMe: key.fromMe }, '[MessageHandler] @lid sem resolução de telefone — descartando'); + return; + } + originalLidJid = key.remoteJid; + key.remoteJid = phoneJid; + } + const contentType = (0, baileys_1.getContentType)(message); + if (!contentType) + return; + // Descarta tipos de protocolo/sistema (sem conteúdo real para o usuário) + if (PROTOCOL_CONTENT_TYPES.has(contentType)) + return; + const msgType = this.mapContentType(contentType); + // Descarta tipos completamente desconhecidos — evita poluir o DB com UNSUPPORTED + if (msgType === client_1.MessageType.UNSUPPORTED) + return; + const fromMe = key.fromMe ?? false; + const remoteJidRaw = key.remoteJid; + /** JID normalizado (sem sufixo de dispositivo :1, :2 etc) */ + const remoteJid = (0, whatsapp_1.normalizeJid)(remoteJidRaw); + const messageId = key.id; + // Ignora mensagens de/para o próprio número da sessão (self-chat / notas pessoais) + const ownJid = (0, whatsapp_1.normalizeJid)(this.sock.user?.id ?? ''); + if (ownJid && remoteJid === ownJid) + return; + // ── 1. Contact: criar ou atualizar ──────────────────────────────────── + // REGRA IMPORTANTE sobre pushName: + // - pushName é o "nome público" que o remetente escolheu no WhatsApp + // - Para mensagens RECEBIDAS (fromMe=false) de contatos individuais: é confiável + // - Para mensagens ENVIADAS (fromMe=true): pushName é o nome da NOSSA sessão + // → NUNCA sobrescrever o nome do contato destinatário com isso ("efeito espelho") + // - Para GRUPOS: pushName é do remetente individual, não do grupo + // → O nome do grupo vem de groups.upsert → Chat.name (subject) + const isGroupMessage = remoteJid.endsWith('@g.us'); + const participantJid = key.participant ?? msg.participant ?? null; + const safePushName = (!fromMe && !isGroupMessage && pushName) ? pushName : null; + const contact = await prisma_1.prisma.contact.upsert({ + where: { tenantId_instanceId_jid: { tenantId: this.tenantId, instanceId: this.instanceId, jid: remoteJid } }, + create: { + tenantId: this.tenantId, + instanceId: this.instanceId, + jid: remoteJid, + name: safePushName, + notify: safePushName, + phone: isGroupMessage ? null : remoteJid.split('@')[0], + // Se resolvemos a partir de @lid, persiste o mapeamento já na criação. + // Sem isso, o próximo @lid do mesmo contato falharia o DB lookup. + ...(originalLidJid && { lidJid: originalLidJid }), + }, + update: { + // Atualiza apenas pushname (notify) — preserva name se já veio de contacts.upsert + ...(safePushName && { notify: safePushName }), + // Garante que o lidJid fique salvo mesmo em contatos pré-existentes + // que ainda não tinham o mapeamento (ex: criados via history sync). + ...(originalLidJid && { lidJid: originalLidJid }), + }, + }); + // ── 2. Chat: criar ou atualizar, vinculando ao Contact ──────────────── + const chat = await prisma_1.prisma.chat.upsert({ + where: { tenantId_instanceId_jid: { tenantId: this.tenantId, instanceId: this.instanceId, jid: remoteJid } }, + create: { + tenantId: this.tenantId, + instanceId: this.instanceId, + jid: remoteJid, + contactId: contact.id, + unreadCount: fromMe ? 0 : 1, + lastMessageAt: new Date(messageTimestamp * 1000), + }, + update: { + contactId: contact.id, // Garante o vínculo (corrige chats órfãos antigos) + lastMessageAt: new Date(messageTimestamp * 1000), + // fromMe: zera unread (estamos respondendo); !fromMe: incrementa + unreadCount: fromMe ? { set: 0 } : { increment: 1 }, + }, + }); + // ── 3. Extrai corpo textual da mensagem ─────────────────────────────── + // nativeFlowResponseMessage: resposta ao clique de botão/lista + let nativeFlowBody = null; + if (message.interactiveResponseMessage?.nativeFlowResponseMessage) { + try { + const params = JSON.parse(message.interactiveResponseMessage.nativeFlowResponseMessage.paramsJson ?? '{}'); + nativeFlowBody = params.display_text ?? params.title ?? params.id ?? null; + } + catch { /* ignora JSON inválido */ } + } + const body = message.conversation ?? + message.extendedTextMessage?.text ?? + message.imageMessage?.caption ?? + message.videoMessage?.caption ?? + message.documentMessage?.caption ?? + nativeFlowBody ?? + null; + // ── 4. Contexto de reply (se for resposta a outra mensagem) ─────────── + const replyToWaId = message.extendedTextMessage?.contextInfo?.stanzaId ?? null; + let replyToId = null; + if (replyToWaId) { + const replyMsg = await prisma_1.prisma.message.findUnique({ + where: { instanceId_messageId: { instanceId: this.instanceId, messageId: replyToWaId } }, + }); + replyToId = replyMsg?.id ?? null; + } + // ── 5. Persiste a mensagem (mídia será baixada depois) ──────────────── + const saved = await prisma_1.prisma.message.upsert({ + where: { instanceId_messageId: { instanceId: this.instanceId, messageId } }, + create: { + tenantId: this.tenantId, + instanceId: this.instanceId, + chatId: chat.id, + remoteJid, + messageId, + fromMe, + type: msgType, + body, + pushName: fromMe ? null : (pushName ?? null), + senderJid: isGroupMessage ? participantJid : null, + replyToId, + status: fromMe ? 'SENT' : 'PENDING', + timestamp: new Date(messageTimestamp * 1000), + }, + update: {}, // Se já existe (duplicata), não sobrescreve + }); + // ── 6. Emite eventos Socket.IO ──────────────────────────────────────── + // 6a. Para quem está com o chat aberto (sala chat:{id}) + // Inclui replyTo quando é uma resposta — o frontend precisa para exibir + // a caixa de citação (quoted block) em tempo real, sem precisar recarregar. + const emitMsg = replyToId + ? await prisma_1.prisma.message.findUnique({ + where: { id: saved.id }, + include: { + replyTo: { + select: { id: true, messageId: true, body: true, fromMe: true, type: true, mediaUrl: true }, + }, + }, + }) ?? saved + : saved; + this.io.to(`chat:${chat.id}`).emit('message:new', { + ...emitMsg, + pushName, + senderJid: isGroupMessage ? participantJid : null, + }); + // ext: bridge — satélites externos recebem via WS /api/ext/v1/stream + hook_bus_1.hookBus.emit('ext:message.new', { + instanceId: this.instanceId, + tenantId: this.tenantId, + chatId: chat.id, + message: { ...emitMsg, pushName, senderJid: isGroupMessage ? participantJid : null }, + timestamp: Date.now(), + }).catch(() => { }); + // 6b. Para TODOS os clientes do tenant (atualiza a lista de chats) + // Necessário para: novos chats, chats não abertos, badge de unread + this.emitChatUpsert(chat.id, { body, fromMe, status: saved.status, type: msgType, timestamp: saved.timestamp }); + // ── 7. Download de mídia assíncrono (não bloqueia o event loop) ──────── + if (this.isMediaMessage(contentType)) { + setImmediate(() => this.downloadAndPersistMedia(msg, saved.id, chat.id, contentType)); + } + // ── 8. Dispara chatbot para mensagens recebidas com texto ───────────── + if (!fromMe && body && this.chatbotService) { + setImmediate(() => this.chatbotService.handleIncoming({ + tenantId: this.tenantId, + instanceId: this.instanceId, + chatId: chat.id, + jid: remoteJid, + text: body, + sock: this.sock, + }).catch((err) => logger_1.logger.error({ err }, '[Chatbot] Erro assíncrono'))); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // RESOLUÇÃO LID → TELEFONE + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Converte um JID LID (ex: "123456789@lid") para o JID com telefone + * (ex: "5511999999999@s.whatsapp.net"). + * + * O WhatsApp moderno usa LID (Linked ID) como identificador interno. + * Para exibir o contato corretamente, precisamos mapear para o telefone real. + * + * Fontes de resolução (em ordem de prioridade): + * 1. key.senderPn / key.participantPn — campo do protocolo WA. + * Útil para mensagens RECEBIDAS e contextos de grupo. + * 2. peerRecipientHints[msgId] — dica extraída da stanza bruta + * (atributo `peer_recipient_pn`, não exposto pelo Baileys). + * Única fonte confiável para mensagens fromMe=true enviadas pelo + * celular pra um contato @lid que ainda não temos mapeado. + * 3. DB lookup — contacts.lidJid (mapeamento persistido anteriormente). + * Populado por: processamento anterior, chats.phoneNumberShare, + * ou contacts.upsert do history sync. + * + * A persistência do mapeamento LID→phone foi movida para processMessage + * (após o upsert do Contact), para evitar o bug anterior onde o + * `updateMany` disparado aqui afetava 0 rows em contatos inexistentes. + * + * @param lidJid - O JID no formato @lid (ex: "123456789@lid") + * @param key - WAMessageKey (id, fromMe, senderPn, participantPn) + * @returns O JID @s.whatsapp.net correspondente, ou null se não resolver + */ + async resolveLidToPhoneJid(lidJid, key) { + // 1. senderPn vem direto do protocolo WhatsApp (mais confiável) + // OBS: em mensagens fromMe=true sincronizadas do celular, senderPn + // descreve o DONO da sessão, não o destinatário — inútil aqui. + const senderPn = key.senderPn || key.participantPn; + if (senderPn && !key.fromMe) { + const phoneJid = (0, whatsapp_1.normalizeJid)(senderPn); + if (phoneJid && phoneJid.endsWith('@s.whatsapp.net')) { + return phoneJid; + } + } + // 2. Dica do peer_recipient_pn (stanza bruta) — fonte primária para + // mensagens fromMe=true que chegam via sync de outro dispositivo. + if (key.fromMe && key.id) { + const hint = this.peerRecipientHints.get(key.id); + this.peerRecipientHints.delete(key.id); // uso único + if (hint) { + const phoneJid = (0, whatsapp_1.normalizeJid)(hint); + if (phoneJid && phoneJid.endsWith('@s.whatsapp.net')) { + return phoneJid; + } + } + } + // 3. Busca no DB por mapeamento LID salvo anteriormente + const contact = await prisma_1.prisma.contact.findFirst({ + where: { tenantId: this.tenantId, instanceId: this.instanceId, lidJid }, + select: { jid: true }, + }); + if (contact?.jid) + return contact.jid; + return null; + } + // ═══════════════════════════════════════════════════════════════════════════ + // EMISSÃO DE EVENTOS SOCKET.IO + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Emite evento `chat:upsert` para todos os clientes do tenant. + * + * Usado para atualizar a lista de chats no frontend em tempo real: + * - Novos chats aparecem instantaneamente + * - Chats existentes atualizam lastMessage, unreadCount, etc. + * - Chats @lid são filtrados (não devem aparecer no frontend) + * + * O frontend usa este evento para reordenar a lista por lastMessageAt + * e exibir o preview da última mensagem. + */ + async emitChatUpsert(chatId, lastMessage) { + try { + const chat = await prisma_1.prisma.chat.findUnique({ + where: { id: chatId }, + include: { + contact: { + select: { + name: true, + verifiedName: true, + notify: true, + phone: true, + avatarUrl: true, + scoreReputacao: true, + flagRestricao: true, + }, + }, + }, + }); + if (!chat) + return; + // Chats @lid são artefatos internos — nunca expor ao frontend + if (chat.jid.endsWith('@lid')) + return; + const c = chat.contact; + const isGroup = chat.jid.endsWith('@g.us'); + // Regra de resolução de nome: + // Grupos: usa Chat.name (subject do grupo, vem de groups.upsert) + // Individuais: name (agenda) > verifiedName (business) > notify (pushname) + // NUNCA usar pushName do remetente como nome de grupo + const displayName = isGroup + ? (chat.name ?? null) + : (c?.name ?? c?.verifiedName ?? c?.notify ?? null); + this.io.to(`tenant:${this.tenantId}`).emit('chat:upsert', { + id: chat.id, + tenantId: chat.tenantId, + instanceId: chat.instanceId, + jid: chat.jid, + name: chat.name ?? null, + unreadCount: chat.unreadCount, + lastMessageAt: chat.lastMessageAt, + isPinned: chat.isPinned, + isArchived: chat.isArchived, + botPaused: chat.botPaused, + contact: c + ? { + name: displayName, + phone: c.phone, + avatarUrl: c.avatarUrl, + scoreReputacao: c.scoreReputacao, + flagRestricao: c.flagRestricao, + } + : isGroup + ? { name: displayName, phone: null, avatarUrl: null, scoreReputacao: 0, flagRestricao: false } + : null, + lastMessage, + }); + } + catch (err) { + logger_1.logger.error({ err, chatId }, '[MessageHandler] Falha ao emitir chat:upsert'); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // DOWNLOAD DE MÍDIA + // ═══════════════════════════════════════════════════════════════════════════ + /** + * Baixa o conteúdo de mídia de uma mensagem e salva no disco. + * + * Executado via setImmediate() para não bloquear o event loop. + * Após salvar, atualiza o registro da mensagem com mediaPath/mediaUrl + * e emite `message:media_ready` para o frontend renderizar o preview. + * + * Organização: /media/{instanceId}/{messageId}.{ext} + */ + async downloadAndPersistMedia(msg, savedMsgId, chatId, contentType) { + try { + const buffer = await (0, baileys_1.downloadMediaMessage)(msg, 'buffer', {}, { logger: logger_1.logger, reuploadRequest: this.sock.updateMediaMessage }); + if (!buffer) + return; + // Para documentos: preserva nome e extensão originais do arquivo + const docMsg = msg.message?.documentMessage; + const originalFileName = docMsg?.fileName?.trim() || null; + const originalMimetype = docMsg?.mimetype || this.mimetypeForType(contentType); + let ext; + if (contentType === 'documentMessage' && originalFileName) { + ext = path_1.default.extname(originalFileName).replace('.', '') || 'bin'; + } + else { + ext = this.extensionForType(contentType); + } + const fileName = `${savedMsgId}.${ext}`; + const filePath = path_1.default.join(MEDIA_DIR, this.instanceId, fileName); + await promises_1.default.mkdir(path_1.default.dirname(filePath), { recursive: true }); + await promises_1.default.writeFile(filePath, buffer); + // Tenta upload para Wasabi; se falhar usa path local como fallback + let mediaUrl = `/media/${this.instanceId}/${fileName}`; + if (StorageProvider_1.storageProvider.isRegistered()) { + try { + const result = await StorageProvider_1.storageProvider.uploadFile({ + category: 'whatsapp', + usuarioSis: this.tenantId, + sessionJID: this.instanceId, + file: { + originalname: originalFileName ?? fileName, + buffer: buffer, + mimetype: originalMimetype, + }, + }); + mediaUrl = result.path; + logger_1.logger.debug({ savedMsgId, path: result.path, provider: result.provider }, 'Mídia enviada ao storage'); + } + catch (uploadErr) { + logger_1.logger.warn({ uploadErr, savedMsgId }, 'Upload Wasabi falhou, usando path local'); + } + } + await prisma_1.prisma.message.update({ + where: { id: savedMsgId }, + data: { + mediaPath: filePath, + mediaUrl, + // Preserva o nome original do documento para exibição no frontend + ...(contentType === 'documentMessage' && originalFileName + ? { fileName: originalFileName } + : {}), + }, + }); + // Notifica frontend que a mídia está pronta para exibição + this.io.to(`chat:${chatId}`).emit('message:media_ready', { + messageId: savedMsgId, + mediaUrl, + }); + logger_1.logger.debug({ savedMsgId, filePath }, 'Mídia salva'); + } + catch (err) { + logger_1.logger.error({ err, savedMsgId }, 'Falha ao baixar mídia'); + } + } + // ═══════════════════════════════════════════════════════════════════════════ + // UTILITÁRIOS + // ═══════════════════════════════════════════════════════════════════════════ + /** Verifica se o contentType é de mídia (precisa de download) */ + isMediaMessage(contentType) { + return ['imageMessage', 'videoMessage', 'audioMessage', 'documentMessage', 'stickerMessage'].includes(contentType); + } + /** + * Mapeia o contentType do Baileys para o enum MessageType do Prisma. + * Tipos não reconhecidos são mapeados para UNSUPPORTED. + */ + mapContentType(contentType) { + const map = { + conversation: client_1.MessageType.TEXT, + extendedTextMessage: client_1.MessageType.TEXT, + imageMessage: client_1.MessageType.IMAGE, + videoMessage: client_1.MessageType.VIDEO, + audioMessage: client_1.MessageType.AUDIO, + documentMessage: client_1.MessageType.DOCUMENT, + stickerMessage: client_1.MessageType.STICKER, + locationMessage: client_1.MessageType.LOCATION, + contactMessage: client_1.MessageType.CONTACT, + reactionMessage: client_1.MessageType.REACTION, + pollCreationMessage: client_1.MessageType.POLL, + pollCreationMessageV2: client_1.MessageType.POLL, // poll multi-select grupos + pollCreationMessageV3: client_1.MessageType.POLL, // poll single-select + // Resposta ao clique de botão / seleção de item de lista + interactiveResponseMessage: client_1.MessageType.INTERACTIVE, + // Mensagem interativa recebida de outro remetente (botões/lista/carrossel) + interactiveMessage: client_1.MessageType.BUTTONS, + }; + return map[contentType] ?? client_1.MessageType.UNSUPPORTED; + } + /** Retorna a extensão de arquivo adequada para o tipo de mídia */ + extensionForType(contentType) { + const map = { + imageMessage: 'jpg', + videoMessage: 'mp4', + audioMessage: 'ogg', + documentMessage: 'bin', + stickerMessage: 'webp', + }; + return map[contentType] ?? 'bin'; + } + /** Retorna o mimetype adequado para o tipo de mídia */ + mimetypeForType(contentType) { + const map = { + imageMessage: 'image/jpeg', + videoMessage: 'video/mp4', + audioMessage: 'audio/ogg', + documentMessage: 'application/octet-stream', + stickerMessage: 'image/webp', + }; + return map[contentType] ?? 'application/octet-stream'; + } +} +exports.MessageHandler = MessageHandler; diff --git a/plugins/ext-api/backend/backend/src/shared/utils/whatsapp.js b/plugins/ext-api/backend/backend/src/shared/utils/whatsapp.js new file mode 100644 index 0000000..130a2b5 --- /dev/null +++ b/plugins/ext-api/backend/backend/src/shared/utils/whatsapp.js @@ -0,0 +1,68 @@ +"use strict"; +/** + * Utilitários para manipulação de JIDs (Jabber IDs) do WhatsApp. + * + * O WhatsApp usa JIDs como identificadores únicos para contatos, grupos e canais: + * - Contatos individuais: "5511999999999@s.whatsapp.net" + * - Grupos: "120363012345678@g.us" + * - Canais/newsletters: "120363012345678@newsletter" + * - LID (Linked ID): "123456789012345@lid" (identificador interno moderno) + * - Broadcast: "status@broadcast" + * + * JIDs de dispositivo incluem sufixo ":N" (ex: "5511999999999:1@s.whatsapp.net") + * que identifica o dispositivo específico (multi-device). Para armazenamento + * e comparação, normalizamos removendo esse sufixo. + * + * @see MessageHandler — usa normalizeJid para persistir mensagens + * @see ContactHandler — usa para vincular contatos a chats + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.normalizeJid = normalizeJid; +exports.extractPhone = extractPhone; +/** + * Normaliza um JID do WhatsApp removendo sufixos de dispositivo (:1, :2, etc). + * + * Exemplos: + * "556199999999:1@s.whatsapp.net" → "556199999999@s.whatsapp.net" + * "556199999999@s.whatsapp.net" → "556199999999@s.whatsapp.net" (sem mudança) + * "120363012345@g.us" → "120363012345@g.us" (grupos não têm sufixo) + * null / undefined → "" (string vazia como fallback seguro) + * + * IMPORTANTE: Não confundir com resolução LID. Este método apenas remove + * o sufixo de dispositivo — NÃO converte @lid para @s.whatsapp.net. + * Para resolução LID, veja MessageHandler.resolveLidToPhoneJid(). + * + * @param jid - JID bruto do Baileys (pode conter sufixo de dispositivo) + * @returns JID normalizado sem sufixo de dispositivo + */ +function normalizeJid(jid) { + if (!jid) + return ''; + const [userPart, domain] = jid.split('@'); + if (!domain) + return jid; // Não é um JID válido (sem @), retorna como está + // Remove sufixo de dispositivo: "556199999999:1" → "556199999999" + const [cleanUser] = userPart.split(':'); + return `${cleanUser}@${domain}`; +} +/** + * Extrai o número de telefone puro de um JID. + * + * Exemplos: + * "5511999999999@s.whatsapp.net" → "5511999999999" + * "5511999999999:1@s.whatsapp.net" → "5511999999999" + * "120363012345@g.us" → null (grupos não têm telefone único) + * null / undefined → null + * + * @param jid - JID do WhatsApp + * @returns Número de telefone (apenas dígitos + código do país) ou null + */ +function extractPhone(jid) { + if (!jid) + return null; + if (jid.endsWith('@g.us')) + return null; // Grupos não têm "telefone" único no JID + const [userPart] = jid.split('@'); + const [cleanUser] = userPart.split(':'); + return cleanUser; +} diff --git a/plugins/ext-api/backend/plugins/ext-api/backend/routes.js b/plugins/ext-api/backend/plugins/ext-api/backend/routes.js new file mode 100644 index 0000000..a13c9a7 --- /dev/null +++ b/plugins/ext-api/backend/plugins/ext-api/backend/routes.js @@ -0,0 +1,1144 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.buildExtRoutes = buildExtRoutes; +/** + * External REST API — /api/ext/v1/ + * + * Endpoints implementados nesta fase: + * + * GET /api/ext/v1/sessions — lista instâncias do tenant + * POST /api/ext/v1/sessions — cria nova instância + * DELETE /api/ext/v1/sessions/:id — remove instância + * GET /api/ext/v1/sessions/:id/qr — QR base64 da instância + * GET /api/ext/v1/inbox?session=&search=&limit= — lista chats + * GET /api/ext/v1/inbox/:chatId/messages — mensagens paginadas + * POST /api/ext/v1/inbox/:chatId/send — envia mensagem de texto + * + * Envelope de resposta de erro: { error: string } + * Envelope de resposta de sucesso: dados diretos (sem wrapper extra) + */ +const fs_1 = require("fs"); +const promises_1 = require("fs/promises"); +const express_1 = require("express"); +const dragonfly_1 = require("../../../backend/src/infra/cache/dragonfly"); +const brain_1 = require("../../secretaria/brain"); +const rich_message_1 = require("../../../backend/dist/modules/whatsapp/rich-message"); +function uuid() { + try { + return crypto.randomUUID(); + } + catch { + return `${Date.now()}-${Math.random().toString(36).slice(2)}`; + } +} +// ── Handoff timers (in-memory, por convId) ──────────────────────────────────── +const HANDOFF_TIMEOUT_MS = 15 * 60 * 1000; +const handoffTimers = new Map(); +function scheduleHandoffTimeout(convId, extChatId, tenantId, db, hooks) { + const existing = handoffTimers.get(convId); + if (existing) + clearTimeout(existing); + const timer = setTimeout(async () => { + handoffTimers.delete(convId); + try { + await db('sec_conversations').where({ id: convId }) + .update({ handoff_mode: 'ia', handoff_human_at: null }); + // chatId = parte após o primeiro ":" no extChatId ("tenantId:chatId") + const chatId = extChatId.includes(':') ? extChatId.split(':').slice(1).join(':') : extChatId; + hooks.emit('ext:handoff', { tenantId, conversationId: convId, chatId, mode: 'ia', reason: 'timeout' }); + } + catch { } + }, HANDOFF_TIMEOUT_MS); + handoffTimers.set(convId, timer); +} +function cancelHandoffTimeout(convId) { + const existing = handoffTimers.get(convId); + if (existing) { + clearTimeout(existing); + handoffTimers.delete(convId); + } +} +function buildExtRoutes(prisma, manager, db, config, hooks) { + const router = (0, express_1.Router)(); + // ── Escalation notification listener ────────────────────────────────────── + // When escalar_humano tool fires, send a WA message to the configured admin phone. + hooks.register('ext:escalated', async (data) => { + try { + const rawPhone = await config.get('admin_notify_phone'); + const adminPhone = typeof rawPhone === 'string' ? rawPhone.trim() : ''; + if (!adminPhone) + return; + // Normalise to JID + const digits = adminPhone.replace(/\D/g, ''); + const normalized = digits.startsWith('55') ? digits : '55' + digits; + const jid = `${normalized}@s.whatsapp.net`; + // Find first connected instance for this tenant + const instance = await prisma.instance.findFirst({ + where: { tenantId: data.tenantId }, + select: { id: true }, + }); + if (!instance) + return; + const sock = manager.getSocket(instance.id); + if (!sock) + return; + const proto = data.protocolNumber ? `#${data.protocolNumber}` : ''; + const motivo = data.motivo ? ` — ${data.motivo}` : ''; + const msg = `⚠️ *Escalação* ${proto}${motivo}\nUm cliente precisa de atendimento humano. Abra o painel para assumir.`; + await sock.sendMessage(jid, { text: msg }); + } + catch { /* notificação é best-effort */ } + }); + // ── GET /sessions ────────────────────────────────────────────────────────── + // Lista todas as instâncias do tenant com status e telefone. + router.get('/sessions', async (req, res) => { + const tenantId = req.extTenantId; + try { + const instances = await prisma.instance.findMany({ + where: { tenantId }, + orderBy: { createdAt: 'asc' }, + select: { + id: true, + name: true, + phone: true, + status: true, + avatar: true, + createdAt: true, + }, + }); + res.json(instances); + } + catch (err) { + res.status(500).json({ error: err.message ?? 'Erro interno' }); + } + }); + // ── POST /sessions ──────────────────────────────────────────────────────── + // Cria uma nova instância e dispara a conexão (QR chega via WS). + router.post('/sessions', async (req, res) => { + const tenantId = req.extTenantId; + const { name } = req.body; + if (!name?.trim()) { + res.status(400).json({ error: 'name é obrigatório' }); + return; + } + try { + const instance = await prisma.instance.create({ + data: { tenantId, name: name.trim() }, + select: { id: true, name: true, phone: true, status: true, avatar: true, createdAt: true }, + }); + manager.connect(instance.id, tenantId).catch((err) => { + console.error('[ext-api] Falha ao conectar instância após criação:', err); + }); + res.status(201).json(instance); + } + catch (err) { + res.status(500).json({ error: err.message ?? 'Erro interno' }); + } + }); + // ── DELETE /sessions/:id ────────────────────────────────────────────────── + // Desconecta e remove a instância do tenant. + router.delete('/sessions/:id', async (req, res) => { + const tenantId = req.extTenantId; + const instanceId = req.params['id']; + try { + const instance = await prisma.instance.findFirst({ where: { id: instanceId, tenantId } }); + if (!instance) { + res.status(404).json({ error: 'Instância não encontrada' }); + return; + } + await manager.disconnect(instanceId).catch(() => { }); + await prisma.instance.delete({ where: { id: instanceId } }); + res.json({ message: 'Instância removida' }); + } + catch (err) { + res.status(500).json({ error: err.message ?? 'Erro interno' }); + } + }); + // ── GET /sessions/:id/qr ────────────────────────────────────────────────── + // Retorna o QR code atual em base64 (poll antes de receber via WS). + router.get('/sessions/:id/qr', async (req, res) => { + const tenantId = req.extTenantId; + const instanceId = req.params['id']; + try { + const instance = await prisma.instance.findFirst({ + where: { id: instanceId, tenantId }, + select: { id: true }, + }); + if (!instance) { + res.status(404).json({ error: 'Instância não encontrada' }); + return; + } + const qrBase64 = await dragonfly_1.dragonfly.get(`instance:${instanceId}:qr`); + if (!qrBase64) { + res.status(404).json({ error: 'QR não disponível — inicie a conexão primeiro' }); + return; + } + res.json({ instanceId, qrBase64 }); + } + catch (err) { + res.status(500).json({ error: err.message ?? 'Erro interno' }); + } + }); + // ── GET /media/:messageId ───────────────────────────────────────────────── + // Serve o arquivo de mídia de uma mensagem. + // Se mediaUrl for URL externa (Wasabi etc.) redireciona 302. + // Se for arquivo local, faz stream com suporte a Range (áudio/vídeo). + router.get('/media/:messageId', async (req, res) => { + const tenantId = req.extTenantId; + const messageId = req.params['messageId']; + try { + const msg = await prisma.message.findFirst({ + where: { id: messageId, tenantId }, + select: { mediaUrl: true, mediaPath: true, mimeType: true, fileName: true }, + }); + if (!msg || (!msg.mediaPath && !msg.mediaUrl)) { + res.status(404).json({ error: 'Mídia não encontrada' }); + return; + } + // URL externa (Wasabi / CDN) → redirect + if (msg.mediaUrl && msg.mediaUrl.startsWith('http')) { + res.redirect(302, msg.mediaUrl); + return; + } + const filePath = msg.mediaPath; + if (!filePath) { + res.status(404).json({ error: 'Arquivo não disponível' }); + return; + } + const info = await (0, promises_1.stat)(filePath).catch(() => null); + if (!info) { + res.status(404).json({ error: 'Arquivo não encontrado no disco' }); + return; + } + const mime = msg.mimeType || 'application/octet-stream'; + const size = info.size; + // Content-Disposition: inline para imagens/áudio/vídeo, attachment para docs + const isInline = mime.startsWith('image/') || mime.startsWith('audio/') || mime.startsWith('video/'); + const disposition = isInline + ? 'inline' + : `attachment; filename="${msg.fileName ?? 'arquivo'}"`; + res.setHeader('Content-Disposition', disposition); + res.setHeader('Content-Type', mime); + res.setHeader('Accept-Ranges', 'bytes'); + res.setHeader('Cache-Control', 'private, max-age=3600'); + // Suporte a Range (essencial para