From 4ce4204b6ce8db066d499c942108356b841055a8 Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:24:08 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .dockerignore | 41 + .env.example | 338 + .env.minimal | 46 + .gitattributes | 3 + .github/ISSUE_TEMPLATE/bug_report.yml | 88 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 41 + .github/dependabot.yml | 61 + .github/pull_request_template.md | 19 + .github/workflows/ci.yml | 240 + .github/workflows/java-sdk-release.yml | 59 + .github/workflows/release.yml | 350 + .github/workflows/sdk-ci.yml | 75 + .github/workflows/split-php-sdk.yml | 73 + .gitignore | 80 + .nvmrc | 1 + .prettierrc | 11 + CHANGELOG.md | 2336 +++ CODE_OF_CONDUCT.md | 36 + CONTRIBUTING.md | 89 + Dockerfile | 145 + LICENSE | 21 + README.md | 413 + README.wehub.md | 7 + SECURITY.md | 50 + dashboard/.gitignore | 24 + dashboard/.npmrc | 1 + dashboard/README.md | 99 + dashboard/eslint.config.js | 29 + dashboard/index.html | 13 + dashboard/package-lock.json | 3476 ++++ dashboard/package.json | 45 + dashboard/public/favicon.svg | 278 + dashboard/public/openwa_logo.webp | Bin 0 -> 76680 bytes dashboard/public/vite.svg | 1 + dashboard/scripts/check-i18n-parity.mjs | 125 + dashboard/src/App.css | 173 + dashboard/src/App.tsx | 137 + dashboard/src/assets/icons/folder.svg | 6 + dashboard/src/assets/icons/postgresql.svg | 16 + dashboard/src/assets/icons/s3.svg | 20 + dashboard/src/assets/icons/sqlite.svg | 2 + dashboard/src/assets/react.svg | 1 + dashboard/src/components/CustomSelect.css | 128 + dashboard/src/components/CustomSelect.tsx | 169 + dashboard/src/components/DashboardCharts.css | 103 + dashboard/src/components/DashboardCharts.tsx | 194 + dashboard/src/components/ErrorBoundary.tsx | 63 + dashboard/src/components/FilterBuilder.css | 289 + dashboard/src/components/FilterBuilder.tsx | 292 + dashboard/src/components/GithubIcon.tsx | 23 + dashboard/src/components/GlobalSearch.css | 125 + dashboard/src/components/GlobalSearch.tsx | 101 + dashboard/src/components/Layout.css | 626 + dashboard/src/components/Layout.tsx | 344 + dashboard/src/components/PageHeader.css | 87 + dashboard/src/components/PageHeader.tsx | 38 + dashboard/src/components/PluginInstances.css | 116 + dashboard/src/components/PluginInstances.tsx | 363 + dashboard/src/components/Toast.css | 111 + dashboard/src/components/Toast.tsx | 178 + .../src/components/chats/MediaLightbox.tsx | 52 + .../src/components/chats/MessageBody.tsx | 48 + dashboard/src/hooks/queries.ts | 347 + dashboard/src/hooks/useChatMessages.ts | 72 + .../src/hooks/useChatScrollPosition.test.ts | 72 + dashboard/src/hooks/useChatScrollPosition.ts | 129 + dashboard/src/hooks/useDocumentTitle.ts | 16 + dashboard/src/hooks/useRole.tsx | 41 + dashboard/src/hooks/useTheme.ts | 83 + dashboard/src/hooks/useWebSocket.ts | 242 + dashboard/src/i18n/index.ts | 93 + dashboard/src/i18n/locales/ar.json | 854 + dashboard/src/i18n/locales/en.json | 850 + dashboard/src/i18n/locales/es.json | 850 + dashboard/src/i18n/locales/fr.json | 850 + dashboard/src/i18n/locales/he.json | 851 + dashboard/src/i18n/locales/it.json | 850 + dashboard/src/i18n/locales/ko.json | 850 + dashboard/src/i18n/locales/pt-BR.json | 850 + dashboard/src/i18n/locales/te.json | 850 + dashboard/src/i18n/locales/zh-CN.json | 850 + dashboard/src/i18n/locales/zh-HK.json | 850 + dashboard/src/index.css | 454 + dashboard/src/main.tsx | 11 + dashboard/src/pages/ApiKeys.css | 451 + dashboard/src/pages/ApiKeys.tsx | 409 + dashboard/src/pages/Chats.css | 1057 + dashboard/src/pages/Chats.tsx | 1238 ++ dashboard/src/pages/Dashboard.css | 348 + dashboard/src/pages/Dashboard.tsx | 178 + dashboard/src/pages/Infrastructure.css | 702 + dashboard/src/pages/Infrastructure.tsx | 1194 ++ dashboard/src/pages/Login.css | 262 + dashboard/src/pages/Login.tsx | 135 + dashboard/src/pages/Logs.css | 380 + dashboard/src/pages/Logs.tsx | 225 + dashboard/src/pages/MessageTester.css | 255 + dashboard/src/pages/MessageTester.tsx | 307 + dashboard/src/pages/Plugins.css | 1138 ++ dashboard/src/pages/Plugins.tsx | 1178 ++ dashboard/src/pages/Sessions.css | 822 + dashboard/src/pages/Sessions.tsx | 807 + dashboard/src/pages/Templates.css | 497 + dashboard/src/pages/Templates.tsx | 435 + dashboard/src/pages/Webhooks.css | 718 + dashboard/src/pages/Webhooks.tsx | 569 + dashboard/src/services/api.ts | 951 + dashboard/src/styles.scope.test.ts | 63 + dashboard/src/types/role.ts | 11 + dashboard/src/utils/chatMessages.test.ts | 199 + dashboard/src/utils/chatMessages.ts | 142 + dashboard/src/utils/chunkReload.test.ts | 52 + dashboard/src/utils/chunkReload.ts | 29 + dashboard/src/utils/clipboard.ts | 31 + dashboard/src/utils/instanceForm.test.ts | 18 + dashboard/src/utils/instanceForm.ts | 19 + dashboard/src/utils/lazyWithRetry.ts | 17 + dashboard/src/utils/localizePlugin.test.ts | 57 + dashboard/src/utils/localizePlugin.ts | 30 + dashboard/src/utils/messageFormatter.test.ts | 94 + dashboard/src/utils/messageFormatter.ts | 168 + dashboard/src/utils/pageWindow.test.ts | 35 + dashboard/src/utils/pageWindow.ts | 18 + dashboard/src/utils/pluginConfigForm.test.ts | 28 + dashboard/src/utils/pluginConfigForm.ts | 33 + dashboard/src/utils/reconnectState.test.ts | 38 + dashboard/src/utils/reconnectState.ts | 41 + dashboard/src/utils/scrollDecision.test.ts | 37 + dashboard/src/utils/scrollDecision.ts | 32 + dashboard/src/utils/search-highlight.test.ts | 26 + dashboard/src/utils/search-highlight.ts | 37 + dashboard/src/utils/urlSecurity.test.ts | 58 + dashboard/src/utils/urlSecurity.ts | 27 + dashboard/src/vite-env.d.ts | 4 + dashboard/tsconfig.app.json | 29 + dashboard/tsconfig.json | 7 + dashboard/tsconfig.node.json | 26 + dashboard/vite.config.ts | 39 + docker-compose.dev.yml | 95 + docker-compose.yml | 319 + docker-entrypoint.sh | 30 + docs/01-project-overview.md | 298 + docs/02-requirements-specification.md | 492 + docs/03-system-architecture.md | 1578 ++ docs/04-security-design.md | 916 + docs/05-database-design.md | 787 + docs/06-api-specification.md | 4705 +++++ docs/07-api-collection.md | 1424 ++ docs/08-development-guidelines.md | 1179 ++ docs/09-testing-strategy.md | 266 + docs/10-devops-infrastructure.md | 1184 ++ docs/11-operational-runbooks.md | 722 + docs/12-troubleshooting-faq.md | 1074 + docs/13-horizontal-scaling.md | 533 + docs/14-migration-guide.md | 1301 ++ docs/15-project-roadmap.md | 712 + docs/16-risk-management.md | 742 + docs/17-dashboard-design.md | 701 + docs/18-sdk-design.md | 924 + docs/19-plugin-architecture.md | 609 + docs/20-community-guidelines.md | 465 + docs/21-glossary.md | 304 + docs/22-n8n-integration.md | 319 + docs/23-community-integrations.md | 15 + docs/23-plugin-sandboxing.md | 83 + docs/24-mcp-integration.md | 243 + docs/25-integration-fabric.md | 224 + docs/26-global-search.md | 199 + docs/27-plugin-search-providers.md | 208 + docs/DOCKER_ID.md | 120 + docs/README.md | 243 + docs/engine-capability-matrix.md | 152 + docs/examples/chat-history-limits.md | 70 + docs/examples/n8n-appointment-booking.md | 145 + docs/examples/session-phone-number-pairing.md | 96 + .../webhook-signature-verification.md | 111 + docs/logo/openwa.svg | 278 + docs/logo/openwa_logo.png | Bin 0 -> 155741 bytes docs/logo/openwa_logo.psd | Bin 0 -> 2263551 bytes docs/logo/openwa_logo.webp | Bin 0 -> 76680 bytes eslint.config.mjs | 67 + nest-cli.json | 8 + openapi.json | 7209 +++++++ package-lock.json | 16635 ++++++++++++++++ package.json | 215 + scripts/backup.sh | 97 + scripts/check-doc-versions.mjs | 48 + scripts/export-openapi.ts | 69 + scripts/openwa.sh | 205 + scripts/pg-uuid-smoke.js | 74 + scripts/postgres-init-schema.sh | 36 + scripts/restore.sh | 73 + scripts/smoke-test-docker-proxy.sh | 53 + scripts/smoke-test-non-root.sh | 48 + sdk/README.md | 193 + sdk/java/.gitignore | 2 + sdk/java/README.md | 130 + sdk/java/pom.xml | 120 + .../com/rmyndharis/openwa/ClientConfig.java | 126 + .../com/rmyndharis/openwa/OpenWAClient.java | 129 + .../openwa/errors/OpenWAApiError.java | 113 + .../openwa/errors/OpenWAAuthError.java | 8 + .../openwa/errors/OpenWAConflictError.java | 8 + .../rmyndharis/openwa/errors/OpenWAError.java | 8 + .../openwa/errors/OpenWAForbiddenError.java | 8 + .../openwa/errors/OpenWANotFoundError.java | 8 + .../errors/OpenWANotImplementedError.java | 8 + .../openwa/errors/OpenWARateLimitError.java | 8 + .../openwa/errors/OpenWATimeoutError.java | 8 + .../openwa/http/DefaultHttpTransport.java | 33 + .../java/com/rmyndharis/openwa/http/Http.java | 60 + .../rmyndharis/openwa/http/HttpMethod.java | 10 + .../openwa/http/HttpRequestData.java | 7 + .../openwa/http/HttpResponseData.java | 7 + .../rmyndharis/openwa/http/HttpTransport.java | 13 + .../openwa/model/AddLabelRequest.java | 22 + .../openwa/model/AuthValidateResponse.java | 4 + .../openwa/model/BatchMessageResult.java | 12 + .../openwa/model/BatchProgress.java | 4 + .../openwa/model/BatchStatusResponse.java | 16 + .../openwa/model/BulkMessageContent.java | 58 + .../openwa/model/BulkMessageItem.java | 46 + .../openwa/model/BulkMessageResponse.java | 9 + .../openwa/model/BulkMessageType.java | 17 + .../rmyndharis/openwa/model/CatalogInfo.java | 9 + .../openwa/model/CatalogProduct.java | 14 + .../openwa/model/CatalogProductsQuery.java | 29 + .../openwa/model/ChannelMessageQuery.java | 22 + .../openwa/model/ChannelRecord.java | 10 + .../openwa/model/ChatHistoryMessage.java | 37 + .../rmyndharis/openwa/model/ChatState.java | 16 + .../rmyndharis/openwa/model/ChatSummary.java | 16 + .../openwa/model/CheckNumberResponse.java | 4 + .../openwa/model/ContactPhoneResponse.java | 4 + .../openwa/model/ContactRecord.java | 10 + .../openwa/model/CreateGroupRequest.java | 31 + .../openwa/model/CreateSessionRequest.java | 43 + .../openwa/model/CreateTemplateRequest.java | 41 + .../openwa/model/CreateWebhookRequest.java | 65 + .../openwa/model/DeleteChatRequest.java | 22 + .../openwa/model/DeleteMessageRequest.java | 34 + .../openwa/model/DeliveryStatus.java | 17 + .../openwa/model/ForwardMessageRequest.java | 33 + .../openwa/model/GroupDescriptionRequest.java | 22 + .../rmyndharis/openwa/model/GroupInfo.java | 16 + .../openwa/model/GroupParticipant.java | 9 + .../openwa/model/GroupSubjectRequest.java | 22 + .../rmyndharis/openwa/model/GroupSummary.java | 10 + .../openwa/model/HealthReadyDetails.java | 4 + .../openwa/model/HealthReadyResponse.java | 4 + .../openwa/model/HealthResponse.java | 4 + .../openwa/model/InviteCodeResponse.java | 4 + .../rmyndharis/openwa/model/LabelRecord.java | 4 + .../openwa/model/ListChatsQuery.java | 27 + .../openwa/model/ListContactsQuery.java | 27 + .../openwa/model/ListGroupsQuery.java | 29 + .../openwa/model/ListMessagesQuery.java | 39 + .../openwa/model/MarkChatRequest.java | 22 + .../openwa/model/MessageDirection.java | 11 + .../openwa/model/MessageHistoryQuery.java | 33 + .../openwa/model/MessageListResponse.java | 6 + .../openwa/model/MessageRecord.java | 24 + .../openwa/model/MessageResponse.java | 4 + .../openwa/model/PaginatedProducts.java | 10 + .../openwa/model/PairingCodeResponse.java | 7 + .../openwa/model/ParticipantsRequest.java | 24 + .../openwa/model/ProfilePictureResponse.java | 4 + .../openwa/model/QrCodeResponse.java | 7 + .../openwa/model/ReactMessageRequest.java | 34 + .../openwa/model/ReactionRecord.java | 6 + .../openwa/model/ReactionSender.java | 4 + .../openwa/model/ReplyMessageRequest.java | 33 + .../model/RequestPairingCodeRequest.java | 22 + .../rmyndharis/openwa/model/SearchHit.java | 22 + .../rmyndharis/openwa/model/SearchQuery.java | 93 + .../openwa/model/SearchResults.java | 10 + .../openwa/model/SendBulkRequest.java | 70 + .../openwa/model/SendCatalogRequest.java | 28 + .../openwa/model/SendChatStateRequest.java | 28 + .../openwa/model/SendContactRequest.java | 33 + .../openwa/model/SendImageStatusRequest.java | 37 + .../openwa/model/SendLocationRequest.java | 51 + .../openwa/model/SendMediaRequest.java | 70 + .../openwa/model/SendProductRequest.java | 34 + .../openwa/model/SendTemplateRequest.java | 44 + .../openwa/model/SendTextRequest.java | 28 + .../openwa/model/SendTextStatusRequest.java | 45 + .../openwa/model/SendVideoStatusRequest.java | 37 + .../openwa/model/SessionResponse.java | 15 + .../openwa/model/SessionStatsOverview.java | 16 + .../openwa/model/SessionStatus.java | 24 + .../openwa/model/StatusListResult.java | 6 + .../openwa/model/StatusMediaInput.java | 34 + .../rmyndharis/openwa/model/StatusRecord.java | 13 + .../rmyndharis/openwa/model/StatusResult.java | 12 + .../openwa/model/SubscribeChannelRequest.java | 22 + .../openwa/model/SuccessResult.java | 4 + .../openwa/model/TemplateRecord.java | 13 + .../openwa/model/UpdateTemplateRequest.java | 41 + .../openwa/model/UpdateWebhookRequest.java | 73 + .../rmyndharis/openwa/model/WebhookEvent.java | 40 + .../openwa/model/WebhookFilterCondition.java | 6 + .../openwa/model/WebhookFilters.java | 6 + .../openwa/model/WebhookResponse.java | 20 + .../openwa/model/WebhookTestResult.java | 4 + .../com/rmyndharis/openwa/package-info.java | 2 + .../openwa/resources/CatalogResource.java | 75 + .../openwa/resources/ChannelsResource.java | 71 + .../openwa/resources/ChatsResource.java | 63 + .../openwa/resources/ContactsResource.java | 92 + .../openwa/resources/GroupsResource.java | 142 + .../openwa/resources/HealthResource.java | 30 + .../openwa/resources/LabelsResource.java | 70 + .../openwa/resources/MessagesResource.java | 225 + .../openwa/resources/SearchResource.java | 35 + .../openwa/resources/SessionsResource.java | 72 + .../openwa/resources/StatusResource.java | 83 + .../openwa/resources/TemplatesResource.java | 68 + .../openwa/resources/WebhooksResource.java | 68 + .../com/rmyndharis/openwa/ClientTest.java | 58 + .../com/rmyndharis/openwa/ConfigTest.java | 59 + .../com/rmyndharis/openwa/ScaffoldTest.java | 13 + .../rmyndharis/openwa/errors/ErrorsTest.java | 72 + .../com/rmyndharis/openwa/http/HttpTest.java | 53 + .../openwa/resources/CatalogResourceTest.java | 73 + .../resources/ChannelsResourceTest.java | 68 + .../openwa/resources/ChatsResourceTest.java | 82 + .../resources/ContactsResourceTest.java | 80 + .../openwa/resources/GroupsResourceTest.java | 131 + .../openwa/resources/HealthResourceTest.java | 43 + .../openwa/resources/LabelsResourceTest.java | 59 + .../resources/MessagesResourceTest.java | 219 + .../openwa/resources/SearchResourceTest.java | 137 + .../resources/SessionsResourceTest.java | 55 + .../openwa/resources/StatusResourceTest.java | 102 + .../resources/TemplatesResourceTest.java | 67 + .../resources/WebhooksResourceTest.java | 81 + .../openwa/support/MockTransport.java | 30 + sdk/javascript/README.md | 51 + sdk/javascript/package-lock.json | 1440 ++ sdk/javascript/package.json | 53 + sdk/javascript/scripts/smoke.mjs | 17 + sdk/javascript/src/client.ts | 117 + sdk/javascript/src/errors.ts | 129 + sdk/javascript/src/http.ts | 153 + sdk/javascript/src/index.ts | 32 + sdk/javascript/src/resources/catalog.ts | 69 + sdk/javascript/src/resources/channels.ts | 62 + sdk/javascript/src/resources/chats.ts | 67 + sdk/javascript/src/resources/contacts.ts | 82 + sdk/javascript/src/resources/groups.ts | 123 + sdk/javascript/src/resources/health.ts | 29 + sdk/javascript/src/resources/labels.ts | 56 + sdk/javascript/src/resources/messages.ts | 186 + sdk/javascript/src/resources/search.ts | 26 + sdk/javascript/src/resources/sessions.ts | 78 + sdk/javascript/src/resources/status.ts | 72 + sdk/javascript/src/resources/templates.ts | 56 + sdk/javascript/src/resources/webhooks.ts | 64 + sdk/javascript/src/types.ts | 747 + sdk/javascript/test/client.test.ts | 188 + sdk/javascript/test/helpers.ts | 108 + .../test/labels-channels-catalog.test.ts | 138 + sdk/javascript/test/messages.test.ts | 132 + sdk/javascript/test/resources.test.ts | 195 + sdk/javascript/test/sessions.test.ts | 51 + sdk/javascript/tsconfig.cjs.json | 19 + sdk/javascript/tsconfig.esm.json | 21 + sdk/javascript/tsconfig.json | 20 + sdk/javascript/vitest.config.ts | 8 + sdk/php/.gitattributes | 8 + sdk/php/.gitignore | 4 + sdk/php/README.md | 80 + sdk/php/composer.json | 43 + sdk/php/composer.lock | 2493 +++ sdk/php/phpunit.xml | 23 + sdk/php/src/Client.php | 152 + sdk/php/src/Exceptions/OpenWAApiException.php | 64 + .../src/Exceptions/OpenWAAuthException.php | 10 + .../Exceptions/OpenWAConflictException.php | 10 + sdk/php/src/Exceptions/OpenWAException.php | 12 + .../Exceptions/OpenWAForbiddenException.php | 10 + .../Exceptions/OpenWANotFoundException.php | 10 + .../OpenWANotImplementedException.php | 10 + .../Exceptions/OpenWARateLimitException.php | 10 + .../src/Exceptions/OpenWATimeoutException.php | 22 + sdk/php/src/Http/HttpExecutor.php | 161 + sdk/php/src/Resources/CatalogResource.php | 73 + sdk/php/src/Resources/ChannelsResource.php | 62 + sdk/php/src/Resources/ChatsResource.php | 59 + sdk/php/src/Resources/ContactsResource.php | 67 + sdk/php/src/Resources/GroupsResource.php | 100 + sdk/php/src/Resources/HealthResource.php | 40 + sdk/php/src/Resources/LabelsResource.php | 58 + sdk/php/src/Resources/MessagesResource.php | 160 + sdk/php/src/Resources/SearchResource.php | 57 + sdk/php/src/Resources/SessionsResource.php | 87 + sdk/php/src/Resources/StatusResource.php | 70 + sdk/php/src/Resources/TemplatesResource.php | 63 + sdk/php/src/Resources/WebhooksResource.php | 63 + sdk/php/tests/ClientTest.php | 196 + sdk/php/tests/LabelsChannelsCatalogTest.php | 81 + sdk/php/tests/MessagesTest.php | 111 + sdk/php/tests/MockBackend.php | 128 + sdk/php/tests/ResourcesTest.php | 219 + sdk/php/tests/SearchTest.php | 118 + sdk/php/tests/TemplatesTest.php | 39 + sdk/python/.gitignore | 8 + sdk/python/README.md | 93 + sdk/python/openwa/__init__.py | 49 + sdk/python/openwa/_http.py | 118 + sdk/python/openwa/client.py | 179 + sdk/python/openwa/errors.py | 99 + sdk/python/openwa/py.typed | 0 sdk/python/openwa/resources/__init__.py | 37 + sdk/python/openwa/resources/catalog.py | 58 + sdk/python/openwa/resources/channels.py | 41 + sdk/python/openwa/resources/chats.py | 46 + sdk/python/openwa/resources/contacts.py | 53 + sdk/python/openwa/resources/groups.py | 87 + sdk/python/openwa/resources/health.py | 27 + sdk/python/openwa/resources/labels.py | 40 + sdk/python/openwa/resources/messages.py | 109 + sdk/python/openwa/resources/py.typed | 0 sdk/python/openwa/resources/search.py | 24 + sdk/python/openwa/resources/sessions.py | 56 + sdk/python/openwa/resources/status.py | 38 + sdk/python/openwa/resources/templates.py | 34 + sdk/python/openwa/resources/webhooks.py | 41 + sdk/python/openwa/types.py | 750 + sdk/python/pyproject.toml | 40 + sdk/python/tests/conftest.py | 81 + sdk/python/tests/test_sdk.py | 578 + src/app.module.ts | 292 + src/common/cache/cache.module.ts | 11 + src/common/cache/cache.service.spec.ts | 44 + src/common/cache/cache.service.ts | 258 + src/common/cache/index.ts | 2 + .../channel-media-not-supported.error.ts | 18 + src/common/errors/channel-not-found.error.ts | 16 + .../chat-labels-unsupported.error.spec.ts | 12 + .../errors/chat-labels-unsupported.error.ts | 15 + src/common/errors/engine-not-ready.error.ts | 17 + .../errors/engine-not-supported.error.spec.ts | 11 + .../errors/engine-not-supported.error.ts | 16 + .../errors/message-not-found.error.spec.ts | 20 + src/common/errors/message-not-found.error.ts | 17 + .../request-metrics.interceptor.spec.ts | 93 + .../request-metrics.interceptor.ts | 54 + src/common/media/load-remote-media.spec.ts | 57 + src/common/media/load-remote-media.ts | 63 + src/common/metrics/request-metrics.spec.ts | 54 + src/common/metrics/request-metrics.ts | 109 + .../metrics/webhook-delivery-metrics.spec.ts | 17 + .../metrics/webhook-delivery-metrics.ts | 20 + .../request-context.middleware.spec.ts | 45 + .../middleware/request-context.middleware.ts | 20 + .../bull-board-auth.middleware.spec.ts | 189 + .../security/bull-board-auth.middleware.ts | 86 + .../proxy-aware-throttler.guard.spec.ts | 49 + .../security/proxy-aware-throttler.guard.ts | 25 + src/common/security/ssrf-guard.spec.ts | 392 + src/common/security/ssrf-guard.ts | 429 + src/common/security/ssrf-undici-pin.spec.ts | 45 + src/common/services/logger.module.ts | 10 + src/common/services/logger.service.spec.ts | 174 + src/common/services/logger.service.ts | 223 + src/common/services/request-context.spec.ts | 37 + src/common/services/request-context.ts | 18 + src/common/services/shutdown.service.spec.ts | 112 + src/common/services/shutdown.service.ts | 83 + src/common/storage/storage.module.ts | 11 + src/common/storage/storage.service.spec.ts | 244 + src/common/storage/storage.service.ts | 492 + .../throttler/redis-throttler.storage.spec.ts | 47 + .../throttler/redis-throttler.storage.ts | 58 + .../transformers/date.transformer.spec.ts | 33 + src/common/transformers/date.transformer.ts | 25 + src/common/utils/column-types.spec.ts | 33 + src/common/utils/column-types.ts | 36 + src/common/utils/concurrency-limiter.spec.ts | 108 + src/common/utils/concurrency-limiter.ts | 49 + src/common/utils/db-errors.spec.ts | 35 + src/common/utils/db-errors.ts | 31 + src/common/utils/ip.spec.ts | 42 + src/common/utils/ip.ts | 97 + src/common/utils/paginate.spec.ts | 55 + src/common/utils/paginate.ts | 36 + src/common/utils/path-safety.spec.ts | 77 + src/common/utils/path-safety.ts | 44 + src/common/utils/secret-file.spec.ts | 55 + src/common/utils/secret-file.ts | 27 + src/common/utils/template-render.spec.ts | 56 + src/common/utils/template-render.ts | 40 + .../utils/unique-constraint.util.spec.ts | 32 + src/common/utils/unique-constraint.util.ts | 15 + src/config/bootstrap-security.spec.ts | 341 + src/config/bootstrap-security.ts | 195 + src/config/configuration.spec.ts | 138 + src/config/configuration.ts | 197 + src/config/env-precedence.spec.ts | 163 + src/config/env-precedence.ts | 62 + src/config/env.validation.spec.ts | 202 + src/config/env.validation.ts | 208 + src/config/feature-flags.spec.ts | 73 + src/config/feature-flags.ts | 52 + src/config/http-timeouts.spec.ts | 37 + src/config/http-timeouts.ts | 46 + src/config/load-env.spec.ts | 86 + src/config/load-env.ts | 93 + src/config/process-error-monitor.spec.ts | 50 + src/config/process-error-monitor.ts | 32 + src/config/swagger.config.spec.ts | 99 + src/config/swagger.config.ts | 79 + src/core/agent-tools/agent-tools.module.ts | 44 + .../session-scope-invariant.spec.ts | 41 + src/core/agent-tools/tool-descriptor.ts | 26 + src/core/agent-tools/tool-invoker.spec.ts | 188 + src/core/agent-tools/tool-invoker.ts | 75 + src/core/agent-tools/tool-registry.service.ts | 25 + src/core/agent-tools/tool-registry.spec.ts | 106 + src/core/agent-tools/tools/contact.tools.ts | 110 + src/core/agent-tools/tools/group.tools.ts | 111 + src/core/agent-tools/tools/message.tools.ts | 385 + src/core/agent-tools/tools/session.tools.ts | 98 + .../tools/tool-output-sanitisation.spec.ts | 264 + src/core/agent-tools/tools/webhook.tools.ts | 50 + src/core/hooks/hook-manager.service.spec.ts | 227 + src/core/hooks/hook-manager.service.ts | 222 + src/core/hooks/hook.interfaces.spec.ts | 8 + src/core/hooks/hook.interfaces.ts | 86 + src/core/hooks/hooks.module.ts | 9 + src/core/hooks/index.ts | 3 + .../plugins/conversation-send-facade.spec.ts | 150 + src/core/plugins/conversation-send-facade.ts | 71 + src/core/plugins/handover-gate.spec.ts | 21 + src/core/plugins/handover-gate.ts | 11 + src/core/plugins/index.ts | 4 + src/core/plugins/plugin-activation.spec.ts | 77 + src/core/plugins/plugin-activation.ts | 53 + src/core/plugins/plugin-capability.spec.ts | 284 + .../plugins/plugin-loader-activation.spec.ts | 140 + .../plugin-loader-sandbox-routing.spec.ts | 159 + .../plugins/plugin-loader.service.spec.ts | 791 + src/core/plugins/plugin-loader.service.ts | 1297 ++ .../plugins/plugin-manifest-ingress.spec.ts | 158 + src/core/plugins/plugin-net.spec.ts | 147 + src/core/plugins/plugin-net.ts | 160 + .../plugins/plugin-storage.service.spec.ts | 146 + src/core/plugins/plugin-storage.service.ts | 316 + src/core/plugins/plugin.interfaces.ts | 567 + src/core/plugins/plugins.module.ts | 17 + .../plugins/sandbox/capability-router.spec.ts | 125 + src/core/plugins/sandbox/capability-router.ts | 82 + .../plugins/sandbox/conversation-send.spec.ts | 12 + .../plugins/sandbox/dispatch-webhook.spec.ts | 130 + src/core/plugins/sandbox/handover-set.spec.ts | 8 + .../sandbox/plugin-worker-host.spec.ts | 575 + .../plugins/sandbox/plugin-worker-host.ts | 480 + .../sandbox/plugin-worker.integration.spec.ts | 260 + src/core/plugins/sandbox/protocol.ts | 119 + src/core/plugins/sandbox/worker-bootstrap.ts | 141 + .../plugins/sandbox/worker-capability.spec.ts | 64 + src/core/plugins/sandbox/worker-capability.ts | 112 + src/core/plugins/sandbox/worker-hooks.spec.ts | 63 + src/core/plugins/sandbox/worker-hooks.ts | 84 + .../sandbox/worker-search-registry.spec.ts | 68 + .../plugins/sandbox/worker-search-registry.ts | 48 + .../plugins/sandbox/worker-thread-channel.ts | 54 + src/core/plugins/sandbox/worker-webhooks.ts | 79 + .../search-provider-registration.spec.ts | 70 + .../search-provider-registration.util.ts | 38 + src/core/plugins/webhook-subscribe.spec.ts | 90 + src/core/plugins/webhook-subscribe.util.ts | 39 + src/database/add-integration-fabric.spec.ts | 42 + .../add-integration-uuid-defaults.spec.ts | 68 + .../add-uuid-defaults-migration.spec.ts | 106 + .../add-webhooks-sessionid-index.spec.ts | 50 + src/database/data-source-main.spec.ts | 55 + src/database/data-source-main.ts | 33 + src/database/data-source.spec.ts | 82 + src/database/data-source.ts | 87 + src/database/load-cli-env.spec.ts | 51 + src/database/load-cli-env.ts | 20 + .../1779900000000-CreateAuthAuditTables.ts | 70 + ...779900000000-CreateAuthAuditTables.spec.ts | 70 + src/database/migrations/.gitkeep | 0 .../1770108659848-AddMessageStatus.ts | 119 + ...0000000-NormalizeSynchronizeUuidColumns.ts | 202 + ...779235200000-AddUuidDefaultsForPostgres.ts | 82 + .../migrations/1779840000000-AddTemplates.ts | 39 + .../1779900100000-AddMessageSessionWaIndex.ts | 22 + .../1781000000000-AddBaileysStoredMessages.ts | 40 + .../1781100000000-AddTemplateNameUnique.ts | 48 + .../1781200000000-AddLidMappings.ts | 34 + ...1300000000-AddMessagesWaMessageIdUnique.ts | 45 + .../1781500000000-AddWebhookFilters.ts | 23 + ...000-DropRedundantMessagesSessionIdIndex.ts | 23 + ...781700000000-AddWebhookDeliveryFailures.ts | 38 + ...81800000000-ScopeBatchIdUniqueToSession.ts | 70 + .../1781900000000-AddIntegrationFabric.ts | 81 + .../1782000000000-AddMessageChatName.ts | 28 + .../1782100000000-WidenIngressDedupKey.ts | 28 + ...1782200000000-AddWebhooksSessionIdIndex.ts | 28 + ...782300000000-AddIntegrationUuidDefaults.ts | 67 + .../1782400000000-AddMessagesFts.ts | 79 + .../1770108659848-AddMessageStatus.spec.ts | 45 + ...00-NormalizeSynchronizeUuidColumns.spec.ts | 263 + .../1779840000000-AddTemplates.spec.ts | 56 + ...000000000-AddBaileysStoredMessages.spec.ts | 37 + ...781100000000-AddTemplateNameUnique.spec.ts | 96 + .../1781200000000-AddLidMappings.spec.ts | 51 + ...00000-AddMessagesWaMessageIdUnique.spec.ts | 85 + ...ropRedundantMessagesSessionIdIndex.spec.ts | 47 + ...0000000-AddWebhookDeliveryFailures.spec.ts | 47 + ...000000-ScopeBatchIdUniqueToSession.spec.ts | 67 + .../1782400000000-AddMessagesFts.pg.spec.ts | 126 + .../1782400000000-AddMessagesFts.spec.ts | 186 + .../adapters/baileys-group-mapper.spec.ts | 87 + src/engine/adapters/baileys-group-mapper.ts | 59 + .../adapters/baileys-message-mapper.spec.ts | 230 + src/engine/adapters/baileys-message-mapper.ts | 230 + .../baileys-message-store.service.spec.ts | 185 + .../adapters/baileys-message-store.service.ts | 124 + .../adapters/baileys-session-store.spec.ts | 350 + src/engine/adapters/baileys-session-store.ts | 346 + .../adapters/baileys-stored-message.entity.ts | 34 + src/engine/adapters/baileys.adapter.spec.ts | 2268 +++ src/engine/adapters/baileys.adapter.ts | 1665 ++ src/engine/adapters/inbound-media-cap.spec.ts | 208 + src/engine/adapters/inbound-media-cap.ts | 114 + src/engine/adapters/message-mapper.spec.ts | 215 + src/engine/adapters/message-mapper.ts | 165 + src/engine/adapters/vcard.spec.ts | 40 + src/engine/adapters/vcard.ts | 27 + .../adapters/whatsapp-web-js.adapter.spec.ts | 1943 ++ .../adapters/whatsapp-web-js.adapter.ts | 2013 ++ src/engine/engine-capability-matrix.ts | 196 + src/engine/engine-parity.spec.ts | 72 + src/engine/engine.factory.spec.ts | 202 + src/engine/engine.factory.ts | 250 + src/engine/engine.module.ts | 15 + .../identity/lid-mapping-store.service.ts | 89 + src/engine/identity/lid-mapping-store.spec.ts | 93 + src/engine/identity/lid-mapping.entity.ts | 31 + src/engine/identity/wa-id.spec.ts | 77 + src/engine/identity/wa-id.ts | 112 + .../interfaces/whatsapp-engine.interface.ts | 545 + src/engine/types/baileys.types.ts | 50 + src/engine/types/whatsapp-web-js.types.ts | 114 + src/engine/wa-web-version.spec.ts | 122 + src/engine/wa-web-version.ts | 150 + src/main.ts | 281 + src/modules/audit/audit-coverage.spec.ts | 66 + src/modules/audit/audit.controller.spec.ts | 17 + src/modules/audit/audit.controller.ts | 44 + src/modules/audit/audit.module.ts | 14 + src/modules/audit/audit.service.spec.ts | 101 + src/modules/audit/audit.service.ts | 185 + .../audit/entities/audit-log.entity.ts | 96 + .../audit/intentionally-unemitted-actions.ts | 35 + src/modules/auth/api-key-hash.spec.ts | 19 + src/modules/auth/api-key-hash.ts | 14 + .../auth/auth-validate.controller.spec.ts | 28 + src/modules/auth/auth-validate.controller.ts | 28 + src/modules/auth/auth.controller.spec.ts | 68 + src/modules/auth/auth.controller.ts | 180 + src/modules/auth/auth.module.ts | 28 + src/modules/auth/auth.service.spec.ts | 562 + src/modules/auth/auth.service.ts | 335 + .../auth/decorators/auth.decorators.ts | 38 + src/modules/auth/dto/api-key.dto.ts | 128 + src/modules/auth/dto/index.ts | 1 + .../auth/dto/is-ip-or-cidr.validator.spec.ts | 65 + .../auth/dto/is-ip-or-cidr.validator.ts | 29 + src/modules/auth/entities/api-key.entity.ts | 56 + src/modules/auth/guards/api-key.guard.spec.ts | 296 + src/modules/auth/guards/api-key.guard.ts | 113 + src/modules/auth/role-coverage.spec.ts | 18 + src/modules/catalog/catalog.controller.ts | 49 + src/modules/catalog/catalog.module.ts | 12 + src/modules/catalog/catalog.service.ts | 53 + src/modules/catalog/dto/send-product.dto.ts | 48 + .../channel/channel.controller.spec.ts | 28 + src/modules/channel/channel.controller.ts | 96 + src/modules/channel/channel.module.ts | 11 + src/modules/channel/channel.service.spec.ts | 26 + src/modules/channel/channel.service.ts | 44 + .../channel/dto/subscribe-channel.dto.ts | 10 + src/modules/contact/contact.controller.ts | 122 + src/modules/contact/contact.module.ts | 12 + src/modules/contact/contact.service.spec.ts | 54 + src/modules/contact/contact.service.ts | 61 + src/modules/docker/compose-network.spec.ts | 30 + src/modules/docker/docker.module.ts | 8 + src/modules/docker/docker.service.spec.ts | 114 + src/modules/docker/docker.service.ts | 594 + src/modules/docker/index.ts | 2 + src/modules/events/dto/ws-messages.dto.ts | 113 + src/modules/events/events.gateway.spec.ts | 390 + src/modules/events/events.gateway.ts | 435 + src/modules/events/events.module.ts | 11 + src/modules/group/dto/group.dto.spec.ts | 48 + src/modules/group/dto/group.dto.ts | 39 + src/modules/group/group.controller.ts | 178 + src/modules/group/group.module.ts | 12 + src/modules/group/group.service.spec.ts | 59 + src/modules/group/group.service.ts | 78 + src/modules/health/health.controller.spec.ts | 83 + src/modules/health/health.controller.ts | 106 + src/modules/health/health.module.ts | 7 + src/modules/infra/dto/import-storage.dto.ts | 14 + src/modules/infra/infra.controller.spec.ts | 1086 + src/modules/infra/infra.controller.ts | 1595 ++ src/modules/infra/infra.module.ts | 19 + .../chatwoot-message_created.json | 8 + .../conversation-mapping.service.spec.ts | 84 + .../conversation-mapping.service.ts | 109 + src/modules/integration/dto/instance.dto.ts | 137 + .../entities/conversation-mapping.entity.ts | 38 + .../entities/ingress-event.entity.ts | 36 + .../integration-delivery-failure.entity.ts | 41 + .../entities/plugin-instance.entity.ts | 40 + src/modules/integration/ingress-ack.spec.ts | 31 + src/modules/integration/ingress-ack.ts | 37 + .../ingress-enqueue.service.spec.ts | 124 + .../integration/ingress-enqueue.service.ts | 125 + .../integration/ingress-event.service.spec.ts | 47 + .../integration/ingress-event.service.ts | 31 + .../integration/ingress-golden.spec.ts | 169 + .../integration/ingress-preflight.spec.ts | 66 + src/modules/integration/ingress-preflight.ts | 34 + .../integration/ingress-signature.spec.ts | 295 + src/modules/integration/ingress-signature.ts | 131 + src/modules/integration/ingress-url.spec.ts | 24 + src/modules/integration/ingress-url.ts | 26 + .../integration/ingress.controller.spec.ts | 92 + src/modules/integration/ingress.controller.ts | 74 + .../integration/ingress.service.spec.ts | 439 + src/modules/integration/ingress.service.ts | 207 + .../instance-throttler.guard.spec.ts | 26 + .../integration/instance-throttler.guard.ts | 45 + .../integration-instance.controller.spec.ts | 186 + .../integration-instance.controller.ts | 188 + .../integration-retention.service.spec.ts | 207 + .../integration-retention.service.ts | 71 + .../integration/integration.constants.ts | 3 + src/modules/integration/integration.module.ts | 101 + src/modules/integration/ordering-lock.spec.ts | 92 + src/modules/integration/ordering-lock.ts | 32 + .../plugin-instance.service.spec.ts | 167 + .../integration/plugin-instance.service.ts | 132 + .../integration/redrive.controller.spec.ts | 13 + src/modules/integration/redrive.controller.ts | 27 + .../integration/redrive.service.spec.ts | 133 + src/modules/integration/redrive.service.ts | 54 + .../integration/scope-binding.service.spec.ts | 123 + .../integration/scope-binding.service.ts | 128 + src/modules/label/dto/add-label.dto.ts | 10 + src/modules/label/label.controller.ts | 104 + src/modules/label/label.module.ts | 11 + src/modules/label/label.service.spec.ts | 26 + src/modules/label/label.service.ts | 44 + src/modules/mcp/mcp-rate-limit.spec.ts | 148 + src/modules/mcp/mcp-rate-limit.ts | 66 + src/modules/mcp/mcp.module.ts | 59 + src/modules/mcp/mcp.server.spec.ts | 137 + src/modules/mcp/mcp.server.ts | 244 + src/modules/mcp/tool-result.ts | 61 + .../message/bulk-message.service.spec.ts | 436 + src/modules/message/bulk-message.service.ts | 519 + .../message/dto/bulk-message.dto.spec.ts | 45 + src/modules/message/dto/bulk-message.dto.ts | 160 + src/modules/message/dto/index.ts | 1 + .../message/dto/message-actions.dto.spec.ts | 95 + .../message/dto/message-actions.dto.ts | 168 + .../message/dto/send-message.dto.spec.ts | 62 + src/modules/message/dto/send-message.dto.ts | 125 + src/modules/message/dto/send-template.dto.ts | 40 + .../message/entities/message-batch.entity.ts | 91 + .../message/entities/message.entity.spec.ts | 29 + .../message/entities/message.entity.ts | 92 + src/modules/message/media-cap.util.spec.ts | 35 + src/modules/message/media-cap.util.ts | 23 + .../message/message-status.util.spec.ts | 53 + src/modules/message/message-status.util.ts | 61 + .../message-type-backfill.service.spec.ts | 32 + .../message/message-type-backfill.service.ts | 63 + src/modules/message/message.controller.ts | 447 + src/modules/message/message.module.ts | 18 + src/modules/message/message.service.spec.ts | 972 + src/modules/message/message.service.ts | 705 + src/modules/metrics/metrics.controller.ts | 29 + src/modules/metrics/metrics.module.ts | 18 + src/modules/metrics/metrics.service.spec.ts | 88 + src/modules/metrics/metrics.service.ts | 120 + src/modules/plugins/catalog.spec.ts | 49 + src/modules/plugins/catalog.ts | 65 + src/modules/plugins/dto/index.ts | 1 + src/modules/plugins/dto/plugin.dto.ts | 90 + src/modules/plugins/plugin-download.ts | 64 + src/modules/plugins/plugin-installer.spec.ts | 229 + src/modules/plugins/plugin-installer.ts | 161 + .../plugins/plugins.controller.spec.ts | 28 + src/modules/plugins/plugins.controller.ts | 174 + src/modules/plugins/plugins.module.ts | 10 + src/modules/plugins/plugins.service.spec.ts | 410 + src/modules/plugins/plugins.service.ts | 511 + src/modules/plugins/redact-config.spec.ts | 370 + src/modules/plugins/redact-config.ts | 198 + .../processors/ingress.processor.spec.ts | 114 + .../queue/processors/ingress.processor.ts | 93 + .../processors/webhook.processor.spec.ts | 181 + .../queue/processors/webhook.processor.ts | 173 + src/modules/queue/queue-names.ts | 6 + src/modules/queue/queue.module.ts | 76 + src/modules/queue/redis-connection.spec.ts | 71 + src/modules/queue/redis-connection.ts | 62 + .../search/dto/search-query.dto.spec.ts | 59 + src/modules/search/dto/search-query.dto.ts | 72 + .../providers/builtin-fts.provider.spec.ts | 137 + .../search/providers/builtin-fts.provider.ts | 256 + .../providers/plugin-search-provider.spec.ts | 129 + .../providers/plugin-search-provider.ts | 63 + .../search/providers/search-dual-db.spec.ts | 88 + .../search/search-provider.contract.spec.ts | 112 + .../search/search-provider.registry.spec.ts | 42 + .../search/search-provider.registry.ts | 38 + src/modules/search/search.constants.ts | 18 + src/modules/search/search.controller.spec.ts | 64 + src/modules/search/search.controller.ts | 41 + src/modules/search/search.module.spec.ts | 39 + src/modules/search/search.module.ts | 52 + src/modules/search/search.service.spec.ts | 85 + src/modules/search/search.service.ts | 31 + src/modules/search/search.types.spec.ts | 30 + src/modules/search/search.types.ts | 59 + .../session/dto/create-session.dto.spec.ts | 36 + src/modules/session/dto/create-session.dto.ts | 58 + src/modules/session/dto/delete-chat.dto.ts | 17 + src/modules/session/dto/index.ts | 6 + .../session/dto/mark-chat-read.dto.spec.ts | 23 + src/modules/session/dto/mark-chat-read.dto.ts | 18 + .../dto/request-pairing-code.dto.spec.ts | 19 + .../session/dto/request-pairing-code.dto.ts | 24 + .../session/dto/send-chat-state.dto.ts | 24 + .../session/dto/session-response.dto.ts | 71 + .../session/entities/session.entity.ts | 65 + src/modules/session/reconnect-config.spec.ts | 53 + src/modules/session/session.controller.ts | 320 + src/modules/session/session.module.ts | 17 + src/modules/session/session.service.spec.ts | 2682 +++ src/modules/session/session.service.ts | 1685 ++ .../settings/settings.controller.spec.ts | 64 + src/modules/settings/settings.controller.ts | 92 + src/modules/settings/settings.module.ts | 7 + src/modules/stats/dto/stats-query.dto.ts | 7 + src/modules/stats/stats.controller.spec.ts | 25 + src/modules/stats/stats.controller.ts | 37 + src/modules/stats/stats.module.ts | 14 + src/modules/stats/stats.service.spec.ts | 228 + src/modules/stats/stats.service.ts | 356 + .../status/dto/send-media-status.dto.spec.ts | 98 + .../status/dto/send-media-status.dto.ts | 91 + .../status/dto/send-text-status.dto.spec.ts | 52 + .../status/dto/send-text-status.dto.ts | 48 + src/modules/status/status.controller.ts | 70 + src/modules/status/status.module.ts | 12 + src/modules/status/status.service.ts | 70 + src/modules/template/dto/index.ts | 1 + src/modules/template/dto/template.dto.ts | 102 + .../template/entities/template.entity.ts | 47 + src/modules/template/template.controller.ts | 69 + src/modules/template/template.module.ts | 13 + src/modules/template/template.service.spec.ts | 253 + src/modules/template/template.service.ts | 104 + src/modules/webhook/dto/index.ts | 1 + .../webhook/dto/is-header-map.validator.ts | 46 + src/modules/webhook/dto/webhook.dto.spec.ts | 124 + src/modules/webhook/dto/webhook.dto.ts | 216 + .../webhook-delivery-failure.entity.ts | 51 + .../webhook/entities/webhook.entity.ts | 63 + .../webhook/filters/filter-evaluator.spec.ts | 169 + .../webhook/filters/filter-evaluator.ts | 106 + src/modules/webhook/filters/filter-types.ts | 142 + .../webhook/filters/filter-validation.ts | 107 + .../webhook/utils/idempotency.util.spec.ts | 169 + src/modules/webhook/utils/idempotency.util.ts | 106 + .../utils/record-delivery-failure.spec.ts | 49 + .../webhook/utils/record-delivery-failure.ts | 47 + .../webhook/webhook-session-scope.spec.ts | 83 + .../webhook/webhook.controller.spec.ts | 142 + src/modules/webhook/webhook.controller.ts | 98 + src/modules/webhook/webhook.module.ts | 26 + src/modules/webhook/webhook.service.spec.ts | 1064 + src/modules/webhook/webhook.service.ts | 607 + .../webhook/webhooks-list.controller.ts | 57 + src/plugins/engines/baileys/index.spec.ts | 87 + src/plugins/engines/baileys/index.ts | 96 + src/plugins/engines/baileys/manifest.json | 21 + .../engines/whatsapp-web-js/index.spec.ts | 82 + src/plugins/engines/whatsapp-web-js/index.ts | 115 + .../engines/whatsapp-web-js/manifest.json | 41 + test/__mocks__/@whiskeysockets/baileys.ts | 44 + test/app.e2e-spec.ts | 69 + test/baileys-engine.e2e-spec.ts | 71 + test/fixtures/sandbox/cap-echo-plugin.cjs | 11 + test/fixtures/sandbox/ctx-aware-plugin.cjs | 7 + .../fixtures/sandbox/ctx-lifecycle-plugin.cjs | 15 + test/fixtures/sandbox/echo-plugin.cjs | 10 + test/fixtures/sandbox/hook-config-plugin.cjs | 13 + test/fixtures/sandbox/hook-hang-plugin.cjs | 7 + test/fixtures/sandbox/hook-plugin.cjs | 10 + test/fixtures/sandbox/runaway-plugin.cjs | 11 + test/fixtures/sandbox/search-plugin.cjs | 31 + test/ingress-instance-throttle.e2e-spec.ts | 141 + test/integration-fabric.e2e-spec.ts | 181 + test/integration-instance.e2e-spec.ts | 166 + test/jest-e2e.json | 11 + test/mcp-auth.e2e-spec.ts | 174 + test/search.e2e-spec.ts | 189 + test/serve-static.e2e-spec.ts | 86 + test/setup-e2e.ts | 28 + test/webhooks.e2e-spec.ts | 346 + tsconfig.build.json | 5 + tsconfig.json | 26 + 927 files changed, 172802 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .env.minimal create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/java-sdk-release.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/sdk-ci.yml create mode 100644 .github/workflows/split-php-sdk.yml create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 .prettierrc create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 SECURITY.md create mode 100644 dashboard/.gitignore create mode 100644 dashboard/.npmrc create mode 100644 dashboard/README.md create mode 100644 dashboard/eslint.config.js create mode 100644 dashboard/index.html create mode 100644 dashboard/package-lock.json create mode 100644 dashboard/package.json create mode 100644 dashboard/public/favicon.svg create mode 100644 dashboard/public/openwa_logo.webp create mode 100644 dashboard/public/vite.svg create mode 100644 dashboard/scripts/check-i18n-parity.mjs create mode 100644 dashboard/src/App.css create mode 100644 dashboard/src/App.tsx create mode 100644 dashboard/src/assets/icons/folder.svg create mode 100644 dashboard/src/assets/icons/postgresql.svg create mode 100644 dashboard/src/assets/icons/s3.svg create mode 100644 dashboard/src/assets/icons/sqlite.svg create mode 100644 dashboard/src/assets/react.svg create mode 100644 dashboard/src/components/CustomSelect.css create mode 100644 dashboard/src/components/CustomSelect.tsx create mode 100644 dashboard/src/components/DashboardCharts.css create mode 100644 dashboard/src/components/DashboardCharts.tsx create mode 100644 dashboard/src/components/ErrorBoundary.tsx create mode 100644 dashboard/src/components/FilterBuilder.css create mode 100644 dashboard/src/components/FilterBuilder.tsx create mode 100644 dashboard/src/components/GithubIcon.tsx create mode 100644 dashboard/src/components/GlobalSearch.css create mode 100644 dashboard/src/components/GlobalSearch.tsx create mode 100644 dashboard/src/components/Layout.css create mode 100644 dashboard/src/components/Layout.tsx create mode 100644 dashboard/src/components/PageHeader.css create mode 100644 dashboard/src/components/PageHeader.tsx create mode 100644 dashboard/src/components/PluginInstances.css create mode 100644 dashboard/src/components/PluginInstances.tsx create mode 100644 dashboard/src/components/Toast.css create mode 100644 dashboard/src/components/Toast.tsx create mode 100644 dashboard/src/components/chats/MediaLightbox.tsx create mode 100644 dashboard/src/components/chats/MessageBody.tsx create mode 100644 dashboard/src/hooks/queries.ts create mode 100644 dashboard/src/hooks/useChatMessages.ts create mode 100644 dashboard/src/hooks/useChatScrollPosition.test.ts create mode 100644 dashboard/src/hooks/useChatScrollPosition.ts create mode 100644 dashboard/src/hooks/useDocumentTitle.ts create mode 100644 dashboard/src/hooks/useRole.tsx create mode 100644 dashboard/src/hooks/useTheme.ts create mode 100644 dashboard/src/hooks/useWebSocket.ts create mode 100644 dashboard/src/i18n/index.ts create mode 100644 dashboard/src/i18n/locales/ar.json create mode 100644 dashboard/src/i18n/locales/en.json create mode 100644 dashboard/src/i18n/locales/es.json create mode 100644 dashboard/src/i18n/locales/fr.json create mode 100644 dashboard/src/i18n/locales/he.json create mode 100644 dashboard/src/i18n/locales/it.json create mode 100644 dashboard/src/i18n/locales/ko.json create mode 100644 dashboard/src/i18n/locales/pt-BR.json create mode 100644 dashboard/src/i18n/locales/te.json create mode 100644 dashboard/src/i18n/locales/zh-CN.json create mode 100644 dashboard/src/i18n/locales/zh-HK.json create mode 100644 dashboard/src/index.css create mode 100644 dashboard/src/main.tsx create mode 100644 dashboard/src/pages/ApiKeys.css create mode 100644 dashboard/src/pages/ApiKeys.tsx create mode 100644 dashboard/src/pages/Chats.css create mode 100644 dashboard/src/pages/Chats.tsx create mode 100644 dashboard/src/pages/Dashboard.css create mode 100644 dashboard/src/pages/Dashboard.tsx create mode 100644 dashboard/src/pages/Infrastructure.css create mode 100644 dashboard/src/pages/Infrastructure.tsx create mode 100644 dashboard/src/pages/Login.css create mode 100644 dashboard/src/pages/Login.tsx create mode 100644 dashboard/src/pages/Logs.css create mode 100644 dashboard/src/pages/Logs.tsx create mode 100644 dashboard/src/pages/MessageTester.css create mode 100644 dashboard/src/pages/MessageTester.tsx create mode 100644 dashboard/src/pages/Plugins.css create mode 100644 dashboard/src/pages/Plugins.tsx create mode 100644 dashboard/src/pages/Sessions.css create mode 100644 dashboard/src/pages/Sessions.tsx create mode 100644 dashboard/src/pages/Templates.css create mode 100644 dashboard/src/pages/Templates.tsx create mode 100644 dashboard/src/pages/Webhooks.css create mode 100644 dashboard/src/pages/Webhooks.tsx create mode 100644 dashboard/src/services/api.ts create mode 100644 dashboard/src/styles.scope.test.ts create mode 100644 dashboard/src/types/role.ts create mode 100644 dashboard/src/utils/chatMessages.test.ts create mode 100644 dashboard/src/utils/chatMessages.ts create mode 100644 dashboard/src/utils/chunkReload.test.ts create mode 100644 dashboard/src/utils/chunkReload.ts create mode 100644 dashboard/src/utils/clipboard.ts create mode 100644 dashboard/src/utils/instanceForm.test.ts create mode 100644 dashboard/src/utils/instanceForm.ts create mode 100644 dashboard/src/utils/lazyWithRetry.ts create mode 100644 dashboard/src/utils/localizePlugin.test.ts create mode 100644 dashboard/src/utils/localizePlugin.ts create mode 100644 dashboard/src/utils/messageFormatter.test.ts create mode 100644 dashboard/src/utils/messageFormatter.ts create mode 100644 dashboard/src/utils/pageWindow.test.ts create mode 100644 dashboard/src/utils/pageWindow.ts create mode 100644 dashboard/src/utils/pluginConfigForm.test.ts create mode 100644 dashboard/src/utils/pluginConfigForm.ts create mode 100644 dashboard/src/utils/reconnectState.test.ts create mode 100644 dashboard/src/utils/reconnectState.ts create mode 100644 dashboard/src/utils/scrollDecision.test.ts create mode 100644 dashboard/src/utils/scrollDecision.ts create mode 100644 dashboard/src/utils/search-highlight.test.ts create mode 100644 dashboard/src/utils/search-highlight.ts create mode 100644 dashboard/src/utils/urlSecurity.test.ts create mode 100644 dashboard/src/utils/urlSecurity.ts create mode 100644 dashboard/src/vite-env.d.ts create mode 100644 dashboard/tsconfig.app.json create mode 100644 dashboard/tsconfig.json create mode 100644 dashboard/tsconfig.node.json create mode 100644 dashboard/vite.config.ts create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 docker-entrypoint.sh create mode 100644 docs/01-project-overview.md create mode 100644 docs/02-requirements-specification.md create mode 100644 docs/03-system-architecture.md create mode 100644 docs/04-security-design.md create mode 100644 docs/05-database-design.md create mode 100644 docs/06-api-specification.md create mode 100644 docs/07-api-collection.md create mode 100644 docs/08-development-guidelines.md create mode 100644 docs/09-testing-strategy.md create mode 100644 docs/10-devops-infrastructure.md create mode 100644 docs/11-operational-runbooks.md create mode 100644 docs/12-troubleshooting-faq.md create mode 100644 docs/13-horizontal-scaling.md create mode 100644 docs/14-migration-guide.md create mode 100644 docs/15-project-roadmap.md create mode 100644 docs/16-risk-management.md create mode 100644 docs/17-dashboard-design.md create mode 100644 docs/18-sdk-design.md create mode 100644 docs/19-plugin-architecture.md create mode 100644 docs/20-community-guidelines.md create mode 100644 docs/21-glossary.md create mode 100644 docs/22-n8n-integration.md create mode 100644 docs/23-community-integrations.md create mode 100644 docs/23-plugin-sandboxing.md create mode 100644 docs/24-mcp-integration.md create mode 100644 docs/25-integration-fabric.md create mode 100644 docs/26-global-search.md create mode 100644 docs/27-plugin-search-providers.md create mode 100644 docs/DOCKER_ID.md create mode 100644 docs/README.md create mode 100644 docs/engine-capability-matrix.md create mode 100644 docs/examples/chat-history-limits.md create mode 100644 docs/examples/n8n-appointment-booking.md create mode 100644 docs/examples/session-phone-number-pairing.md create mode 100644 docs/examples/webhook-signature-verification.md create mode 100644 docs/logo/openwa.svg create mode 100644 docs/logo/openwa_logo.png create mode 100644 docs/logo/openwa_logo.psd create mode 100644 docs/logo/openwa_logo.webp create mode 100644 eslint.config.mjs create mode 100644 nest-cli.json create mode 100644 openapi.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100755 scripts/backup.sh create mode 100644 scripts/check-doc-versions.mjs create mode 100644 scripts/export-openapi.ts create mode 100755 scripts/openwa.sh create mode 100644 scripts/pg-uuid-smoke.js create mode 100755 scripts/postgres-init-schema.sh create mode 100755 scripts/restore.sh create mode 100644 scripts/smoke-test-docker-proxy.sh create mode 100644 scripts/smoke-test-non-root.sh create mode 100644 sdk/README.md create mode 100644 sdk/java/.gitignore create mode 100644 sdk/java/README.md create mode 100644 sdk/java/pom.xml create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/ClientConfig.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/OpenWAClient.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/errors/OpenWAApiError.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/errors/OpenWAAuthError.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/errors/OpenWAConflictError.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/errors/OpenWAError.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/errors/OpenWAForbiddenError.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/errors/OpenWANotFoundError.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/errors/OpenWANotImplementedError.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/errors/OpenWARateLimitError.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/errors/OpenWATimeoutError.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/http/DefaultHttpTransport.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/http/Http.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/http/HttpMethod.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/http/HttpRequestData.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/http/HttpResponseData.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/http/HttpTransport.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/AddLabelRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/AuthValidateResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/BatchMessageResult.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/BatchProgress.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/BatchStatusResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/BulkMessageContent.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/BulkMessageItem.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/BulkMessageResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/BulkMessageType.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/CatalogInfo.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/CatalogProduct.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/CatalogProductsQuery.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ChannelMessageQuery.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ChannelRecord.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ChatHistoryMessage.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ChatState.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ChatSummary.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/CheckNumberResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ContactPhoneResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ContactRecord.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/CreateGroupRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/CreateSessionRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/CreateTemplateRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/CreateWebhookRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/DeleteChatRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/DeleteMessageRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/DeliveryStatus.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ForwardMessageRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/GroupDescriptionRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/GroupInfo.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/GroupParticipant.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/GroupSubjectRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/GroupSummary.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/HealthReadyDetails.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/HealthReadyResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/HealthResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/InviteCodeResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/LabelRecord.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ListChatsQuery.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ListContactsQuery.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ListGroupsQuery.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ListMessagesQuery.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/MarkChatRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/MessageDirection.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/MessageHistoryQuery.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/MessageListResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/MessageRecord.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/MessageResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/PaginatedProducts.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/PairingCodeResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ParticipantsRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ProfilePictureResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/QrCodeResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ReactMessageRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ReactionRecord.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ReactionSender.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/ReplyMessageRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/RequestPairingCodeRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SearchHit.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SearchQuery.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SearchResults.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendBulkRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendCatalogRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendChatStateRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendContactRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendImageStatusRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendLocationRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendMediaRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendProductRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendTemplateRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendTextRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendTextStatusRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SendVideoStatusRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SessionResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SessionStatsOverview.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SessionStatus.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/StatusListResult.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/StatusMediaInput.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/StatusRecord.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/StatusResult.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SubscribeChannelRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/SuccessResult.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/TemplateRecord.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/UpdateTemplateRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/UpdateWebhookRequest.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/WebhookEvent.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/WebhookFilterCondition.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/WebhookFilters.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/WebhookResponse.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/model/WebhookTestResult.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/package-info.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/CatalogResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/ChannelsResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/ChatsResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/ContactsResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/GroupsResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/HealthResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/LabelsResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/MessagesResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/SearchResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/SessionsResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/StatusResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/TemplatesResource.java create mode 100644 sdk/java/src/main/java/com/rmyndharis/openwa/resources/WebhooksResource.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/ClientTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/ConfigTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/ScaffoldTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/errors/ErrorsTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/http/HttpTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/CatalogResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/ChannelsResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/ChatsResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/ContactsResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/GroupsResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/HealthResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/LabelsResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/MessagesResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/SearchResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/SessionsResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/StatusResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/TemplatesResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/resources/WebhooksResourceTest.java create mode 100644 sdk/java/src/test/java/com/rmyndharis/openwa/support/MockTransport.java create mode 100644 sdk/javascript/README.md create mode 100644 sdk/javascript/package-lock.json create mode 100644 sdk/javascript/package.json create mode 100644 sdk/javascript/scripts/smoke.mjs create mode 100644 sdk/javascript/src/client.ts create mode 100644 sdk/javascript/src/errors.ts create mode 100644 sdk/javascript/src/http.ts create mode 100644 sdk/javascript/src/index.ts create mode 100644 sdk/javascript/src/resources/catalog.ts create mode 100644 sdk/javascript/src/resources/channels.ts create mode 100644 sdk/javascript/src/resources/chats.ts create mode 100644 sdk/javascript/src/resources/contacts.ts create mode 100644 sdk/javascript/src/resources/groups.ts create mode 100644 sdk/javascript/src/resources/health.ts create mode 100644 sdk/javascript/src/resources/labels.ts create mode 100644 sdk/javascript/src/resources/messages.ts create mode 100644 sdk/javascript/src/resources/search.ts create mode 100644 sdk/javascript/src/resources/sessions.ts create mode 100644 sdk/javascript/src/resources/status.ts create mode 100644 sdk/javascript/src/resources/templates.ts create mode 100644 sdk/javascript/src/resources/webhooks.ts create mode 100644 sdk/javascript/src/types.ts create mode 100644 sdk/javascript/test/client.test.ts create mode 100644 sdk/javascript/test/helpers.ts create mode 100644 sdk/javascript/test/labels-channels-catalog.test.ts create mode 100644 sdk/javascript/test/messages.test.ts create mode 100644 sdk/javascript/test/resources.test.ts create mode 100644 sdk/javascript/test/sessions.test.ts create mode 100644 sdk/javascript/tsconfig.cjs.json create mode 100644 sdk/javascript/tsconfig.esm.json create mode 100644 sdk/javascript/tsconfig.json create mode 100644 sdk/javascript/vitest.config.ts create mode 100644 sdk/php/.gitattributes create mode 100644 sdk/php/.gitignore create mode 100644 sdk/php/README.md create mode 100644 sdk/php/composer.json create mode 100644 sdk/php/composer.lock create mode 100644 sdk/php/phpunit.xml create mode 100644 sdk/php/src/Client.php create mode 100644 sdk/php/src/Exceptions/OpenWAApiException.php create mode 100644 sdk/php/src/Exceptions/OpenWAAuthException.php create mode 100644 sdk/php/src/Exceptions/OpenWAConflictException.php create mode 100644 sdk/php/src/Exceptions/OpenWAException.php create mode 100644 sdk/php/src/Exceptions/OpenWAForbiddenException.php create mode 100644 sdk/php/src/Exceptions/OpenWANotFoundException.php create mode 100644 sdk/php/src/Exceptions/OpenWANotImplementedException.php create mode 100644 sdk/php/src/Exceptions/OpenWARateLimitException.php create mode 100644 sdk/php/src/Exceptions/OpenWATimeoutException.php create mode 100644 sdk/php/src/Http/HttpExecutor.php create mode 100644 sdk/php/src/Resources/CatalogResource.php create mode 100644 sdk/php/src/Resources/ChannelsResource.php create mode 100644 sdk/php/src/Resources/ChatsResource.php create mode 100644 sdk/php/src/Resources/ContactsResource.php create mode 100644 sdk/php/src/Resources/GroupsResource.php create mode 100644 sdk/php/src/Resources/HealthResource.php create mode 100644 sdk/php/src/Resources/LabelsResource.php create mode 100644 sdk/php/src/Resources/MessagesResource.php create mode 100644 sdk/php/src/Resources/SearchResource.php create mode 100644 sdk/php/src/Resources/SessionsResource.php create mode 100644 sdk/php/src/Resources/StatusResource.php create mode 100644 sdk/php/src/Resources/TemplatesResource.php create mode 100644 sdk/php/src/Resources/WebhooksResource.php create mode 100644 sdk/php/tests/ClientTest.php create mode 100644 sdk/php/tests/LabelsChannelsCatalogTest.php create mode 100644 sdk/php/tests/MessagesTest.php create mode 100644 sdk/php/tests/MockBackend.php create mode 100644 sdk/php/tests/ResourcesTest.php create mode 100644 sdk/php/tests/SearchTest.php create mode 100644 sdk/php/tests/TemplatesTest.php create mode 100644 sdk/python/.gitignore create mode 100644 sdk/python/README.md create mode 100644 sdk/python/openwa/__init__.py create mode 100644 sdk/python/openwa/_http.py create mode 100644 sdk/python/openwa/client.py create mode 100644 sdk/python/openwa/errors.py create mode 100644 sdk/python/openwa/py.typed create mode 100644 sdk/python/openwa/resources/__init__.py create mode 100644 sdk/python/openwa/resources/catalog.py create mode 100644 sdk/python/openwa/resources/channels.py create mode 100644 sdk/python/openwa/resources/chats.py create mode 100644 sdk/python/openwa/resources/contacts.py create mode 100644 sdk/python/openwa/resources/groups.py create mode 100644 sdk/python/openwa/resources/health.py create mode 100644 sdk/python/openwa/resources/labels.py create mode 100644 sdk/python/openwa/resources/messages.py create mode 100644 sdk/python/openwa/resources/py.typed create mode 100644 sdk/python/openwa/resources/search.py create mode 100644 sdk/python/openwa/resources/sessions.py create mode 100644 sdk/python/openwa/resources/status.py create mode 100644 sdk/python/openwa/resources/templates.py create mode 100644 sdk/python/openwa/resources/webhooks.py create mode 100644 sdk/python/openwa/types.py create mode 100644 sdk/python/pyproject.toml create mode 100644 sdk/python/tests/conftest.py create mode 100644 sdk/python/tests/test_sdk.py create mode 100644 src/app.module.ts create mode 100644 src/common/cache/cache.module.ts create mode 100644 src/common/cache/cache.service.spec.ts create mode 100644 src/common/cache/cache.service.ts create mode 100644 src/common/cache/index.ts create mode 100644 src/common/errors/channel-media-not-supported.error.ts create mode 100644 src/common/errors/channel-not-found.error.ts create mode 100644 src/common/errors/chat-labels-unsupported.error.spec.ts create mode 100644 src/common/errors/chat-labels-unsupported.error.ts create mode 100644 src/common/errors/engine-not-ready.error.ts create mode 100644 src/common/errors/engine-not-supported.error.spec.ts create mode 100644 src/common/errors/engine-not-supported.error.ts create mode 100644 src/common/errors/message-not-found.error.spec.ts create mode 100644 src/common/errors/message-not-found.error.ts create mode 100644 src/common/interceptors/request-metrics.interceptor.spec.ts create mode 100644 src/common/interceptors/request-metrics.interceptor.ts create mode 100644 src/common/media/load-remote-media.spec.ts create mode 100644 src/common/media/load-remote-media.ts create mode 100644 src/common/metrics/request-metrics.spec.ts create mode 100644 src/common/metrics/request-metrics.ts create mode 100644 src/common/metrics/webhook-delivery-metrics.spec.ts create mode 100644 src/common/metrics/webhook-delivery-metrics.ts create mode 100644 src/common/middleware/request-context.middleware.spec.ts create mode 100644 src/common/middleware/request-context.middleware.ts create mode 100644 src/common/security/bull-board-auth.middleware.spec.ts create mode 100644 src/common/security/bull-board-auth.middleware.ts create mode 100644 src/common/security/proxy-aware-throttler.guard.spec.ts create mode 100644 src/common/security/proxy-aware-throttler.guard.ts create mode 100644 src/common/security/ssrf-guard.spec.ts create mode 100644 src/common/security/ssrf-guard.ts create mode 100644 src/common/security/ssrf-undici-pin.spec.ts create mode 100644 src/common/services/logger.module.ts create mode 100644 src/common/services/logger.service.spec.ts create mode 100644 src/common/services/logger.service.ts create mode 100644 src/common/services/request-context.spec.ts create mode 100644 src/common/services/request-context.ts create mode 100644 src/common/services/shutdown.service.spec.ts create mode 100644 src/common/services/shutdown.service.ts create mode 100644 src/common/storage/storage.module.ts create mode 100644 src/common/storage/storage.service.spec.ts create mode 100644 src/common/storage/storage.service.ts create mode 100644 src/common/throttler/redis-throttler.storage.spec.ts create mode 100644 src/common/throttler/redis-throttler.storage.ts create mode 100644 src/common/transformers/date.transformer.spec.ts create mode 100644 src/common/transformers/date.transformer.ts create mode 100644 src/common/utils/column-types.spec.ts create mode 100644 src/common/utils/column-types.ts create mode 100644 src/common/utils/concurrency-limiter.spec.ts create mode 100644 src/common/utils/concurrency-limiter.ts create mode 100644 src/common/utils/db-errors.spec.ts create mode 100644 src/common/utils/db-errors.ts create mode 100644 src/common/utils/ip.spec.ts create mode 100644 src/common/utils/ip.ts create mode 100644 src/common/utils/paginate.spec.ts create mode 100644 src/common/utils/paginate.ts create mode 100644 src/common/utils/path-safety.spec.ts create mode 100644 src/common/utils/path-safety.ts create mode 100644 src/common/utils/secret-file.spec.ts create mode 100644 src/common/utils/secret-file.ts create mode 100644 src/common/utils/template-render.spec.ts create mode 100644 src/common/utils/template-render.ts create mode 100644 src/common/utils/unique-constraint.util.spec.ts create mode 100644 src/common/utils/unique-constraint.util.ts create mode 100644 src/config/bootstrap-security.spec.ts create mode 100644 src/config/bootstrap-security.ts create mode 100644 src/config/configuration.spec.ts create mode 100644 src/config/configuration.ts create mode 100644 src/config/env-precedence.spec.ts create mode 100644 src/config/env-precedence.ts create mode 100644 src/config/env.validation.spec.ts create mode 100644 src/config/env.validation.ts create mode 100644 src/config/feature-flags.spec.ts create mode 100644 src/config/feature-flags.ts create mode 100644 src/config/http-timeouts.spec.ts create mode 100644 src/config/http-timeouts.ts create mode 100644 src/config/load-env.spec.ts create mode 100644 src/config/load-env.ts create mode 100644 src/config/process-error-monitor.spec.ts create mode 100644 src/config/process-error-monitor.ts create mode 100644 src/config/swagger.config.spec.ts create mode 100644 src/config/swagger.config.ts create mode 100644 src/core/agent-tools/agent-tools.module.ts create mode 100644 src/core/agent-tools/session-scope-invariant.spec.ts create mode 100644 src/core/agent-tools/tool-descriptor.ts create mode 100644 src/core/agent-tools/tool-invoker.spec.ts create mode 100644 src/core/agent-tools/tool-invoker.ts create mode 100644 src/core/agent-tools/tool-registry.service.ts create mode 100644 src/core/agent-tools/tool-registry.spec.ts create mode 100644 src/core/agent-tools/tools/contact.tools.ts create mode 100644 src/core/agent-tools/tools/group.tools.ts create mode 100644 src/core/agent-tools/tools/message.tools.ts create mode 100644 src/core/agent-tools/tools/session.tools.ts create mode 100644 src/core/agent-tools/tools/tool-output-sanitisation.spec.ts create mode 100644 src/core/agent-tools/tools/webhook.tools.ts create mode 100644 src/core/hooks/hook-manager.service.spec.ts create mode 100644 src/core/hooks/hook-manager.service.ts create mode 100644 src/core/hooks/hook.interfaces.spec.ts create mode 100644 src/core/hooks/hook.interfaces.ts create mode 100644 src/core/hooks/hooks.module.ts create mode 100644 src/core/hooks/index.ts create mode 100644 src/core/plugins/conversation-send-facade.spec.ts create mode 100644 src/core/plugins/conversation-send-facade.ts create mode 100644 src/core/plugins/handover-gate.spec.ts create mode 100644 src/core/plugins/handover-gate.ts create mode 100644 src/core/plugins/index.ts create mode 100644 src/core/plugins/plugin-activation.spec.ts create mode 100644 src/core/plugins/plugin-activation.ts create mode 100644 src/core/plugins/plugin-capability.spec.ts create mode 100644 src/core/plugins/plugin-loader-activation.spec.ts create mode 100644 src/core/plugins/plugin-loader-sandbox-routing.spec.ts create mode 100644 src/core/plugins/plugin-loader.service.spec.ts create mode 100644 src/core/plugins/plugin-loader.service.ts create mode 100644 src/core/plugins/plugin-manifest-ingress.spec.ts create mode 100644 src/core/plugins/plugin-net.spec.ts create mode 100644 src/core/plugins/plugin-net.ts create mode 100644 src/core/plugins/plugin-storage.service.spec.ts create mode 100644 src/core/plugins/plugin-storage.service.ts create mode 100644 src/core/plugins/plugin.interfaces.ts create mode 100644 src/core/plugins/plugins.module.ts create mode 100644 src/core/plugins/sandbox/capability-router.spec.ts create mode 100644 src/core/plugins/sandbox/capability-router.ts create mode 100644 src/core/plugins/sandbox/conversation-send.spec.ts create mode 100644 src/core/plugins/sandbox/dispatch-webhook.spec.ts create mode 100644 src/core/plugins/sandbox/handover-set.spec.ts create mode 100644 src/core/plugins/sandbox/plugin-worker-host.spec.ts create mode 100644 src/core/plugins/sandbox/plugin-worker-host.ts create mode 100644 src/core/plugins/sandbox/plugin-worker.integration.spec.ts create mode 100644 src/core/plugins/sandbox/protocol.ts create mode 100644 src/core/plugins/sandbox/worker-bootstrap.ts create mode 100644 src/core/plugins/sandbox/worker-capability.spec.ts create mode 100644 src/core/plugins/sandbox/worker-capability.ts create mode 100644 src/core/plugins/sandbox/worker-hooks.spec.ts create mode 100644 src/core/plugins/sandbox/worker-hooks.ts create mode 100644 src/core/plugins/sandbox/worker-search-registry.spec.ts create mode 100644 src/core/plugins/sandbox/worker-search-registry.ts create mode 100644 src/core/plugins/sandbox/worker-thread-channel.ts create mode 100644 src/core/plugins/sandbox/worker-webhooks.ts create mode 100644 src/core/plugins/search-provider-registration.spec.ts create mode 100644 src/core/plugins/search-provider-registration.util.ts create mode 100644 src/core/plugins/webhook-subscribe.spec.ts create mode 100644 src/core/plugins/webhook-subscribe.util.ts create mode 100644 src/database/add-integration-fabric.spec.ts create mode 100644 src/database/add-integration-uuid-defaults.spec.ts create mode 100644 src/database/add-uuid-defaults-migration.spec.ts create mode 100644 src/database/add-webhooks-sessionid-index.spec.ts create mode 100644 src/database/data-source-main.spec.ts create mode 100644 src/database/data-source-main.ts create mode 100644 src/database/data-source.spec.ts create mode 100644 src/database/data-source.ts create mode 100644 src/database/load-cli-env.spec.ts create mode 100644 src/database/load-cli-env.ts create mode 100644 src/database/migrations-main/1779900000000-CreateAuthAuditTables.ts create mode 100644 src/database/migrations-main/__tests__/1779900000000-CreateAuthAuditTables.spec.ts create mode 100644 src/database/migrations/.gitkeep create mode 100644 src/database/migrations/1770108659848-AddMessageStatus.ts create mode 100644 src/database/migrations/1770200000000-NormalizeSynchronizeUuidColumns.ts create mode 100644 src/database/migrations/1779235200000-AddUuidDefaultsForPostgres.ts create mode 100644 src/database/migrations/1779840000000-AddTemplates.ts create mode 100644 src/database/migrations/1779900100000-AddMessageSessionWaIndex.ts create mode 100644 src/database/migrations/1781000000000-AddBaileysStoredMessages.ts create mode 100644 src/database/migrations/1781100000000-AddTemplateNameUnique.ts create mode 100644 src/database/migrations/1781200000000-AddLidMappings.ts create mode 100644 src/database/migrations/1781300000000-AddMessagesWaMessageIdUnique.ts create mode 100644 src/database/migrations/1781500000000-AddWebhookFilters.ts create mode 100644 src/database/migrations/1781600000000-DropRedundantMessagesSessionIdIndex.ts create mode 100644 src/database/migrations/1781700000000-AddWebhookDeliveryFailures.ts create mode 100644 src/database/migrations/1781800000000-ScopeBatchIdUniqueToSession.ts create mode 100644 src/database/migrations/1781900000000-AddIntegrationFabric.ts create mode 100644 src/database/migrations/1782000000000-AddMessageChatName.ts create mode 100644 src/database/migrations/1782100000000-WidenIngressDedupKey.ts create mode 100644 src/database/migrations/1782200000000-AddWebhooksSessionIdIndex.ts create mode 100644 src/database/migrations/1782300000000-AddIntegrationUuidDefaults.ts create mode 100644 src/database/migrations/1782400000000-AddMessagesFts.ts create mode 100644 src/database/migrations/__tests__/1770108659848-AddMessageStatus.spec.ts create mode 100644 src/database/migrations/__tests__/1770200000000-NormalizeSynchronizeUuidColumns.spec.ts create mode 100644 src/database/migrations/__tests__/1779840000000-AddTemplates.spec.ts create mode 100644 src/database/migrations/__tests__/1781000000000-AddBaileysStoredMessages.spec.ts create mode 100644 src/database/migrations/__tests__/1781100000000-AddTemplateNameUnique.spec.ts create mode 100644 src/database/migrations/__tests__/1781200000000-AddLidMappings.spec.ts create mode 100644 src/database/migrations/__tests__/1781300000000-AddMessagesWaMessageIdUnique.spec.ts create mode 100644 src/database/migrations/__tests__/1781600000000-DropRedundantMessagesSessionIdIndex.spec.ts create mode 100644 src/database/migrations/__tests__/1781700000000-AddWebhookDeliveryFailures.spec.ts create mode 100644 src/database/migrations/__tests__/1781800000000-ScopeBatchIdUniqueToSession.spec.ts create mode 100644 src/database/migrations/__tests__/1782400000000-AddMessagesFts.pg.spec.ts create mode 100644 src/database/migrations/__tests__/1782400000000-AddMessagesFts.spec.ts create mode 100644 src/engine/adapters/baileys-group-mapper.spec.ts create mode 100644 src/engine/adapters/baileys-group-mapper.ts create mode 100644 src/engine/adapters/baileys-message-mapper.spec.ts create mode 100644 src/engine/adapters/baileys-message-mapper.ts create mode 100644 src/engine/adapters/baileys-message-store.service.spec.ts create mode 100644 src/engine/adapters/baileys-message-store.service.ts create mode 100644 src/engine/adapters/baileys-session-store.spec.ts create mode 100644 src/engine/adapters/baileys-session-store.ts create mode 100644 src/engine/adapters/baileys-stored-message.entity.ts create mode 100644 src/engine/adapters/baileys.adapter.spec.ts create mode 100644 src/engine/adapters/baileys.adapter.ts create mode 100644 src/engine/adapters/inbound-media-cap.spec.ts create mode 100644 src/engine/adapters/inbound-media-cap.ts create mode 100644 src/engine/adapters/message-mapper.spec.ts create mode 100644 src/engine/adapters/message-mapper.ts create mode 100644 src/engine/adapters/vcard.spec.ts create mode 100644 src/engine/adapters/vcard.ts create mode 100644 src/engine/adapters/whatsapp-web-js.adapter.spec.ts create mode 100644 src/engine/adapters/whatsapp-web-js.adapter.ts create mode 100644 src/engine/engine-capability-matrix.ts create mode 100644 src/engine/engine-parity.spec.ts create mode 100644 src/engine/engine.factory.spec.ts create mode 100644 src/engine/engine.factory.ts create mode 100644 src/engine/engine.module.ts create mode 100644 src/engine/identity/lid-mapping-store.service.ts create mode 100644 src/engine/identity/lid-mapping-store.spec.ts create mode 100644 src/engine/identity/lid-mapping.entity.ts create mode 100644 src/engine/identity/wa-id.spec.ts create mode 100644 src/engine/identity/wa-id.ts create mode 100644 src/engine/interfaces/whatsapp-engine.interface.ts create mode 100644 src/engine/types/baileys.types.ts create mode 100644 src/engine/types/whatsapp-web-js.types.ts create mode 100644 src/engine/wa-web-version.spec.ts create mode 100644 src/engine/wa-web-version.ts create mode 100644 src/main.ts create mode 100644 src/modules/audit/audit-coverage.spec.ts create mode 100644 src/modules/audit/audit.controller.spec.ts create mode 100644 src/modules/audit/audit.controller.ts create mode 100644 src/modules/audit/audit.module.ts create mode 100644 src/modules/audit/audit.service.spec.ts create mode 100644 src/modules/audit/audit.service.ts create mode 100644 src/modules/audit/entities/audit-log.entity.ts create mode 100644 src/modules/audit/intentionally-unemitted-actions.ts create mode 100644 src/modules/auth/api-key-hash.spec.ts create mode 100644 src/modules/auth/api-key-hash.ts create mode 100644 src/modules/auth/auth-validate.controller.spec.ts create mode 100644 src/modules/auth/auth-validate.controller.ts create mode 100644 src/modules/auth/auth.controller.spec.ts create mode 100644 src/modules/auth/auth.controller.ts create mode 100644 src/modules/auth/auth.module.ts create mode 100644 src/modules/auth/auth.service.spec.ts create mode 100644 src/modules/auth/auth.service.ts create mode 100644 src/modules/auth/decorators/auth.decorators.ts create mode 100644 src/modules/auth/dto/api-key.dto.ts create mode 100644 src/modules/auth/dto/index.ts create mode 100644 src/modules/auth/dto/is-ip-or-cidr.validator.spec.ts create mode 100644 src/modules/auth/dto/is-ip-or-cidr.validator.ts create mode 100644 src/modules/auth/entities/api-key.entity.ts create mode 100644 src/modules/auth/guards/api-key.guard.spec.ts create mode 100644 src/modules/auth/guards/api-key.guard.ts create mode 100644 src/modules/auth/role-coverage.spec.ts create mode 100644 src/modules/catalog/catalog.controller.ts create mode 100644 src/modules/catalog/catalog.module.ts create mode 100644 src/modules/catalog/catalog.service.ts create mode 100644 src/modules/catalog/dto/send-product.dto.ts create mode 100644 src/modules/channel/channel.controller.spec.ts create mode 100644 src/modules/channel/channel.controller.ts create mode 100644 src/modules/channel/channel.module.ts create mode 100644 src/modules/channel/channel.service.spec.ts create mode 100644 src/modules/channel/channel.service.ts create mode 100644 src/modules/channel/dto/subscribe-channel.dto.ts create mode 100644 src/modules/contact/contact.controller.ts create mode 100644 src/modules/contact/contact.module.ts create mode 100644 src/modules/contact/contact.service.spec.ts create mode 100644 src/modules/contact/contact.service.ts create mode 100644 src/modules/docker/compose-network.spec.ts create mode 100644 src/modules/docker/docker.module.ts create mode 100644 src/modules/docker/docker.service.spec.ts create mode 100644 src/modules/docker/docker.service.ts create mode 100644 src/modules/docker/index.ts create mode 100644 src/modules/events/dto/ws-messages.dto.ts create mode 100644 src/modules/events/events.gateway.spec.ts create mode 100644 src/modules/events/events.gateway.ts create mode 100644 src/modules/events/events.module.ts create mode 100644 src/modules/group/dto/group.dto.spec.ts create mode 100644 src/modules/group/dto/group.dto.ts create mode 100644 src/modules/group/group.controller.ts create mode 100644 src/modules/group/group.module.ts create mode 100644 src/modules/group/group.service.spec.ts create mode 100644 src/modules/group/group.service.ts create mode 100644 src/modules/health/health.controller.spec.ts create mode 100644 src/modules/health/health.controller.ts create mode 100644 src/modules/health/health.module.ts create mode 100644 src/modules/infra/dto/import-storage.dto.ts create mode 100644 src/modules/infra/infra.controller.spec.ts create mode 100644 src/modules/infra/infra.controller.ts create mode 100644 src/modules/infra/infra.module.ts create mode 100644 src/modules/integration/__fixtures__/chatwoot-message_created.json create mode 100644 src/modules/integration/conversation-mapping.service.spec.ts create mode 100644 src/modules/integration/conversation-mapping.service.ts create mode 100644 src/modules/integration/dto/instance.dto.ts create mode 100644 src/modules/integration/entities/conversation-mapping.entity.ts create mode 100644 src/modules/integration/entities/ingress-event.entity.ts create mode 100644 src/modules/integration/entities/integration-delivery-failure.entity.ts create mode 100644 src/modules/integration/entities/plugin-instance.entity.ts create mode 100644 src/modules/integration/ingress-ack.spec.ts create mode 100644 src/modules/integration/ingress-ack.ts create mode 100644 src/modules/integration/ingress-enqueue.service.spec.ts create mode 100644 src/modules/integration/ingress-enqueue.service.ts create mode 100644 src/modules/integration/ingress-event.service.spec.ts create mode 100644 src/modules/integration/ingress-event.service.ts create mode 100644 src/modules/integration/ingress-golden.spec.ts create mode 100644 src/modules/integration/ingress-preflight.spec.ts create mode 100644 src/modules/integration/ingress-preflight.ts create mode 100644 src/modules/integration/ingress-signature.spec.ts create mode 100644 src/modules/integration/ingress-signature.ts create mode 100644 src/modules/integration/ingress-url.spec.ts create mode 100644 src/modules/integration/ingress-url.ts create mode 100644 src/modules/integration/ingress.controller.spec.ts create mode 100644 src/modules/integration/ingress.controller.ts create mode 100644 src/modules/integration/ingress.service.spec.ts create mode 100644 src/modules/integration/ingress.service.ts create mode 100644 src/modules/integration/instance-throttler.guard.spec.ts create mode 100644 src/modules/integration/instance-throttler.guard.ts create mode 100644 src/modules/integration/integration-instance.controller.spec.ts create mode 100644 src/modules/integration/integration-instance.controller.ts create mode 100644 src/modules/integration/integration-retention.service.spec.ts create mode 100644 src/modules/integration/integration-retention.service.ts create mode 100644 src/modules/integration/integration.constants.ts create mode 100644 src/modules/integration/integration.module.ts create mode 100644 src/modules/integration/ordering-lock.spec.ts create mode 100644 src/modules/integration/ordering-lock.ts create mode 100644 src/modules/integration/plugin-instance.service.spec.ts create mode 100644 src/modules/integration/plugin-instance.service.ts create mode 100644 src/modules/integration/redrive.controller.spec.ts create mode 100644 src/modules/integration/redrive.controller.ts create mode 100644 src/modules/integration/redrive.service.spec.ts create mode 100644 src/modules/integration/redrive.service.ts create mode 100644 src/modules/integration/scope-binding.service.spec.ts create mode 100644 src/modules/integration/scope-binding.service.ts create mode 100644 src/modules/label/dto/add-label.dto.ts create mode 100644 src/modules/label/label.controller.ts create mode 100644 src/modules/label/label.module.ts create mode 100644 src/modules/label/label.service.spec.ts create mode 100644 src/modules/label/label.service.ts create mode 100644 src/modules/mcp/mcp-rate-limit.spec.ts create mode 100644 src/modules/mcp/mcp-rate-limit.ts create mode 100644 src/modules/mcp/mcp.module.ts create mode 100644 src/modules/mcp/mcp.server.spec.ts create mode 100644 src/modules/mcp/mcp.server.ts create mode 100644 src/modules/mcp/tool-result.ts create mode 100644 src/modules/message/bulk-message.service.spec.ts create mode 100644 src/modules/message/bulk-message.service.ts create mode 100644 src/modules/message/dto/bulk-message.dto.spec.ts create mode 100644 src/modules/message/dto/bulk-message.dto.ts create mode 100644 src/modules/message/dto/index.ts create mode 100644 src/modules/message/dto/message-actions.dto.spec.ts create mode 100644 src/modules/message/dto/message-actions.dto.ts create mode 100644 src/modules/message/dto/send-message.dto.spec.ts create mode 100644 src/modules/message/dto/send-message.dto.ts create mode 100644 src/modules/message/dto/send-template.dto.ts create mode 100644 src/modules/message/entities/message-batch.entity.ts create mode 100644 src/modules/message/entities/message.entity.spec.ts create mode 100644 src/modules/message/entities/message.entity.ts create mode 100644 src/modules/message/media-cap.util.spec.ts create mode 100644 src/modules/message/media-cap.util.ts create mode 100644 src/modules/message/message-status.util.spec.ts create mode 100644 src/modules/message/message-status.util.ts create mode 100644 src/modules/message/message-type-backfill.service.spec.ts create mode 100644 src/modules/message/message-type-backfill.service.ts create mode 100644 src/modules/message/message.controller.ts create mode 100644 src/modules/message/message.module.ts create mode 100644 src/modules/message/message.service.spec.ts create mode 100644 src/modules/message/message.service.ts create mode 100644 src/modules/metrics/metrics.controller.ts create mode 100644 src/modules/metrics/metrics.module.ts create mode 100644 src/modules/metrics/metrics.service.spec.ts create mode 100644 src/modules/metrics/metrics.service.ts create mode 100644 src/modules/plugins/catalog.spec.ts create mode 100644 src/modules/plugins/catalog.ts create mode 100644 src/modules/plugins/dto/index.ts create mode 100644 src/modules/plugins/dto/plugin.dto.ts create mode 100644 src/modules/plugins/plugin-download.ts create mode 100644 src/modules/plugins/plugin-installer.spec.ts create mode 100644 src/modules/plugins/plugin-installer.ts create mode 100644 src/modules/plugins/plugins.controller.spec.ts create mode 100644 src/modules/plugins/plugins.controller.ts create mode 100644 src/modules/plugins/plugins.module.ts create mode 100644 src/modules/plugins/plugins.service.spec.ts create mode 100644 src/modules/plugins/plugins.service.ts create mode 100644 src/modules/plugins/redact-config.spec.ts create mode 100644 src/modules/plugins/redact-config.ts create mode 100644 src/modules/queue/processors/ingress.processor.spec.ts create mode 100644 src/modules/queue/processors/ingress.processor.ts create mode 100644 src/modules/queue/processors/webhook.processor.spec.ts create mode 100644 src/modules/queue/processors/webhook.processor.ts create mode 100644 src/modules/queue/queue-names.ts create mode 100644 src/modules/queue/queue.module.ts create mode 100644 src/modules/queue/redis-connection.spec.ts create mode 100644 src/modules/queue/redis-connection.ts create mode 100644 src/modules/search/dto/search-query.dto.spec.ts create mode 100644 src/modules/search/dto/search-query.dto.ts create mode 100644 src/modules/search/providers/builtin-fts.provider.spec.ts create mode 100644 src/modules/search/providers/builtin-fts.provider.ts create mode 100644 src/modules/search/providers/plugin-search-provider.spec.ts create mode 100644 src/modules/search/providers/plugin-search-provider.ts create mode 100644 src/modules/search/providers/search-dual-db.spec.ts create mode 100644 src/modules/search/search-provider.contract.spec.ts create mode 100644 src/modules/search/search-provider.registry.spec.ts create mode 100644 src/modules/search/search-provider.registry.ts create mode 100644 src/modules/search/search.constants.ts create mode 100644 src/modules/search/search.controller.spec.ts create mode 100644 src/modules/search/search.controller.ts create mode 100644 src/modules/search/search.module.spec.ts create mode 100644 src/modules/search/search.module.ts create mode 100644 src/modules/search/search.service.spec.ts create mode 100644 src/modules/search/search.service.ts create mode 100644 src/modules/search/search.types.spec.ts create mode 100644 src/modules/search/search.types.ts create mode 100644 src/modules/session/dto/create-session.dto.spec.ts create mode 100644 src/modules/session/dto/create-session.dto.ts create mode 100644 src/modules/session/dto/delete-chat.dto.ts create mode 100644 src/modules/session/dto/index.ts create mode 100644 src/modules/session/dto/mark-chat-read.dto.spec.ts create mode 100644 src/modules/session/dto/mark-chat-read.dto.ts create mode 100644 src/modules/session/dto/request-pairing-code.dto.spec.ts create mode 100644 src/modules/session/dto/request-pairing-code.dto.ts create mode 100644 src/modules/session/dto/send-chat-state.dto.ts create mode 100644 src/modules/session/dto/session-response.dto.ts create mode 100644 src/modules/session/entities/session.entity.ts create mode 100644 src/modules/session/reconnect-config.spec.ts create mode 100644 src/modules/session/session.controller.ts create mode 100644 src/modules/session/session.module.ts create mode 100644 src/modules/session/session.service.spec.ts create mode 100644 src/modules/session/session.service.ts create mode 100644 src/modules/settings/settings.controller.spec.ts create mode 100644 src/modules/settings/settings.controller.ts create mode 100644 src/modules/settings/settings.module.ts create mode 100644 src/modules/stats/dto/stats-query.dto.ts create mode 100644 src/modules/stats/stats.controller.spec.ts create mode 100644 src/modules/stats/stats.controller.ts create mode 100644 src/modules/stats/stats.module.ts create mode 100644 src/modules/stats/stats.service.spec.ts create mode 100644 src/modules/stats/stats.service.ts create mode 100644 src/modules/status/dto/send-media-status.dto.spec.ts create mode 100644 src/modules/status/dto/send-media-status.dto.ts create mode 100644 src/modules/status/dto/send-text-status.dto.spec.ts create mode 100644 src/modules/status/dto/send-text-status.dto.ts create mode 100644 src/modules/status/status.controller.ts create mode 100644 src/modules/status/status.module.ts create mode 100644 src/modules/status/status.service.ts create mode 100644 src/modules/template/dto/index.ts create mode 100644 src/modules/template/dto/template.dto.ts create mode 100644 src/modules/template/entities/template.entity.ts create mode 100644 src/modules/template/template.controller.ts create mode 100644 src/modules/template/template.module.ts create mode 100644 src/modules/template/template.service.spec.ts create mode 100644 src/modules/template/template.service.ts create mode 100644 src/modules/webhook/dto/index.ts create mode 100644 src/modules/webhook/dto/is-header-map.validator.ts create mode 100644 src/modules/webhook/dto/webhook.dto.spec.ts create mode 100644 src/modules/webhook/dto/webhook.dto.ts create mode 100644 src/modules/webhook/entities/webhook-delivery-failure.entity.ts create mode 100644 src/modules/webhook/entities/webhook.entity.ts create mode 100644 src/modules/webhook/filters/filter-evaluator.spec.ts create mode 100644 src/modules/webhook/filters/filter-evaluator.ts create mode 100644 src/modules/webhook/filters/filter-types.ts create mode 100644 src/modules/webhook/filters/filter-validation.ts create mode 100644 src/modules/webhook/utils/idempotency.util.spec.ts create mode 100644 src/modules/webhook/utils/idempotency.util.ts create mode 100644 src/modules/webhook/utils/record-delivery-failure.spec.ts create mode 100644 src/modules/webhook/utils/record-delivery-failure.ts create mode 100644 src/modules/webhook/webhook-session-scope.spec.ts create mode 100644 src/modules/webhook/webhook.controller.spec.ts create mode 100644 src/modules/webhook/webhook.controller.ts create mode 100644 src/modules/webhook/webhook.module.ts create mode 100644 src/modules/webhook/webhook.service.spec.ts create mode 100644 src/modules/webhook/webhook.service.ts create mode 100644 src/modules/webhook/webhooks-list.controller.ts create mode 100644 src/plugins/engines/baileys/index.spec.ts create mode 100644 src/plugins/engines/baileys/index.ts create mode 100644 src/plugins/engines/baileys/manifest.json create mode 100644 src/plugins/engines/whatsapp-web-js/index.spec.ts create mode 100644 src/plugins/engines/whatsapp-web-js/index.ts create mode 100644 src/plugins/engines/whatsapp-web-js/manifest.json create mode 100644 test/__mocks__/@whiskeysockets/baileys.ts create mode 100644 test/app.e2e-spec.ts create mode 100644 test/baileys-engine.e2e-spec.ts create mode 100644 test/fixtures/sandbox/cap-echo-plugin.cjs create mode 100644 test/fixtures/sandbox/ctx-aware-plugin.cjs create mode 100644 test/fixtures/sandbox/ctx-lifecycle-plugin.cjs create mode 100644 test/fixtures/sandbox/echo-plugin.cjs create mode 100644 test/fixtures/sandbox/hook-config-plugin.cjs create mode 100644 test/fixtures/sandbox/hook-hang-plugin.cjs create mode 100644 test/fixtures/sandbox/hook-plugin.cjs create mode 100644 test/fixtures/sandbox/runaway-plugin.cjs create mode 100644 test/fixtures/sandbox/search-plugin.cjs create mode 100644 test/ingress-instance-throttle.e2e-spec.ts create mode 100644 test/integration-fabric.e2e-spec.ts create mode 100644 test/integration-instance.e2e-spec.ts create mode 100644 test/jest-e2e.json create mode 100644 test/mcp-auth.e2e-spec.ts create mode 100644 test/search.e2e-spec.ts create mode 100644 test/serve-static.e2e-spec.ts create mode 100644 test/setup-e2e.ts create mode 100644 test/webhooks.e2e-spec.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2f12f7b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,41 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Test coverage +coverage/ + +# Environment files +.env +.env.local +.env.*.local + +# Data files (sessions, media, database) +data/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log +npm-debug.log* + +# Temporary files +tmp/ +temp/ + +# Docker +.docker/ + +# Debug +.pnpm-debug.log* diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..18cd8d0 --- /dev/null +++ b/.env.example @@ -0,0 +1,338 @@ +# ============================================================================= +# OpenWA - Environment Configuration +# ============================================================================= +# Copy this file to .env and customize as needed. +# This is the Single Source of Truth for all configuration. + +# ============================================================================= +# CORE SETTINGS +# ============================================================================= +NODE_ENV=production +# Port the app binds to when run directly (bare metal / `npm run start:prod`). In the bundled Docker +# Compose the container always listens on 2785; the API_PORT below is only the HOST-published port. +PORT=2785 +# Docker Compose only: the host-side port mapped to the container's 2785 (no effect on a bare-metal run). +API_PORT=2785 +LOG_LEVEL=info # error | warn | info | debug +# Console output format. Default: json in production (containers, log aggregators), +# human-readable pretty otherwise. Force one with: json | pretty +# LOG_FORMAT=pretty + +# Auto-start previously authenticated sessions on server boot. Recommended `true` for a SINGLE-instance +# production deployment so a crash-restart self-heals authenticated sessions; keep `false` if you run +# multiple replicas behind a scheduler (two replicas resurrecting one session risks a forced logout/ban — +# see docs/13-horizontal-scaling.md). +AUTO_START_SESSIONS=false +# 0 = unlimited. Set a positive integer to cap concurrently running/initializing sessions. +# Failed sessions still hold a slot until stopped (their engine stays resident); stop them to free capacity. +MAX_CONCURRENT_SESSIONS=0 + +# Graceful-shutdown drain: on SIGTERM/SIGINT the app flips readiness to 503 (so a load balancer stops +# routing) and keeps serving in-flight requests for this many ms before teardown. Default 3000 in +# production, 0 elsewhere (a dev hot-reload/Ctrl+C is not slowed). Capped at 30000. A second signal +# forces an immediate exit. In Docker, keep `stop_grace_period` >= this + your worst-case teardown. +# SHUTDOWN_DELAY_MS=3000 + +# Domain Configuration +DOMAIN=localhost + +# Host interface the dev compose binds the API/dashboard ports to. Defaults to 127.0.0.1 +# (localhost only). Set to 0.0.0.0 to reach the dev stack from another machine (e.g. a remote +# VPS) — put a TLS reverse proxy in front for anything internet-facing (the API key is sent in +# cleartext over plain HTTP). +# BIND_HOST=0.0.0.0 + +# Public URLs (for startup banner and external access) +# BASE_URL=https://api.yourdomain.com +# DASHBOARD_URL=https://dashboard.yourdomain.com + +# CORS Configuration: comma-separated allowed origins. The wildcard "*" is allowed +# only in development; in production it is REFUSED (cross-origin browser requests are +# blocked) — set explicit origin(s) there, e.g. https://dashboard.yourdomain.com +CORS_ORIGINS=* + +# SSL/TLS: terminate TLS at your own reverse proxy (nginx, Caddy, a cloud load +# balancer, or a k8s Ingress) in front of the API — see docs/12-troubleshooting-faq.md. + +# Trusted reverse proxies (comma-separated IPs/CIDRs) whose X-Forwarded-For +# header is trusted to determine the real client IP for API-key IP whitelisting. +# Leave empty (default) to ignore X-Forwarded-For and use the direct socket +# address, which prevents IP spoofing. If you run behind a reverse proxy AND use +# per-key allowedIps restrictions, set this to the proxy's address/subnet, e.g.: +# TRUSTED_PROXIES=172.18.0.0/16 + +# ============================================================================= +# ENGINE CONFIGURATION +# ============================================================================= +# Which WhatsApp engine to use (plugin-based) +# Options: whatsapp-web.js, baileys +# Left unset by default so the dashboard (Infrastructure > Engine) governs the active engine via +# data/.env.generated (defaults to whatsapp-web.js). Uncomment to pin an engine from the environment; +# a value set here always wins over the dashboard selection. +# ENGINE_TYPE=whatsapp-web.js + +# Engine-specific settings +SESSION_DATA_PATH=./data/sessions +PUPPETEER_HEADLESS=true +PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu +# Path to a system Chromium/Chrome binary. Leave unset to use the bundled Puppeteer browser. +# Required in the Docker image and on hosts without a bundled browser (e.g. Alpine/ARM). +# PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium + +# ── Container resource limits (docker-compose only) ─────────────────────────── +# These map to docker-compose `mem_limit` / `pids_limit` and only apply when running via the +# bundled compose files. They are read by docker-compose.yml, not by the app process. +# Override in your .env to tune for your fleet. + +# Memory ceiling for the openwa-api container. whatsapp-web.js runs a full Chromium per session, so +# raise this for multi-session deployments (e.g. 4g). Baileys (no Chromium) is far lighter. +OPENWA_MEM_LIMIT=2g + +# Per-container process (PID) ceiling — a fork-bomb guard, NOT an allocation (the kernel only +# rejects forks once the count is reached, so a higher limit is free for light containers). +# Default 2048 fits ~8-10 whatsapp-web.js sessions with startup-spike headroom; each Chromium is +# itself multi-process (browser + renderer + GPU + zygote + utilities) and WhatsApp Web is +# process-heavy, so the old default (512) could get a session's Chromium killed mid-spawn during +# startup — surfacing in the API as a `Code: null` launch failure (#636). Baileys is single-process +# and uses a handful of PIDs regardless. Raise for larger fleets; do NOT set -1 (drops the guard). +OPENWA_PIDS_LIMIT=2048 + +# Optional WhatsApp Web client version pin. Leave unset (or use "latest", "auto", or "off") to +# let whatsapp-web.js auto-select. If sessions get stuck at "authenticating" after scanning the QR, +# set this to a known-good version from https://github.com/wppconnect-team/wa-version (browse the +# html/ folder). WWEBJS_WEB_VERSION_REMOTE_PATH overrides the URL template (use {version} as the +# placeholder) if you self-host the version HTML. +# WWEBJS_WEB_VERSION=2.3000.1040641150-alpha + +# Extend the initial boot/inject wait (milliseconds) before QR generation. On slow first boots +# (WSL2 or low-resource containers) the default 30000ms can expire before WhatsApp Web finishes +# loading. Raise it (e.g. 120000) if startup times out. Unset keeps the default (30000). +# WWEBJS_AUTH_TIMEOUT_MS=120000 + +# Baileys engine (used when ENGINE_TYPE=baileys). WebSocket client, no Chromium. +# NOTE: the Baileys engine is loaded lazily (dynamic import, only when ENGINE_TYPE=baileys), so there is no +# global Node.js version floor. Node.js 22 LTS is recommended for all deployments. +BAILEYS_AUTH_DIR=./data/baileys +# NOTE: proxy (PROXY_URL/PROXY_TYPE) is not yet supported by the baileys engine; it is ignored. +# Pull the FULL message-history archive on connect (large). Off by default: the engine still enables the +# initial sync (contacts, chats, recent messages, lid mappings) without downloading the entire history. +BAILEYS_SYNC_FULL_HISTORY=false +# Surface the Baileys library's own logs for debugging (trace|debug|info|warn|error). Silent by default. +# `trace` dumps the decoded WhatsApp wire frames to stdout (context "baileys-wire"). +# BAILEYS_LOG_LEVEL=debug + +# Humanise single sends: show a "typing…" indicator and pause briefly (length-scaled, jittered) +# before each text send so messages don't look instantaneous (anti-ban). ON by default — set to +# false to disable. SIMULATE_TYPING_MAX_MS caps the pause (default 5000). Does not affect bulk +# sends, which have their own delayBetweenMessages throttle. +# SIMULATE_TYPING=false +# SIMULATE_TYPING_MAX_MS=5000 + +# Inline @lid -> phone resolution (#263). When a sender is identified by a WhatsApp privacy id +# (@lid) instead of a phone number, attach a best-effort `senderPhone` (MSISDN digits, or null when +# the engine can't map it) to the message.received webhook + websocket payload. OFF by default — +# it adds a per-sender lookup (cached). The on-demand endpoint GET /sessions/:id/contacts/:id/phone +# works regardless of this flag. +# RESOLVE_LID_TO_PHONE=true + +# Observability — Prometheus scrape endpoint at GET /api/metrics. +# Disabled by default; set a token to enable. Scrapers must send `Authorization: Bearer `. +# METRICS_TOKEN=change-me-to-a-long-random-string + +# Audit-log retention. Logs older than this many days are pruned daily (and once at startup). +# Default 90; set to 0 to keep audit logs forever (disable pruning). +# AUDIT_RETENTION_DAYS=90 + +# ============================================================================= +# DATABASE +# ============================================================================= +# Options: sqlite, postgres +DATABASE_TYPE=sqlite +POSTGRES_BUILTIN=false # Use built-in PostgreSQL container? + +# PostgreSQL settings (ignored for sqlite, auto-configured if POSTGRES_BUILTIN=true) +DATABASE_HOST=localhost +DATABASE_PORT=5432 +# PostgreSQL database name ONLY. Leave unset for SQLite (DATABASE_TYPE=sqlite) to use the +# default file path ./data/openwa.sqlite — a bare value here would become the SQLite file PATH +# and, under a read-only container rootfs, trigger a SQLITE_CANTOPEN boot-loop (#677). +# DATABASE_NAME=openwa +# PostgreSQL schema (ignored for sqlite). Default 'public' keeps the historical behavior. +# Set to a dedicated schema to isolate OpenWA's tables + migration ledger (e.g. on managed +# Postgres where you get a project schema, or to share one DB across apps). The schema must +# already exist — the built-in container creates it; for external/managed Postgres create it +# once (CREATE SCHEMA openwa;). A missing schema fails fast at migration time. +POSTGRES_SCHEMA=public +DATABASE_USERNAME=openwa +# MUST set a strong, unique value before using Postgres — no default is shipped. +# Production refuses to start if this is empty or a known placeholder when DATABASE_TYPE=postgres. +DATABASE_PASSWORD= +DATABASE_SYNCHRONIZE=false # WARNING: Set false in production! (data DB) +DATABASE_LOGGING=false +# Auth/audit (main) DB schema management. Default ON (zero-config first boot). +# Set to "false" to manage the api_keys/audit_logs schema via the bundled main-owned +# migration instead of synchronize (migrationsRun creates them at boot). +MAIN_DATABASE_SYNCHRONIZE=true +DATABASE_SSL=false # Set true for managed Postgres (Supabase, Heroku, Render, Railway) +DATABASE_SSL_REJECT_UNAUTHORIZED=true # Set false to allow self-signed certs (only when DATABASE_SSL=true) +# Postgres pool/query timeouts (ms). Defaults are conservative; set any to 0 to disable. +DATABASE_STATEMENT_TIMEOUT_MS=30000 # Aborts a single runtime query that runs longer (server-side; not applied to migrations) +DATABASE_IDLE_TIMEOUT_MS=30000 # Closes idle pooled connections after this long +DATABASE_CONNECTION_TIMEOUT_MS=10000 # Fails fast if a new connection can't be acquired in time + +# ============================================================================= +# REDIS / QUEUE (Phase 2) +# ============================================================================= +REDIS_ENABLED=false # Enable Redis for queue and caching +REDIS_BUILTIN=false # Use built-in Redis container? +# Process webhooks/ingress through the BullMQ queue (needs a reachable Redis via REDIS_HOST/REDIS_PORT). +# Off by default (inline dispatch). In the bundled Docker Compose this is dashboard-managed +# (Infrastructure > Redis & Queue) — a host value here is not forwarded to the container. +# QUEUE_ENABLED=false +# Redis-backed caching, independent of the queue. Off by default (in-memory cache). +# CACHE_ENABLED=false + +# Redis settings (auto-configured if REDIS_BUILTIN=true) +REDIS_HOST=localhost +REDIS_PORT=6379 +# Fail Redis queue/cache connection attempts after this many ms. +REDIS_CONNECT_TIMEOUT_MS=5000 +# REDIS_USERNAME= +# REDIS_PASSWORD= + +# ============================================================================= +# STORAGE +# ============================================================================= +# Options: local, s3 +STORAGE_TYPE=local +MINIO_BUILTIN=false # Use built-in MinIO container? + +# Local storage path (if STORAGE_TYPE=local) +STORAGE_LOCAL_PATH=./data/media + +# S3/MinIO settings (if STORAGE_TYPE=s3, auto-configured if MINIO_BUILTIN=true) +S3_ENDPOINT=http://localhost:9000 +S3_BUCKET=openwa +S3_REGION=us-east-1 +# MUST set strong, unique values before using S3/MinIO — no defaults shipped. +# Production refuses to start if these are empty/placeholder when STORAGE_TYPE=s3 (external). +# Canonical names (what the app reads first and the dashboard writes); the legacy +# S3_ACCESS_KEY / S3_SECRET_KEY are still accepted as a fallback for older setups. +S3_ACCESS_KEY_ID= +S3_SECRET_ACCESS_KEY= + +# ============================================================================= +# WEBHOOK +# ============================================================================= +WEBHOOK_TIMEOUT=10000 # Timeout in milliseconds +WEBHOOK_MAX_RETRIES=3 # Number of retry attempts +WEBHOOK_RETRY_DELAY=5000 # Delay between retries in ms +# Days to keep webhook delivery-failure records before pruning them (default 90; set <= 0 to disable). +# WEBHOOK_FAILURE_RETENTION_DAYS=90 +# Block outbound WEBHOOK deliveries to internal/reserved addresses (SSRF +# protection). ON by default; set to "false" only on closed networks. When on, +# webhook URLs resolving to loopback/private/link-local/metadata ranges are +# refused (at registration AND delivery) and redirects are not followed. +# (Server-side media-by-URL fetches are ALWAYS SSRF-guarded, regardless of this flag.) +WEBHOOK_SSRF_PROTECT=true + +# Expose the FULL sender `contact` field set (id, number, shortName, business flags, isBlocked, +# labels, …) on the message.received webhook + websocket payload. OFF by default — the payload keeps +# the minimal { name, pushName }. Opt in only if your consumer needs the richer data; all fields are +# read from the already-cached contact (no extra WhatsApp API calls). +# WEBHOOK_CONTACT_DETAILS=true +# Comma-separated hosts/IPs allowed to bypass SSRF protection for BOTH webhooks +# and media — escape-hatch for trusted internal targets (e.g. a localhost media +# store or a sidecar webhook receiver). +# SSRF_ALLOWED_HOSTS=localhost,minio +# Server-side media size/time limits: +# MEDIA_DOWNLOAD_ENABLED=true # Set to false to skip downloading inbound media entirely +# # (no decryption, no memory allocation, no storage). +# # NOTE: disabling also makes hasMedia report false and removes the +# # media field from webhooks and the dashboard. +# STORE_EPHEMERAL_MESSAGES=true # Set to false to skip persisting and dispatching incoming +# # WhatsApp disappearing messages (ephemeralDuration > 0). +# # Default: true (backward compatible — store everything). +# MEDIA_DOWNLOAD_MAX_BYTES=52428800 # cap remote-URL sends, inbound media, AND outbound base64 sends (default 50 MiB; oversized inbound media is dropped, message kept; oversized base64 is rejected with 400) +# MEDIA_DOWNLOAD_TIMEOUT_MS=30000 # abort a slow media download (default 30s) +# Storage import/export limits (ADMIN /infra/storage/* endpoints): +# STORAGE_IMPORT_MAX_BYTES=209715200 # per-entry cap for a tar.gz import; aborts on overflow (default 200 MiB) +# STORAGE_IMPORT_MAX_ENTRIES=100000 # max entries in an import archive; aborts beyond this (default 100000) +# STORAGE_EXPORT_TTL_MS=3600000 # auto-delete an export archive after this long (default 1h) + +# ============================================================================= +# RATE LIMITING (all TTLs are in MILLISECONDS) +# ============================================================================= +# The "medium" tier is the one enforced on the API; short/long are optional extra tiers. +RATE_LIMIT_MEDIUM_TTL=60000 # window in ms (default 60000 = 60s) +RATE_LIMIT_MEDIUM_LIMIT=100 # max requests per window +# RATE_LIMIT_SHORT_TTL=1000 # 1s burst window (default) +# RATE_LIMIT_SHORT_LIMIT=10 +# RATE_LIMIT_LONG_TTL=3600000 # 1h window (default) +# RATE_LIMIT_LONG_LIMIT=1000 + +# Per-instance fairness cap on the Integration Fabric ingress route (POST /api/ingress/:pluginId/:instanceId/*). +# Providers deliver every tenant's webhooks from one shared egress IP, so this is keyed on +# (pluginId, instanceId) instead of IP — a noisy tenant gets 429'd without throttling its neighbors. +# Independent of, and in addition to, the IP-keyed tiers above. +# INGRESS_INSTANCE_LIMIT=120 # max requests per instance per window (default 120) +# INGRESS_INSTANCE_TTL=60000 # window in ms (default 60000 = 60s) + +# ============================================================================= +# MCP (Model Context Protocol) — Agent / AI-assistant tool server +# ============================================================================= +# Off by default. When enabled, mounts a stateless Streamable-HTTP MCP server +# at POST /mcp on the same port. See docs/24-mcp-integration.md for details. +# MCP_ENABLED=true + +# Mount read-only tools only (no sends, no mutations). Recommended for observer agents. +# MCP_READONLY=true + +# Per-key sliding-window rate limit for tool calls. +# Any blank/non-positive/non-numeric value falls back to the default. +# MCP_RATE_LIMIT_MAX=60 # max tool calls per key per window (default 60) +# MCP_RATE_LIMIT_WINDOW_MS=60000 # window size in ms (default 60000 = 1 min) +# Pre-auth, per-IP sliding-window throttle for the /mcp mount (gates invalid-key requests before +# key validation, since the raw mount bypasses the global throttler). Same fallback rules. +# MCP_IP_RATE_LIMIT_MAX=120 # max requests per IP per window (default 120) +# MCP_IP_RATE_LIMIT_WINDOW_MS=60000 # window size in ms (default 60000 = 1 min) + +# ============================================================================= +# GLOBAL MESSAGE SEARCH +# ============================================================================= +# Global message search (optional). Default ON, using the built-in DB full-text +# provider (Postgres tsvector/GIN, SQLite FTS5) — zero external services. +SEARCH_ENABLED=true # set to false to disable the /search route + module entirely +SEARCH_PROVIDER=auto # auto | builtin-fts | none (plugin ids selectable once Spec 2 lands) +SEARCH_LIMIT_MAX=100 # hard cap on the `limit` query param + +# ============================================================================= +# PLUGINS +# ============================================================================= +PLUGINS_ENABLED=true # Enable plugin system +PLUGINS_DIR=./data/plugins # Plugin directory + +# ============================================================================= +# SECURITY +# ============================================================================= +# Master API key (leave empty to disable, or set to secure value) +API_MASTER_KEY= + +# First-boot default admin key. By default a cryptographically random key is +# generated (printed in the startup banner / written to data/.api-key). Set this +# to true ONLY for local development to seed the well-known, insecure +# `dev-admin-key` instead. Ignored when API_MASTER_KEY is set. +# ALLOW_DEV_API_KEY=true + +# Optional server-side pepper for API-key hashing (HMAC-SHA256 instead of plain SHA-256). +# Recommended in production. Note: setting or changing it invalidates all existing key hashes, +# so re-issue keys after enabling. Leave unset to keep the current (unpeppered) behaviour. +# API_KEY_PEPPER= + +# ============================================================================= +# DEVELOPER SETTINGS +# ============================================================================= +ENABLE_SWAGGER=true # Enable API documentation at /api/docs (set false to disable on exposed deployments) +BODY_SIZE_LIMIT=25mb # Max request body size (base64 media sends ride in the JSON body) +# CSP_UPGRADE_INSECURE_REQUESTS=false # Set false for an HTTP-only deployment on a trusted private network + # (default: on in production). Stops the browser upgrading the dashboard to https. diff --git a/.env.minimal b/.env.minimal new file mode 100644 index 0000000..934e978 --- /dev/null +++ b/.env.minimal @@ -0,0 +1,46 @@ +# =========================================== +# OpenWA - Environment Configuration (Minimal) +# =========================================== +# This is a minimal configuration for development +# and single-session personal bots using SQLite. + +# Auto-start previously authenticated sessions on server boot +AUTO_START_SESSIONS=false + +# Server +PORT=2785 +NODE_ENV=development + +# Database (SQLite - no external service required) +DATABASE_TYPE=sqlite +DATABASE_NAME=./data/openwa.sqlite +DATABASE_SYNCHRONIZE=true +DATABASE_LOGGING=false + +# WhatsApp Engine +ENGINE_TYPE=whatsapp-web.js +SESSION_DATA_PATH=./data/sessions +PUPPETEER_HEADLESS=true +PUPPETEER_ARGS=--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage + +# Webhook +WEBHOOK_TIMEOUT=10000 +WEBHOOK_MAX_RETRIES=3 +WEBHOOK_RETRY_DELAY=5000 + +# Storage (Local filesystem) +STORAGE_TYPE=local +STORAGE_LOCAL_PATH=./data/media + +# Redis & Queue (disabled for minimal setup) +REDIS_ENABLED=false +REDIS_BUILTIN=false +QUEUE_ENABLED=false +CACHE_ENABLED=false + +# Built-in Docker services (all disabled for minimal setup) +POSTGRES_BUILTIN=false +MINIO_BUILTIN=false + +# API Security (optional for development) +# API_MASTER_KEY=your-master-api-key-here diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8663443 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Force LF line endings for files that must run inside Linux containers +*.sh text eol=lf +Dockerfile* text eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..22d07aa --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,88 @@ +name: Bug report +description: Report a problem with OpenWA +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a bug! Please fill in the details below — the + version, deployment, and logs make a huge difference for triage. + + ⚠️ **Do not report security vulnerabilities here.** See + [SECURITY.md](https://github.com/rmyndharis/OpenWA/blob/main/SECURITY.md). + - type: checkboxes + attributes: + label: Pre-flight + options: + - label: I searched existing issues and this isn't a duplicate + required: true + - label: I'm on the latest released version (or noted my version below) + required: true + - type: input + id: version + attributes: + label: OpenWA version + description: e.g. 0.2.1 (shown on the dashboard Login screen) or a commit SHA + placeholder: "0.2.1" + validations: + required: true + - type: dropdown + id: deployment + attributes: + label: Deployment + options: + - Docker Compose + - Docker (manual run) + - Bare metal (npm) + - Other (describe below) + validations: + required: true + - type: dropdown + id: database + attributes: + label: Database + options: + - SQLite (default) + - PostgreSQL + validations: + required: true + - type: input + id: engine + attributes: + label: WhatsApp engine + description: The default is whatsapp-web.js; note the version if you changed it. + value: "whatsapp-web.js" + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear description of the bug and its impact. + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Steps to reproduce + description: Exact steps (including the API call or dashboard action) to reproduce. + placeholder: | + 1. ... + 2. ... + 3. ... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + - type: textarea + id: logs + attributes: + label: Relevant logs + description: Server/container logs around the failure (redact API keys & secrets). + render: shell + - type: textarea + id: context + attributes: + label: Environment / additional context + description: OS, Node version, reverse proxy, anything else relevant. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..e662971 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: 📚 Documentation + url: https://github.com/rmyndharis/OpenWA/tree/main/docs + about: Setup, API specification, and operational guides. + - name: 🔒 Report a security vulnerability + url: https://github.com/rmyndharis/OpenWA/security/advisories/new + about: Please report vulnerabilities privately — not as a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..573c33f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,41 @@ +name: Feature request +description: Suggest an idea or improvement for OpenWA +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for the suggestion! Please describe the problem first — that helps us find + the best solution, which isn't always the one initially imagined. + - type: checkboxes + attributes: + label: Pre-flight + options: + - label: I searched existing issues and this isn't already requested + required: true + - type: textarea + id: problem + attributes: + label: Problem / motivation + description: What are you trying to do, and what's getting in the way today? + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed solution + description: What would you like to see? API shape, dashboard behavior, config, etc. + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + - type: checkboxes + id: scope + attributes: + label: Scope + description: A quick reality check on engine capabilities. + options: + - label: >- + I understand some features are limited by the underlying WhatsApp engine + (e.g. interactive Buttons/List messages are not supported on whatsapp-web.js). diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c3c4b67 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,61 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 5 + groups: + minor-and-patch: + update-types: + - minor + - patch + major: + update-types: + - major + labels: + - dependencies + ignore: + # Ignore major bumps for packages that need manual migration + # Uncomment specific packages below if you want to skip them entirely + # - dependency-name: "typescript" + # update-types: ["version-update:semver-major"] + + - package-ecosystem: npm + directory: /dashboard + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 3 + groups: + minor-and-patch: + update-types: + - minor + - patch + major: + update-types: + - major + labels: + - dependencies + - dashboard + + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + labels: + - ci + + # Base images (Dockerfile) + the compose stack's pinned images (socket-proxy, postgres, + # redis, minio). Keeps the security-critical docker-socket-proxy pin and the datastore + # images moving instead of drifting on a stale tag. + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + day: monday + open-pull-requests-limit: 3 + labels: + - dependencies + - docker diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..d13be4f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +## Description +Brief description of changes + +## Type of Change +- [ ] Bug fix +- [ ] New feature +- [ ] Breaking change +- [ ] Documentation update + +## Checklist +- [ ] Tests added/updated +- [ ] Documentation updated +- [ ] Lint passes +- [ ] Self-reviewed + +## Screenshots (if applicable) + +## Related Issues +Closes # diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3737406 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,240 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +env: + NODE_VERSION: '22' + DOCKER_PLATFORMS: linux/amd64,linux/arm64 + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Security audit + run: npm audit --audit-level=critical + + - name: Run ESLint + run: npm run lint + + # `nest build` uses tsconfig.build.json which excludes **/*spec.ts, and ts-jest/eslint don't + # full-program type-check specs — so without this gate, type errors in spec files are invisible + # to CI. tsconfig.json already includes both `src` and `test`, so this is the cheapest full-program + # check covering specs (no separate tsconfig.spec.json needed). + - name: Type-check full program (including specs) + run: npx tsc --noEmit -p tsconfig.json + + - name: Check formatting + run: npm run format -- --check + + - name: Check version consistency (docs track package.json) + run: npm run check:versions + + # The committed openapi.json is the machine-readable API contract. Fail if a controller/DTO + # change landed without regenerating the snapshot (npm run openapi:export). The generator + # bootstraps the app hermetically (in-memory/temp SQLite, no listen), so this is a static check. + - name: Check OpenAPI snapshot is up to date + run: npm run openapi:check + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test -- --coverage + + - name: Run e2e smoke tests + run: npm run test:e2e + + - name: Upload coverage + uses: codecov/codecov-action@v7 + if: always() + with: + files: ./coverage/lcov.info + fail_ci_if_error: false + + test-postgres: + name: Test (PostgreSQL migrations) + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: openwa + POSTGRES_PASSWORD: openwa + POSTGRES_DB: openwa + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U openwa" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build (compiles the data migrations to dist/) + run: npm run build + + # Applies the full data-migration chain to a real Postgres and asserts every generated-uuid PK + # has a DB DEFAULT — the dialect gap SQLite-only tests can't see. + - name: Migrate + uuid-default smoke against PostgreSQL + run: npm run test:pg-smoke + env: + DATABASE_TYPE: postgres + DATABASE_HOST: localhost + DATABASE_PORT: '5432' + DATABASE_USERNAME: openwa + DATABASE_PASSWORD: openwa + DATABASE_NAME: openwa + + # Runtime-proves BuiltInFtsProvider on Postgres (websearch_to_tsquery + ts_headline against the + # STORED body_ts tsvector). The spec self-skips unless DATABASE_TYPE=postgres, so it is a no-op + # in the default test job and only executes here against the postgres:16 service. + - name: Postgres FTS provider spec + run: npx jest src/database/migrations/__tests__/1782400000000-AddMessagesFts.pg.spec.ts + env: + DATABASE_TYPE: postgres + DATABASE_HOST: localhost + DATABASE_PORT: '5432' + DATABASE_USERNAME: openwa + DATABASE_PASSWORD: openwa + DATABASE_NAME: openwa + + dashboard: + name: Dashboard + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: dashboard/package-lock.json + + - name: Install dependencies + run: cd dashboard && npm ci + + - name: Run ESLint + run: cd dashboard && npm run lint + + - name: Check i18n parity + run: cd dashboard && npm run i18n:check + + - name: Build dashboard + run: cd dashboard && npm run build + + - name: Run dashboard unit tests + run: cd dashboard && npm run test:unit + + build: + name: Build + runs-on: ubuntu-latest + needs: [lint, test, dashboard] + steps: + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Upload build artifacts + uses: actions/upload-artifact@v7 + with: + name: dist + path: dist/ + retention-days: 7 + + docker: + name: Docker Build + runs-on: ubuntu-latest + needs: [build, test-postgres] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v7 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=ref,event=branch + type=sha,prefix= + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + + - name: Build and push + uses: docker/build-push-action@v7 + with: + context: . + push: true + platforms: ${{ env.DOCKER_PLATFORMS }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # provenance is generated by default (build-push-action v7); pin it explicitly and opt into + # an SBOM attestation so each published image carries an in-toto SLSA provenance + SBOM pair, + # verifiable via `docker buildx imagetools inspect`. + provenance: true + sbom: true + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/java-sdk-release.yml b/.github/workflows/java-sdk-release.yml new file mode 100644 index 0000000..8691bf9 --- /dev/null +++ b/.github/workflows/java-sdk-release.yml @@ -0,0 +1,59 @@ +name: Java SDK Release + +# Publishes com.rmyndharis:openwa to Maven Central from sdk/java. Cleanly no-ops +# until the one-time setup is done (see sdk/java/README.md -> RELEASING): +# - MAVEN_CENTRAL_USERNAME / MAVEN_CENTRAL_PASSWORD (Sonatype Central user token halves) +# - GPG_PRIVATE_KEY (ASCII-armored signing key) +# - GPG_PASSPHRASE +# The SDK version is the pom on a dedicated tag (e.g. java-sdk-v0.1.0), +# NOT the monorepo v* app tags. + +on: + push: + tags: ['java-sdk-v*'] + workflow_dispatch: {} + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + defaults: + run: + working-directory: sdk/java + steps: + - uses: actions/checkout@v7 + + - name: Guard — skip if publish credentials are absent + id: guard + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + run: | + if [ -z "${MAVEN_CENTRAL_USERNAME:-}" ]; then + echo "::notice::MAVEN_CENTRAL_USERNAME not set — skipping publish. See sdk/java/README.md -> RELEASING." + echo "publish=false" >> "$GITHUB_OUTPUT" + else + echo "publish=true" >> "$GITHUB_OUTPUT" + fi + + - name: Set up JDK + signing key + if: steps.guard.outputs.publish == 'true' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: maven + server-id: central + server-username: MAVEN_CENTRAL_USERNAME + server-password: MAVEN_CENTRAL_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + + - name: Deploy to Maven Central + if: steps.guard.outputs.publish == 'true' + env: + MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} + MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: mvn -B -Prelease deploy -DskipTests diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..96411a4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,350 @@ +name: Release + +on: + push: + tags: ['v*'] + +env: + NODE_VERSION: '22' + DOCKER_PLATFORMS: linux/amd64,linux/arm64 + +permissions: + contents: write + packages: write + +jobs: + # ──── Release Gate (CI-strength) ────────────────────────────────── + # Mirror the CI workflow's gate jobs so a tag can never publish an image or GitHub Release that + # lint, the spec type-check, unit + e2e tests, the Postgres migration smoke, or the dashboard + # build/lint would have rejected. Each job below matches its CI counterpart; only the tag-match + # guard and the publish jobs (docker, release) are release-specific. + lint: + name: Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + # Fail fast (before tests, image build, or the GitHub Release) if the release is inconsistent: + # the tag must match package.json, and the docs/CHANGELOG version guard must pass. This stops a + # mis-tagged or under-documented release from ever publishing an image or a GitHub Release. + - name: Verify tag matches package.json version + env: + TAG: ${{ github.ref_name }} + run: | + PKG_VERSION="$(node -p "require('./package.json').version")" + TAG_VERSION="${TAG#v}" + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "::error::Tag '$TAG' (version '$TAG_VERSION') does not match package.json version '$PKG_VERSION'. Bump package.json or fix the tag before releasing." + exit 1 + fi + echo "Tag $TAG matches package.json $PKG_VERSION" + + - name: Security audit + run: npm audit --audit-level=critical + + - name: Run ESLint + run: npm run lint + + # Same gap as CI: `nest build` excludes specs, so a spec-only type regression slips past every + # existing release gate. tsconfig.json includes both src and test, so this is the full-program + # check that also covers spec files. + - name: Type-check full program (including specs) + run: npx tsc --noEmit -p tsconfig.json + + - name: Check formatting + run: npm run format -- --check + + - name: Check version consistency (docs track package.json) + run: npm run check:versions + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Run unit tests + run: npm test -- --coverage + + - name: Run e2e smoke tests + run: npm run test:e2e + + test-postgres: + name: Test (PostgreSQL migrations) + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: openwa + POSTGRES_PASSWORD: openwa + POSTGRES_DB: openwa + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U openwa" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build (compiles the data migrations to dist/) + run: npm run build + + # Applies the full data-migration chain to a real Postgres and asserts every generated-uuid PK + # has a DB DEFAULT — the dialect gap SQLite-only tests can't see. + - name: Migrate + uuid-default smoke against PostgreSQL + run: npm run test:pg-smoke + env: + DATABASE_TYPE: postgres + DATABASE_HOST: localhost + DATABASE_PORT: '5432' + DATABASE_USERNAME: openwa + DATABASE_PASSWORD: openwa + DATABASE_NAME: openwa + + # Runtime-proves BuiltInFtsProvider on Postgres (websearch_to_tsquery + ts_headline against the + # STORED body_ts tsvector). The spec self-skips unless DATABASE_TYPE=postgres, so it is a no-op + # in the default test job and only executes here against the postgres:16 service. + - name: Postgres FTS provider spec + run: npx jest src/database/migrations/__tests__/1782400000000-AddMessagesFts.pg.spec.ts + env: + DATABASE_TYPE: postgres + DATABASE_HOST: localhost + DATABASE_PORT: '5432' + DATABASE_USERNAME: openwa + DATABASE_PASSWORD: openwa + DATABASE_NAME: openwa + + dashboard: + name: Dashboard + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: dashboard/package-lock.json + + - name: Install dependencies + run: cd dashboard && npm ci + + - name: Run ESLint + run: cd dashboard && npm run lint + + - name: Check i18n parity + run: cd dashboard && npm run i18n:check + + - name: Build dashboard + run: cd dashboard && npm run build + + - name: Run dashboard unit tests + run: cd dashboard && npm run test:unit + + build: + name: Build + runs-on: ubuntu-latest + needs: [lint, test, dashboard] + steps: + - uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + # ──── Create GitHub Release ─────────────────────────────────────── + release: + name: GitHub Release + runs-on: ubuntu-latest + # Gate the public Release on every CI-strength job AND a successfully built+pushed image that + # also boots, so a tag never produces a GitHub Release without passing the same gate CI enforces, + # a matching container image, and a runtime boot check on both architectures (boot-smoke runs the + # published image on amd64 + arm64). Trades a few minutes of buildx/boot latency for release + # safety. + needs: [lint, test, test-postgres, dashboard, build, docker, boot-smoke] + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Extract version from tag + id: version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Extract release notes from CHANGELOG + id: changelog + run: | + # Extract the section for this version from CHANGELOG.md + VERSION="${{ steps.version.outputs.VERSION }}" + NOTES=$(awk "/^## \[${VERSION}\]/{flag=1; next} /^## \[/{flag=0} flag" CHANGELOG.md) + if [ -z "$NOTES" ]; then + NOTES="Release v${VERSION}" + fi + # Use EOF delimiter for multi-line output + echo "NOTES<> $GITHUB_OUTPUT + echo "$NOTES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v3 + with: + # Author the release as a real user (the PAT owner) instead of github-actions[bot]. + # Add a repo secret RELEASE_PAT — a fine-grained PAT scoped to this repo with + # "Contents: read and write". Falls back to GITHUB_TOKEN (bot-authored) when the secret + # is absent, so the job never breaks before the token is configured. + token: ${{ secrets.RELEASE_PAT || secrets.GITHUB_TOKEN }} + tag_name: ${{ github.ref_name }} + name: v${{ steps.version.outputs.VERSION }} + body: ${{ steps.changelog.outputs.NOTES }} + draft: false + prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') || contains(github.ref_name, '-alpha') }} + + # ──── Docker Image with Version Tag ─────────────────────────────── + docker: + name: Docker Release + runs-on: ubuntu-latest + needs: [lint, test, test-postgres, dashboard, build] + steps: + - uses: actions/checkout@v7 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }} + + - name: Build and push + uses: docker/build-push-action@v7 + with: + context: . + push: true + platforms: ${{ env.DOCKER_PLATFORMS }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # provenance is generated by default (build-push-action v7); pin it explicitly and opt into + # an SBOM attestation so each release image carries an in-toto SLSA provenance + SBOM pair, + # verifiable via `docker buildx imagetools inspect`. + provenance: true + sbom: true + cache-from: type=gha + cache-to: type=gha,mode=max + + # ──── Boot Smoke (amd64 + arm64) ────────────────────────────────── + # The docker job proves the image BUILDS for both architectures; this proves the just-pushed image + # actually BOOTS and serves the liveness endpoint on both. A boot regression that only surfaces at + # runtime on one architecture (e.g. a native-dep/Chromium SIGTRAP on arm64 that a clean build still + # produces) cannot reach a public Release. Runs the published image — not a rebuild — against the + # dependency-free /api/health/live (NOT /ready, which needs a DB and would conflate boot failure + # with database setup). Both architectures are tried even if the first fails, so logs name both. + boot-smoke: + name: Boot smoke (amd64 + arm64) + runs-on: ubuntu-latest + needs: [docker] + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve published image ref + id: image + # metadata-action publishes the {{version}} tag (e.g. 0.8.17) from the v-prefixed git tag, so + # strip the leading "v" to match it. ${REPO,,} lowercases github.repository — GHCR normalizes + # the repo name to lowercase on push, and `docker run` rejects a mixed-case reference, so the + # pull target must be lowercase too (this repo is rmyndharis/OpenWA). + env: + REPO: ${{ github.repository }} + run: echo "ref=ghcr.io/${REPO,,}:${GITHUB_REF#refs/tags/v}" >> "$GITHUB_OUTPUT" + + - name: Boot + liveness check on each architecture + env: + IMAGE: ${{ steps.image.outputs.ref }} + run: | + exit_code=0 + for platform in linux/amd64 linux/arm64; do + echo "::group::boot-smoke $platform" + docker rm -f openwa-smoke >/dev/null 2>&1 || true + if ! docker run --rm --platform "$platform" -d -p 2785:2785 --name openwa-smoke "$IMAGE" >/dev/null; then + echo "FAIL: $platform — could not start container (image pull/run error)" + exit_code=1 + echo "::endgroup::" + continue + fi + ok="" + for i in $(seq 1 40); do + if curl -sf http://127.0.0.1:2785/api/health/live >/dev/null 2>&1; then ok=1; break; fi + sleep 5 + done + if [ -n "$ok" ]; then + echo "OK: $platform — /api/health/live returned 200" + else + echo "FAIL: $platform — /api/health/live did not return 200 within ~200s; container logs follow:" + docker logs openwa-smoke 2>&1 | tail -60 || true + exit_code=1 + fi + docker stop openwa-smoke >/dev/null 2>&1 || true + docker rm -f openwa-smoke >/dev/null 2>&1 || true + echo "::endgroup::" + done + exit "$exit_code" diff --git a/.github/workflows/sdk-ci.yml b/.github/workflows/sdk-ci.yml new file mode 100644 index 0000000..c364e17 --- /dev/null +++ b/.github/workflows/sdk-ci.yml @@ -0,0 +1,75 @@ +name: SDK CI + +# Runs the JavaScript/Python/PHP client SDK suites. Path-filtered to SDK sources AND the server +# contract surfaces the SDK types mirror (DTOs + the engine interface), so a backend DTO change also +# re-runs the SDK suites and surfaces contract drift. The JavaScript job also builds and runs the +# dual-format packaging smoke test (require() + import() both consumable). +on: + push: + branches: [main, develop] + paths: ['sdk/**', '.github/workflows/sdk-ci.yml', 'src/**/dto/**', 'src/**/*.controller.ts', 'src/**/*.service.ts', 'src/engine/interfaces/whatsapp-engine.interface.ts'] + pull_request: + branches: [main, develop] + paths: ['sdk/**', '.github/workflows/sdk-ci.yml', 'src/**/dto/**', 'src/**/*.controller.ts', 'src/**/*.service.ts', 'src/engine/interfaces/whatsapp-engine.interface.ts'] + +jobs: + javascript: + name: JavaScript SDK + runs-on: ubuntu-latest + defaults: + run: + working-directory: sdk/javascript + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v6 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: sdk/javascript/package-lock.json + - run: npm ci + - run: npm test + - run: npm run build + - run: npm run smoke # dual CJS+ESM consumable — must run after build + + python: + name: Python SDK + runs-on: ubuntu-latest + defaults: + run: + working-directory: sdk/python + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - run: pip install -e '.[dev]' + - run: pytest + + php: + name: PHP SDK + runs-on: ubuntu-latest + defaults: + run: + working-directory: sdk/php + steps: + - uses: actions/checkout@v7 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + - run: composer install --no-interaction --no-progress + - run: ./vendor/bin/phpunit + + java: + name: Java SDK + runs-on: ubuntu-latest + defaults: + run: + working-directory: sdk/java + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + cache: maven + - run: mvn -B verify diff --git a/.github/workflows/split-php-sdk.yml b/.github/workflows/split-php-sdk.yml new file mode 100644 index 0000000..01e03f3 --- /dev/null +++ b/.github/workflows/split-php-sdk.yml @@ -0,0 +1,73 @@ +name: Split PHP SDK + +# Mirrors the PHP SDK (sdk/php) into the standalone repo rmyndharis/openwa-php, +# which is what Packagist installs (Packagist needs composer.json at a repo root +# and does not support subdirectories). Uses a dependency-free `git subtree +# split` + force-push — no third-party action. +# +# IMPORTANT: this only syncs the `main` branch (-> Packagist `dev-main`). The +# monorepo's `v*` tags are the APP version, NOT the SDK version, so they are +# deliberately NOT propagated. To cut a versioned SDK release, tag the mirror +# repo directly (see sdk/php — RELEASING below), e.g. `git tag 0.1.0`. +# +# One-time setup: +# 1. Create an empty GitHub repo: rmyndharis/openwa-php +# 2. Create a PAT (fine-grained, repo = openwa-php, Contents: read & write) +# 3. Add it to THIS repo as the secret PHP_SDK_SPLIT_TOKEN + +on: + push: + branches: [main] + paths: ['sdk/php/**'] + workflow_dispatch: {} + +permissions: + contents: read + +concurrency: + group: split-php-sdk + cancel-in-progress: false + +jobs: + test: + name: PHP SDK tests + runs-on: ubuntu-latest + defaults: + run: + working-directory: sdk/php + steps: + - uses: actions/checkout@v7 + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + - run: composer install --no-interaction --no-progress + - run: ./vendor/bin/phpunit + + split: + name: Mirror sdk/php -> rmyndharis/openwa-php + # Never mirror an untested (red) SDK to Packagist dev-main. + needs: [test] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 # full history is required for `git subtree split` + persist-credentials: false # stop the GITHUB_TOKEN extraheader from shadowing the PAT in the push URL + + - name: Split and force-push to the mirror repo + env: + SPLIT_TOKEN: ${{ secrets.PHP_SDK_SPLIT_TOKEN }} + run: | + set -euo pipefail + if [ -z "${SPLIT_TOKEN:-}" ]; then + echo "::notice::PHP_SDK_SPLIT_TOKEN is not set — skipping. Create rmyndharis/openwa-php and add the secret to enable the mirror." + exit 0 + fi + git config user.name "rmyndharis" + git config user.email "yudhi@rmyndharis.com" + SPLIT_SHA="$(git subtree split --prefix=sdk/php HEAD)" + echo "Split SHA: $SPLIT_SHA" + git push --force \ + "https://x-access-token:${SPLIT_TOKEN}@github.com/rmyndharis/openwa-php.git" \ + "${SPLIT_SHA}:refs/heads/main" + echo "Mirrored sdk/php -> rmyndharis/openwa-php@main" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d815c7d --- /dev/null +++ b/.gitignore @@ -0,0 +1,80 @@ +# Dependencies +node_modules/ +node_modules +.pnp +.pnp.js + +# Build output +dist/ +build/ +.next/ +out/ + +# Testing +coverage/ + +# Environment +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +logs/ +*.log + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*.sublime-workspace +*.sublime-project + +# Data and storage +data/ +media/ +!src/**/media/ +uploads/ +.wwebjs_auth/ +.wwebjs_cache/ + +# Database +*.db +*.sqlite +*.sqlite3 + +# Docker +.docker/ + +# Misc +*.bak +*.tmp +*.temp + +# Git worktrees +.worktrees/ + +# AI Agents +.playwright-mcp/ +.remember/ +.agent/ +.claude/ +docs/plans/ +docs/superpowers/ +CLAUDE.md +_docs/ +.superpowers/ \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1a3466e --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "trailingComma": "all", + "printWidth": 120, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "auto" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0cb135f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2336 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + + +## [0.8.17] - 2026-07-13 + +### Added + +- **`AuditAction` emit-coverage gate.** A structural test now fails the build when an `AuditAction` + enum value is neither emitted at a real call site nor registered (with a reason) in a new + intentionally-unemitted registry. A declared audit event can no longer silently exist with no + emission site, and a new action cannot land without either wiring it or documenting why it is held + back. The registry is also checked for stale entries (an action that is in fact emitted) and empty + reasons, so it cannot decay into a dumping ground. + +- **Operator-tunable HTTP server timeouts.** The gateway now pins `requestTimeout`, + `headersTimeout`, and `keepAliveTimeout` on its HTTP server explicitly (previously Node's + implicit defaults), exposed via `REQUEST_TIMEOUT_MS` / `HEADERS_TIMEOUT_MS` / + `KEEPALIVE_TIMEOUT_MS` and logged at boot. Defaults match Node 22 (300s / 65s / 5s); + `headersTimeout` is normalized to stay above `keepAliveTimeout` (Node requires it), and the three + are validated as positive integers at boot. + +- **Committed OpenAPI snapshot + CI sync gate.** The gateway's OpenAPI document is now committed as + `openapi.json` (generated from the NestJS Swagger decorators via `npm run openapi:export`) and a CI + check (`npm run openapi:check`) fails when a controller/DTO change lands without regenerating it, so + the machine-readable contract can never silently drift from the code. SDK/API consumers now have a + versioned artifact at the repo root. + +- **Pre-release boot smoke on amd64 + arm64.** Cutting a release tag now runs the just-published + image on both `linux/amd64` and `linux/arm64` (via QEMU) and polls the dependency-free + `/api/health/live` endpoint before the GitHub Release is created — so a runtime-only boot regression + on one architecture (a clean build can still produce a native-dep/Chromium SIGTRAP on arm64) cannot + ship under a release. The Release job waits on the new boot-smoke job. + +- **SBOM attestation on published images.** Each image built by CI and on release now carries an + in-toto SBOM attestation alongside the SLSA provenance that `docker/build-push-action` already + generates by default. Both are verifiable with + `docker buildx imagetools inspect ghcr.io/rmyndharis/openwa:`. Provenance is now also pinned + explicitly (`provenance: true`) so the attestation pair is self-documenting rather than reliant on + the action default. + +- **HTTP RED metrics.** The `/api/metrics` endpoint now exposes + `http_requests_total{method,route,status}` and an `http_request_duration_seconds` histogram (per + route), recorded by a global interceptor. Route labels use the Express route pattern (bounded — + `/api/sessions/:id`, not the raw URL) with a `Controller#handler` fallback, and `/api/health` + + `/api/metrics` are not counted. Conventional unprefixed names so a generic RED dashboard or a 5xx + error-rate alert matches them directly. + +- **Request correlation ids (`X-Request-ID`).** Every inbound request now carries an id that + propagates through the whole request via AsyncLocalStorage, so each JSON log line and each + audit-log metadata blob stamps it — a request can be traced end-to-end. A valid client-supplied + `X-Request-ID` (alphanumeric + dash, ≤128 chars) is echoed; anything else (including a CRLF + header-injection attempt) is replaced with a generated UUID. The id is also set on the response. + +- **Engine capability matrix + drift gate.** A committed, source-verified matrix + (`src/engine/engine-capability-matrix.ts`) records, for every `IWhatsAppEngine` method on each + engine (whatsapp-web.js default, Baileys), whether it is `supported` or `not-available` — and for + the not-available ones, the root cause: `adapter-gap` (the underlying library supports it, OpenWA + just hasn't wired it — fixable) vs `library-limitation` (no first-class library API), with the + cited library symbol as evidence. A drift gate fails when a method's throw-availability changes. + `docs/engine-capability-matrix.md` inventories the unwired adapter-gaps as a prioritized capability + backlog. + +- **Delete-for-me on the Baileys engine.** `deleteMessage(…, forEveryone=false)` now performs a + delete-for-me via Baileys' `chatModify({ deleteForMe })` instead of returning 501. Revoke-for- + everyone (`forEveryone=true`) was already wired; this completes `deleteMessage` on the Baileys + engine for the most common delete mode. + +- **Status posts on the whatsapp-web.js engine.** `postTextStatus`, `postImageStatus`, and + `postVideoStatus` now work on whatsapp-web.js (the default engine) — they route through + `sendMessage('status@broadcast', …)` (text styling via `extra: { backgroundColor, fontStyle }`; + media via `MessageMedia` + `caption`). Previously these returned 501 on the default engine despite + the library supporting them; the stale "blocked upstream, #455" guard is removed (#455 is a closed + feature request, and whatsapp-web.js 1.34.7 ships a real status-send path). Caveat: the library has + no status-recipient arg, so `StatusPostOptions.recipients` is not honored on this engine (it + broadcasts to the account's status-privacy audience; a one-time warning is logged). The Baileys + engine continues to honor `recipients`. + +- **Chat labels on the Baileys engine.** `addLabelToChat` and `removeLabelFromChat` now work on + the Baileys engine — 1:1 to `sock.addChatLabel(chatId, labelId)` / `sock.removeChatLabel(chatId, + labelId)` instead of returning 501. WhatsApp-Business-only (rejects on personal accounts). Label + *listing* (`getLabels` / `getLabelById` / `getChatLabels`) remains unavailable on Baileys (no + first-class library API — see `docs/engine-capability-matrix.md`). + +- **Status delete on the whatsapp-web.js engine.** `deleteStatus(statusId)` now works on + whatsapp-web.js via `client.revokeStatusMessage(statusId)` instead of returning 501 — completing + the status lifecycle (post + delete) on the default engine. Own-status only (the library revokes + the caller's own status posts). + +- **Read contact stories on the whatsapp-web.js engine.** `getContactStatuses()` and + `getContactStatus(contactId)` now return contact "stories" (24h status posts) via + whatsapp-web.js `getBroadcasts()` / `getBroadcastById()` flattened to `Status[]` (contact via + `broadcast.getContact()`, type from `MessageTypes`, 24h TTL) instead of stubbing to `[]`. The + Baileys engine still cannot read stories — `fetchStatus` returns the *about* text, not stories + (documented as a library limitation). + +- **Channel lookup / subscribe / unsubscribe on the Baileys engine.** `getChannelById(id)`, + `subscribeToChannel(inviteCode)`, and `unsubscribeFromChannel(id)` now work via Baileys + `newsletterMetadata` (mapped to `Channel` with optional fields), `newsletterFollow` (subscribe, + resolving invite→jid first), and `newsletterUnfollow` (unsubscribe, 1:1). `getChannelById` on + Baileys resolves ANY channel by jid (richer than the whatsapp-web.js subscribed-list lookup). + `getChannelMessages` remains unsupported — `newsletterFetchMessages` returns a raw BinaryNode with + no library parser, so it stays a documented gap rather than an unverified walk. + +- **Bounded webhook fan-out.** An event matching N webhooks now delivers at most + `WEBHOOK_DISPATCH_CONCURRENCY` (default 16) concurrently instead of opening N outbound sockets at + once; the rest queue and run as slots free. Per-webhook isolation (`Promise.allSettled`) is + unchanged. The shared `ConcurrencyLimiter` was also promoted from `engine/adapters` to + `common/utils` (it has no engine-specific logic), so the webhook module no longer imports across the + engine boundary. + +- **Optional Redis-backed rate-limit storage.** When `REDIS_ENABLED=true`, API rate-limit counters + now persist to Redis (a new `RedisThrottlerStorage` implementing @nestjs/throttler v6's + `ThrottlerStorage`), so limits aggregate across replicas behind a load balancer instead of being + per-process. Default off (single-node deployments gain nothing and it adds a connection dependency). + Fail-OPEN on Redis error — rate limiting is a secondary control, and fail-closed would self-DoS the + API. + +### Fixed + +- **Silent delivery failure for 1:1 Baileys sends to LID-migrated contacts (ack 463).** On the + Baileys engine, a 1:1 send addressed by phone (`@c.us` / `@s.whatsapp.net`) to a + contact WhatsApp has migrated to LID addressing was rejected server-side with ack error 463 + (`NackCallerReachoutTimelocked` / "missing tctoken" — the privacy token is stored and honored + under the LID), while the same send addressed to the contact's `@lid` delivered. Because + Baileys generates the message id locally, the API still returned a `messageId`, so the + non-delivery was silent to the caller. The adapter now resolves phone-dialect 1:1 chat ids to + the contact's LID at the send boundary via `sock.signalRepository.lidMapping.getLIDForPN` (the + same mapping the Baileys send path consults), applied in `sendTextMessage`, `sendContent` (all + media/location/contact/poll sends), and `sendChatState`. Groups, broadcast, already-`@lid`, and + unmapped ids pass through unchanged (non-migrated contacts behave identically), and resolution + is best-effort: any lookup error falls back to the phone jid. The disappearing-timer lookup + still resolves under the LID, since `getEphemeralExpiration` already keys on the raw, engine, + and neutral forms of the jid. Thanks @isaacmendes. [#717] + +- **Diagnosable failure for a stale browser profile after a binary-changing upgrade.** Upgrading + across the v0.8.12 amd64 browser-binary switch (Debian Chromium → Chrome for Testing, #663) — or any + later change to the Chromium/Chrome binary — can leave an already-authenticated `whatsapp-web.js` + session's persistent browser profile incompatible with the new binary: on the next start the page + context is destroyed during injection and the engine fails with Puppeteer's opaque + `Execution context was destroyed`, which reads like a Puppeteer bug and gave no hint that the stale + profile was the cause. The `whatsapp-web.js` adapter now detects that error in its `initialize()` + catch and logs an advisory pointing the operator at the remedy (delete the session profile dir and + re-scan); the error still propagates unchanged, so existing failure handling is unaffected. The + profile is not auto-recovered — a tainted profile is not safely portable across Chromium major + versions (clearing only the cache subdirs is insufficient), so a one-time re-authentication is + required. [#708] + +- **OpenAPI export script under current env validation.** `scripts/export-openapi.ts` had been broken + since the SQLite `DATABASE_NAME` file-path validation tightened (it pinned an in-memory data DB, + which that rule rejects). The data connection now uses a temp-dir SQLite file that is removed on + exit, so the snapshot generator runs hermetically again. + + +## [0.8.16] - 2026-07-12 + +### Added + +- **Integration SDK v1 `response` contract for inbound routes.** A route may now declare a host-side + `preflight` (today: `session-alive`, returning 503 for a definitively-dead WhatsApp session) and a + declarative `ack` (status/body/headers) returned synchronously to the provider. The plugin ALWAYS runs + async (enqueued, full DLQ/retry); for routes declaring `response`, the ack is returned without awaiting + enqueue so a queue-disabled deployment cannot block the provider's deadline. A dead session (no live + engine or `FAILED`) on a concrete-scoped route now fails fast with 503 instead of being swallowed into + 202; recoverable statuses still 202+enqueue and let the worker fail fast. The inert `mode: 'sync-reply'` + value is deprecated in favor of `response` (kept for SDK v1 additive-only compatibility). Routes with no + `response` are byte-identical to today's default fast-ack. + +- **`standard-webhooks` ingress signature scheme.** A route may now declare + `signature.scheme: "standard-webhooks"` to verify [Standard Webhooks](https://github.com/standard-webhooks/standard-webhooks) + payloads host-side (Supabase Auth's Send SMS hook, and any Svix-routed provider). The wire format is + fixed by the spec, so only `toleranceSec` (default 300s) and `dedupHeader` apply. The operator pastes + the provider's Svix secret (`v1,whsec_`) as the instance secret. This surfaces a bad signature + as a synchronous 401 and — because the `session-alive` preflight runs after verify — makes that preflight + safe to use (an unauthenticated caller can no longer probe liveness). Additive; existing + `hmac-sha256`/`shared-secret`/`none` behavior is unchanged. + +## [0.8.15] - 2026-07-11 + +- **WhatsApp Web sessions no longer wedge silently in `INITIALIZING` forever.** `engine.initialize()` was awaited with no timeout, and neither whatsapp-web.js nor Puppeteer bounds the initial browser launch/navigation (`page.goto` is called with `timeout: 0` and the web-version-cache fetch carries no timeout). If Chromium stalled under container memory pressure — realistic at the documented 2 GB Standard profile — the await never resolved or rejected: the session sat in `INITIALIZING` indefinitely, `GET /sessions/:id/qr` 400'd forever, and nothing was logged. The #635 abandoned-engine reaper is reactive (terminal `onError` / rejected re-init only), so nothing recovered it. `initializeEngine()` now races `engine.initialize()` against a deadline derived from the configured auth wait (floor 60 s, or `WWEBJS_AUTH_TIMEOUT_MS` + 30 s when an operator has raised that for slow first boots) so a legitimate slow init is never cut short; on timeout it force-kills the wedged browser, marks the session `DISCONNECTED` (retryable, so the existing reconnect backoff picks it up), and rethrows — surfacing the failure to a manual `POST /start` caller instead of hanging. The race's catch is scoped to the timeout only (`EngineInitTimeoutError`): a real init rejection (e.g. Chromium can't launch) propagates untouched so `start()`'s existing `FAILED`+reason diagnostics are preserved — handling both in one catch would downgrade real failures to `DISCONNECTED` and hide their reason. Thanks @INAPA-desarrolloTIC. [#667] + +- **Dashboard primary buttons were invisible until hover in light mode.** A leftover Vite template rule — `:root:not([data-theme='dark']) button { background-color:#f9f9f9 }` inside `@media (prefers-color-scheme: light)` — carried specificity `(0,2,1)` (`:root` + the `:not([data-theme])` argument + `button`), higher than every page-scoped `.X-page .btn-primary` rule `(0,2,0)`. In light mode (the default) it overrode the green design-system background with a near-white `#f9f9f9`, leaving the buttons' white text invisible until the leftover Vite `button:hover { border-color:#646cff }` ring revealed them on hover; the same `(0,2,1)` rule also pushed `.btn-secondary` and `.btn-icon` off their intended backgrounds. Removed that rule (Create-session and other primary buttons are green again), and cleared the remaining Vite scaffolding brand-colors from `index.css` (`a { color:#646cff }`, the purple `a:hover`, `button { background-color:#1a1a1a }`, `button:hover { border-color:#646cff }`) that competed with the App.css design system on import-order tiebreaks; the structural button CSS (border-radius, padding, font, cursor) is retained. Plain ` + {isOpen && ( +
+ {options.map((option, index) => ( + + ))} +
+ )} + + ); +} diff --git a/dashboard/src/components/DashboardCharts.css b/dashboard/src/components/DashboardCharts.css new file mode 100644 index 0000000..b178a03 --- /dev/null +++ b/dashboard/src/components/DashboardCharts.css @@ -0,0 +1,103 @@ +/* Dashboard analytics — all rules scoped under .dashboard-charts to avoid global collisions. */ +.dashboard-charts { + margin-bottom: 2rem; +} + +.dashboard-charts .charts-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1rem; +} + +.dashboard-charts .charts-title { + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text-primary); +} + +.dashboard-charts .charts-title h2 { + font-size: 1.125rem; + font-weight: 700; + margin: 0; +} + +/* Segmented pill period selector (matches the plugin/install tab control). */ +.dashboard-charts .period-toggle { + display: inline-flex; + gap: 2px; + padding: 3px; + background: var(--bg-light); + border: 1px solid var(--border); + border-radius: 10px; +} + +.dashboard-charts .period-tab { + padding: 0.35rem 0.85rem; + background: none; + border: none; + border-radius: 7px; + color: var(--text-secondary); + font-size: 0.8125rem; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease, color 0.15s ease; +} + +.dashboard-charts .period-tab.active { + background: var(--bg-white); + color: var(--text-primary); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06); +} + +.dashboard-charts .charts-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +.dashboard-charts .chart-card { + background: var(--bg-white); + border: 1px solid var(--border); + border-radius: 12px; + padding: 1.25rem; + min-width: 0; /* let recharts ResponsiveContainer shrink inside the grid track */ +} + +.dashboard-charts .chart-wide { + grid-column: 1 / -1; +} + +.dashboard-charts .chart-card h3 { + font-size: 0.9375rem; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 1rem; +} + +.dashboard-charts .charts-empty { + background: var(--bg-white); + border: 1px dashed var(--border); + border-radius: 12px; + padding: 2.5rem 1rem; + text-align: center; + color: var(--text-muted); + font-size: 0.875rem; +} + +.dashboard-charts .charts-empty.small { + border: none; + padding: 2rem 1rem; +} + +@media (max-width: 768px) { + .dashboard-charts .charts-grid { + grid-template-columns: 1fr; + } + .dashboard-charts .chart-wide { + grid-column: auto; + } +} diff --git a/dashboard/src/components/DashboardCharts.tsx b/dashboard/src/components/DashboardCharts.tsx new file mode 100644 index 0000000..9a25689 --- /dev/null +++ b/dashboard/src/components/DashboardCharts.tsx @@ -0,0 +1,194 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + ResponsiveContainer, + AreaChart, + Area, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + Legend, + PieChart, + Pie, + Cell, + BarChart, + Bar, +} from 'recharts'; +import { BarChart3 } from 'lucide-react'; +import { useStatsMessagesQuery } from '../hooks/queries'; +import type { StatsPeriod } from '../services/api'; +import './DashboardCharts.css'; + +const PERIODS: StatsPeriod[] = ['24h', '7d', '30d']; + +// Stable, distinct color per message type (recharts needs literal colors). Keyed by type name — +// not array index — so two types can never share a color, and a slice keeps its color even when the +// set of present types changes between requests. Covers every type mapMessageType() can emit. +const TYPE_COLORS: Record = { + text: '#25d366', + image: '#3b82f6', + contact: '#a855f7', + document: '#f59e0b', + audio: '#06b6d4', + voice: '#ec4899', + video: '#14b8a6', + sticker: '#ef4444', + location: '#84cc16', + poll: '#6366f1', + revoked: '#f43f5e', + masked: '#8b5cf6', + unknown: '#64748b', +}; + +// Deterministic fallback for any unmapped type, so its color is stable across renders. +const FALLBACK_COLORS = ['#0ea5e9', '#d946ef', '#f97316', '#10b981', '#6366f1', '#eab308']; +function colorForType(name: string): string { + if (TYPE_COLORS[name]) return TYPE_COLORS[name]; + let hash = 0; + for (let i = 0; i < name.length; i++) hash = (hash * 31 + name.charCodeAt(i)) | 0; + return FALLBACK_COLORS[Math.abs(hash) % FALLBACK_COLORS.length]; +} + +// '2026-06-24 14:00:00' (hour buckets) → '14:00'; '2026-06-24' (day buckets) → '06-24'. +function formatTick(ts: string, period: StatsPeriod): string { + return period === '24h' ? ts.slice(11, 16) : ts.slice(5); +} + +// WhatsApp ids look like '62812...@c.us' / '...@g.us' / '...@lid' — show just the local part. +function shortChat(chatId: string): string { + return chatId.split('@')[0] || chatId; +} + +export function DashboardCharts() { + const { t } = useTranslation(); + const [period, setPeriod] = useState('7d'); + const { data, isLoading, isError, error } = useStatsMessagesQuery(period); + + // Non-admin keys 403 on /stats/messages → hide the section entirely. Any OTHER error (e.g. a + // server 500) is a real fault: surface a small notice below instead of silently vanishing, which + // is what masked the #488 stats crash and made the whole chart "disappear" with no explanation. + const forbidden = (error as (Error & { status?: number }) | null)?.status === 403; + if (isError && forbidden) return null; + + const timeSeries = (data?.timeSeries ?? []).map(p => ({ ...p, label: formatTick(p.timestamp, period) })); + const byType = Object.entries(data?.byType ?? {}) + .map(([name, value]) => ({ name, value })) + .sort((a, b) => b.value - a.value); + const topChats = (data?.topChats ?? []).slice(0, 8).map(c => ({ name: c.chatName || shortChat(c.chatId), count: c.messageCount })); + const hasData = timeSeries.length > 0 || byType.length > 0 || topChats.length > 0; + + return ( +
+
+
+ +

{t('dashboard.charts.title')}

+
+
+ {PERIODS.map(p => ( + + ))} +
+
+ + {isLoading ? ( +
{t('common.loading')}
+ ) : isError ? ( +
{t('dashboard.charts.error')}
+ ) : !hasData ? ( +
{t('dashboard.charts.empty')}
+ ) : ( +
+
+

{t('dashboard.charts.overTime')}

+ + + + + + + + + + + + + + + + + + + + + +
+ +
+

{t('dashboard.charts.byType')}

+ {byType.length === 0 ? ( +
{t('dashboard.charts.empty')}
+ ) : ( + + + + {byType.map(entry => ( + + ))} + + + + + + )} +
+ +
+

{t('dashboard.charts.topChats')}

+ {topChats.length === 0 ? ( +
{t('dashboard.charts.empty')}
+ ) : ( + + + + + + + + + + )} +
+
+ )} +
+ ); +} diff --git a/dashboard/src/components/ErrorBoundary.tsx b/dashboard/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000..3bf9e7e --- /dev/null +++ b/dashboard/src/components/ErrorBoundary.tsx @@ -0,0 +1,63 @@ +import { Component, type ReactNode, type ErrorInfo } from 'react'; +import { AlertCircle, RefreshCw } from 'lucide-react'; +import i18n from '../i18n'; + +interface Props { + children: ReactNode; +} + +interface State { + hasError: boolean; + error: Error | null; +} + +export class ErrorBoundary extends Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: ErrorInfo) { + console.error('[ErrorBoundary] Uncaught error:', error, errorInfo); + } + + handleReload = () => { + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + return ( +
+ +

{i18n.t('errorBoundary.title')}

+

+ {i18n.t('errorBoundary.description')} +

+ +
+ ); + } + + return this.props.children; + } +} diff --git a/dashboard/src/components/FilterBuilder.css b/dashboard/src/components/FilterBuilder.css new file mode 100644 index 0000000..ddc4185 --- /dev/null +++ b/dashboard/src/components/FilterBuilder.css @@ -0,0 +1,289 @@ +.filter-builder { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.25rem; +} + +.filter-builder-head { + display: flex; + flex-direction: column; + gap: 0.125rem; +} + +.filter-builder-title { + display: block; + font-size: 0.875rem; + font-weight: 600; + color: var(--text-secondary, #64748b); +} + +.filter-builder-hint { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.filter-row { + display: flex; + flex-wrap: nowrap; + align-items: flex-start; + gap: 0.5rem; + padding: 0.5rem; + background: var(--bg-light); + border: 1px solid var(--border); + border-radius: 8px; +} + +.filter-row select { + height: 2.25rem; + padding: 0 0.5rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-white); + color: var(--text-primary); + font-size: 0.8125rem; +} + +/* field + operator: fixed width; value flexes to fill the rest */ +.filter-row > select { + width: 100%; +} + +.filter-field { + flex: 0 0 8.5rem; +} + +.filter-operator { + flex: 0 0 7rem; +} + +.filter-value { + flex: 1 1 auto; + min-width: 0; +} + +.filter-remove { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + height: 2.25rem; + width: 2.25rem; + border: 1px solid var(--border); + border-radius: 6px; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; +} + +.filter-remove svg { + width: 16px; + height: 16px; + flex-shrink: 0; + stroke: var(--text-secondary); +} + +.filter-remove:hover { + border-color: var(--error); + color: var(--error); +} + +.filter-remove:hover svg { + stroke: var(--error); +} + +.filter-add { + align-self: flex-start; + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.375rem 0.625rem; + border: 1px dashed var(--border); + border-radius: 6px; + background: transparent; + color: var(--primary); + font-size: 0.8125rem; + font-weight: 600; + cursor: pointer; +} + +.filter-add:hover { + border-color: var(--primary); +} + +/* Text operator */ +.filter-text { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.375rem; +} + +/* The global `.modal-body input` (full width + large padding + bg) leaks into the + case-sensitive checkbox; reset it (2-class selector beats `.modal-body input`). */ +.filter-case input { + width: auto; + margin: 0; + padding: 0; +} + +.filter-text input[type='text'] { + width: 100%; + height: 2.25rem; + padding: 0 0.625rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-white); + color: var(--text-primary); + font-size: 0.8125rem; +} + +.filter-case { + display: inline-flex; + align-items: center; + gap: 0.375rem; + font-size: 0.75rem; + color: var(--text-secondary); + cursor: pointer; +} + +/* Enum multi-select */ +.filter-enum { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; +} + +.enum-tag { + font-size: 0.75rem; + padding: 0.3125rem 0.5rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-white); + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s; +} + +.enum-tag:hover { + border-color: var(--primary); + color: var(--primary); +} + +.enum-tag.selected { + background: var(--primary); + border-color: var(--primary); + color: #fff; +} + +/* Chips input with autocomplete */ +.chips-input { + position: relative; +} + +.chips-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.375rem; + min-height: 2.25rem; + padding: 0.25rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--bg-white); +} + +.chip { + display: inline-flex; + align-items: center; + gap: 0.25rem; + padding: 0.125rem 0.375rem; + background: rgba(37, 211, 102, 0.15); + border: 1px solid rgba(37, 211, 102, 0.25); + color: var(--primary); + border-radius: 6px; + font-size: 0.75rem; + font-weight: 600; + max-width: 100%; +} + +.chip-remove { + display: inline-flex; + align-items: center; + background: transparent; + border: none; + color: inherit; + cursor: pointer; + padding: 0; +} + +/* `.chips-row` prefix raises specificity above the global `.modal-body input`, which + would otherwise force width:100% (dropping the field onto its own line) + bg + padding. */ +.chips-row .chips-text { + flex: 1 1 6rem; + min-width: 6rem; + width: auto; + height: 26px; + padding: 2px 8px; + border: none; + outline: none; + background: var(--bg-light); + color: var(--text-primary); + font-size: 0.75rem; + border: 1px solid var(--border); + border-radius: 6px; +} + +/* keep the yes/no select compact rather than filling the value column */ +.filter-bool { + width: auto; +} + +.chips-suggestions { + position: absolute; + z-index: 20; + top: calc(100% + 2px); + left: 0; + right: 0; + max-height: 14rem; + overflow-y: auto; + background: var(--bg-white); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18); +} + +.chips-suggestion { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + padding: 0.5rem 0.625rem; + border: none; + background: transparent; + cursor: pointer; + text-align: left; +} + +.chips-suggestion:hover { + background: var(--bg-light); +} + +.chips-suggestion-name { + font-size: 0.8125rem; + color: var(--text-primary); +} + +.chips-suggestion-id { + font-size: 0.6875rem; + color: var(--text-secondary); + font-family: 'JetBrains Mono', monospace; +} + +.chips-suggestion-add { + color: var(--primary); + font-weight: 600; + font-size: 0.8125rem; + border-top: 1px solid var(--border); +} diff --git a/dashboard/src/components/FilterBuilder.tsx b/dashboard/src/components/FilterBuilder.tsx new file mode 100644 index 0000000..51f2529 --- /dev/null +++ b/dashboard/src/components/FilterBuilder.tsx @@ -0,0 +1,292 @@ +import { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Plus, X } from 'lucide-react'; +import { + type Chat, + type WebhookFilters, + type WebhookFilterCondition, + type WebhookFilterOperator, +} from '../services/api'; +import './FilterBuilder.css'; + +type FieldKind = 'id' | 'idArray' | 'text' | 'enum' | 'boolean'; + +interface FieldDescriptor { + field: string; + kind: FieldKind; + operators: WebhookFilterOperator[]; + enumValues?: string[]; +} + +const MESSAGE_TYPES = [ + 'text', + 'image', + 'video', + 'audio', + 'voice', + 'document', + 'sticker', + 'location', + 'contact', + 'call', + 'revoked', + 'masked', + 'unknown', +]; + +// Mirrors the backend message-family field registry (src/modules/webhook/filters/filter-types.ts). +const MESSAGE_FIELDS: FieldDescriptor[] = [ + { field: 'sender', kind: 'id', operators: ['is', 'isNot'] }, + { field: 'recipient', kind: 'id', operators: ['is', 'isNot'] }, + { field: 'body', kind: 'text', operators: ['contains', 'equals'] }, + { field: 'type', kind: 'enum', operators: ['is', 'isNot'], enumValues: MESSAGE_TYPES }, + { field: 'isGroup', kind: 'boolean', operators: ['is'] }, + { field: 'fromMe', kind: 'boolean', operators: ['is'] }, + { field: 'hasMedia', kind: 'boolean', operators: ['is'] }, + { field: 'mentions', kind: 'idArray', operators: ['is', 'isNot'] }, +]; + +const descriptorFor = (field: string): FieldDescriptor => + MESSAGE_FIELDS.find(f => f.field === field) ?? MESSAGE_FIELDS[0]; + +function defaultValueFor(kind: FieldKind): WebhookFilterCondition['value'] { + if (kind === 'boolean') return true; + if (kind === 'text') return ''; + return []; +} + +/** Accepts a full JID or a phone number; normalizes bare numbers to `@c.us`. */ +function normalizeToJid(raw: string): string | null { + const value = raw.trim(); + if (!value) return null; + if (value.includes('@')) return value.toLowerCase(); + const digits = value.replace(/[^0-9]/g, ''); + return digits ? `${digits}@c.us` : null; +} + +interface ContactChipsInputProps { + value: string[]; + onChange: (next: string[]) => void; + chats: Chat[]; +} + +function ContactChipsInput({ value, onChange, chats }: ContactChipsInputProps) { + const { t } = useTranslation(); + const [text, setText] = useState(''); + const [open, setOpen] = useState(false); + + const suggestions = useMemo(() => { + const query = text.trim().toLowerCase(); + const chosen = new Set(value); + return chats + .filter(c => !chosen.has(c.id)) + .filter(c => !query || c.name.toLowerCase().includes(query) || c.id.toLowerCase().includes(query)) + .slice(0, 8); + }, [text, chats, value]); + + const labelFor = (jid: string) => chats.find(c => c.id === jid)?.name ?? jid; + const add = (jid: string) => { + if (jid && !value.includes(jid)) onChange([...value, jid]); + setText(''); + }; + const addTyped = () => { + const jid = normalizeToJid(text); + if (jid) add(jid); + }; + + return ( +
+
+ {value.map(jid => ( + + {labelFor(jid)} + + + ))} + setText(e.target.value)} + onFocus={() => setOpen(true)} + onBlur={() => setTimeout(() => setOpen(false), 120)} + onKeyDown={e => { + if (e.key === 'Enter') { + e.preventDefault(); + addTyped(); + } else if (e.key === 'Backspace' && !text && value.length) { + onChange(value.slice(0, -1)); + } + }} + /> +
+ {open && (suggestions.length > 0 || text.trim()) && ( +
+ {suggestions.map(c => ( + + ))} + {text.trim() && normalizeToJid(text) && ( + + )} +
+ )} +
+ ); +} + +interface FilterBuilderProps { + filters: WebhookFilters | null | undefined; + onChange: (filters: WebhookFilters | null) => void; + chats: Chat[]; +} + +export function FilterBuilder({ filters, onChange, chats }: FilterBuilderProps) { + const { t } = useTranslation(); + const conditions = filters?.conditions ?? []; + + const emit = (next: WebhookFilterCondition[]) => onChange(next.length ? { conditions: next } : null); + + const updateAt = (index: number, patch: Partial) => + emit(conditions.map((c, i) => (i === index ? { ...c, ...patch } : c))); + + const addCondition = () => { + const def = MESSAGE_FIELDS[0]; + emit([...conditions, { field: def.field, operator: def.operators[0], value: defaultValueFor(def.kind) }]); + }; + + const changeField = (index: number, field: string) => { + const def = descriptorFor(field); + updateAt(index, { field, operator: def.operators[0], value: defaultValueFor(def.kind), caseSensitive: undefined }); + }; + + return ( +
+
+ {t('webhooks.filters.title')} + {t('webhooks.filters.hint')} +
+ + {conditions.map((condition, index) => { + const def = descriptorFor(condition.field); + return ( +
+ + + + +
+ {(def.kind === 'id' || def.kind === 'idArray') && ( + updateAt(index, { value: next })} + chats={chats} + /> + )} + + {def.kind === 'enum' && ( +
+ {def.enumValues?.map(option => { + const selected = Array.isArray(condition.value) && (condition.value as string[]).includes(option); + return ( + + ); + })} +
+ )} + + {def.kind === 'text' && ( +
+ updateAt(index, { value: e.target.value })} + /> + +
+ )} + + {def.kind === 'boolean' && ( + + )} +
+ + +
+ ); + })} + + +
+ ); +} diff --git a/dashboard/src/components/GithubIcon.tsx b/dashboard/src/components/GithubIcon.tsx new file mode 100644 index 0000000..6d7b0d5 --- /dev/null +++ b/dashboard/src/components/GithubIcon.tsx @@ -0,0 +1,23 @@ +/** + * GitHub brand mark — inlined because lucide-react v1 removed all brand icons. + * Filled path + `fill="currentColor"` so it inherits color from CSS like the + * lucide icon it replaced. Decorative (`aria-hidden`); label the parent link. + */ +interface GithubIconProps { + size?: number; +} + +export function GithubIcon({ size = 24 }: GithubIconProps) { + return ( + + ); +} diff --git a/dashboard/src/components/GlobalSearch.css b/dashboard/src/components/GlobalSearch.css new file mode 100644 index 0000000..0721824 --- /dev/null +++ b/dashboard/src/components/GlobalSearch.css @@ -0,0 +1,125 @@ +.global-search { + position: relative; + display: flex; + align-items: center; + gap: 0.5rem; + width: 100%; +} + +.global-search-input { + flex: 1 1 auto; + min-width: 12rem; + height: 2.25rem; + padding: 0 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius, 8px); + background: var(--bg-white); + color: var(--text-primary); + font-size: 0.875rem; +} + +.global-search-input:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 2px var(--primary-soft); +} + +.global-search-scope { + display: inline-flex; + align-items: center; + gap: 0.25rem; + flex: 0 0 auto; + font-size: 0.75rem; + color: var(--text-secondary); + white-space: nowrap; + user-select: none; +} + +.global-search-scope input { + margin: 0; +} + +.global-search-results { + position: absolute; + top: calc(100% + 0.25rem); + left: 0; + right: 0; + z-index: 20; + max-height: 24rem; + overflow-y: auto; + background: var(--bg-white); + border: 1px solid var(--border); + border-radius: var(--radius, 8px); + box-shadow: var(--shadow-lg, var(--shadow-md)); +} + +.global-search-state { + padding: 0.75rem; + font-size: 0.8125rem; + color: var(--text-secondary); +} + +.global-search-hit { + display: flex; + flex-direction: column; + gap: 0.25rem; + width: 100%; + padding: 0.5rem 0.75rem; + background: transparent; + border: 0; + border-bottom: 1px solid var(--border); + text-align: left; + cursor: pointer; + font: inherit; + color: var(--text-primary); +} + +.global-search-hit:hover, +.global-search-hit:focus-visible { + background: var(--bg-light); + outline: none; +} + +.global-search-hit:last-child { + border-bottom: 0; +} + +.global-search-hit-meta { + font-size: 0.75rem; + color: var(--text-muted); + word-break: break-all; +} + +.global-search-hit-snippet { + font-size: 0.8125rem; + color: var(--text-primary); + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.global-search-hit-snippet mark { + background: var(--warning, #fde68a); + color: inherit; + border-radius: 2px; + padding: 0 1px; +} + +.global-search-more { + display: block; + width: 100%; + padding: 0.5rem; + background: var(--bg-light); + border: 0; + border-top: 1px solid var(--border); + color: var(--text-secondary); + font-size: 0.75rem; + cursor: pointer; +} + +.global-search-more:hover { + background: var(--primary-soft); + color: var(--primary); +} diff --git a/dashboard/src/components/GlobalSearch.tsx b/dashboard/src/components/GlobalSearch.tsx new file mode 100644 index 0000000..e31a7cd --- /dev/null +++ b/dashboard/src/components/GlobalSearch.tsx @@ -0,0 +1,101 @@ +import { useState, useEffect, useCallback, useRef } from 'react'; +import { searchApi, type SearchHit } from '../services/api'; +import { renderHighlightedSnippet, buildSearchParams } from '../utils/search-highlight'; +import { useTranslation } from 'react-i18next'; +import './GlobalSearch.css'; + +interface GlobalSearchProps { + /** Called when the user clicks a result — the parent navigates to that chat/message. */ + onHit: (hit: SearchHit) => void; + /** When set, the scope toggle defaults to this session (optional). */ + currentSessionId?: string; +} + +const DEBOUNCE_MS = 300; +const PAGE_SIZE = 20; + +export function GlobalSearch({ onHit, currentSessionId }: GlobalSearchProps) { + const { t } = useTranslation(); + const [q, setQ] = useState(''); + const [hits, setHits] = useState([]); + const [total, setTotal] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [scopeCurrent, setScopeCurrent] = useState(false); + const [open, setOpen] = useState(false); + const timer = useRef | null>(null); + const blurTimer = useRef | null>(null); + + const run = useCallback(async (query: string, offset: number, append: boolean) => { + const params = buildSearchParams(query, scopeCurrent && currentSessionId ? { sessionId: currentSessionId } : undefined, { limit: PAGE_SIZE, offset }); + if (!params) { setHits([]); setTotal(0); setError(null); setLoading(false); return; } + setLoading(true); setError(null); + try { + const res = await searchApi.search(params); + setHits(prev => append ? [...prev, ...res.hits] : res.hits); + setTotal(res.total); + } catch (e: unknown) { + const status = (e as { status?: number }).status; + if (status === 501) setError(t('search.unavailable')); + else if (status === 503) setError(t('search.error')); + else setError(t('search.error')); + setHits([]); setTotal(0); + } finally { + setLoading(false); + } + }, [scopeCurrent, currentSessionId, t]); + + // Debounce on input change. + useEffect(() => { + if (timer.current) clearTimeout(timer.current); + if (!q.trim()) { setHits([]); setTotal(0); setError(null); return; } + timer.current = setTimeout(() => { setOpen(true); void run(q, 0, false); }, DEBOUNCE_MS); + return () => { if (timer.current) clearTimeout(timer.current); }; + }, [q, run]); + + // Clear any pending blur timeout on unmount so it can't fire setState after teardown. + useEffect(() => { + return () => { if (blurTimer.current) clearTimeout(blurTimer.current); }; + }, []); + + const loadMore = () => void run(q, hits.length, true); + + return ( +
+ setQ(e.target.value)} + onFocus={() => q.trim() && setOpen(true)} + onBlur={() => { blurTimer.current = setTimeout(() => setOpen(false), 150); }} + aria-label={t('search.placeholder')} + /> + {currentSessionId && ( + + )} + {open && q.trim() && ( +
+ {loading &&
{t('search.loading')}
} + {!loading && error &&
{error}
} + {!loading && !error && hits.length === 0 &&
{t('search.empty')}
} + {!loading && !error && hits.map((h) => ( + + ))} + {!loading && !error && hits.length < total && ( + + )} +
+ )} +
+ ); +} diff --git a/dashboard/src/components/Layout.css b/dashboard/src/components/Layout.css new file mode 100644 index 0000000..0fdd729 --- /dev/null +++ b/dashboard/src/components/Layout.css @@ -0,0 +1,626 @@ +.layout { + display: flex; + min-height: 100vh; +} + +/* ==================== Sidebar ==================== */ +.sidebar { + width: 260px; + background: var(--bg-white); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + position: fixed; + height: 100vh; + z-index: 100; + transition: + width 0.3s ease, + transform 0.3s ease; +} + +.sidebar.collapsed { + width: 72px; +} + +.sidebar-header { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1.5rem; + border-bottom: 1px solid var(--border); + min-height: 72px; +} + +.sidebar.collapsed .sidebar-header { + justify-content: center; + padding: 1.5rem 1rem; +} + +.sidebar-logo { + width: 28px; + height: 28px; + object-fit: contain; + flex-shrink: 0; +} + +.mobile-brand .sidebar-logo { + width: 24px; + height: 24px; +} + +.sidebar-brand { + display: flex; + flex-direction: column; + overflow: hidden; + white-space: nowrap; +} + +.brand-name { + font-size: 1.125rem; + font-weight: 800; + color: var(--text-primary); + letter-spacing: -0.01em; +} + +.brand-subtitle { + font-size: 0.7rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); +} + +.brand-version { + font-size: 0.625rem; + font-weight: 500; + letter-spacing: 0.02em; + color: var(--text-muted); + opacity: 0.7; + margin-top: 1px; +} + +/* Collapse Toggle Button - Circular design at edge */ +.collapse-toggle { + position: absolute; + right: -14px; + top: 36px; + width: 28px; + height: 28px; + border-radius: 50%; + background: var(--bg-white); + border: 1px solid var(--border); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: var(--text-secondary); + transition: all 0.2s; + z-index: 101; + padding: 0; +} + +.collapse-toggle:hover { + background: var(--primary); + border-color: var(--primary); + color: white; + box-shadow: 0 4px 12px rgba(37, 211, 102, 0.3); +} + +.collapse-toggle svg { + width: 16px; + height: 16px; +} + +.sidebar.collapsed .collapse-toggle { + right: -14px; +} + +.sidebar-nav { + flex: 1; + padding: 1rem 0.75rem; + display: flex; + flex-direction: column; + gap: 0.25rem; + overflow-y: auto; +} + +.sidebar.collapsed .sidebar-nav { + padding: 1rem 0.5rem; +} + +.nav-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.7rem 1rem; + color: var(--text-secondary); + text-decoration: none; + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 500; + transition: all 0.2s; + white-space: nowrap; + overflow: hidden; +} + +.sidebar.collapsed .nav-item { + justify-content: center; + padding: 0.7rem; +} + +.nav-item:hover { + background: var(--bg-light); + color: var(--text-primary); + text-decoration: none; +} + +.nav-item.active { + background: var(--primary-soft); + color: var(--primary); +} + +.nav-item.active svg { + color: var(--primary); +} + +.sidebar-footer { + padding: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + border-top: 1px solid var(--border); +} + +.sidebar.collapsed .sidebar-footer { + padding: 0.75rem 0.5rem; +} + +.theme-toggle-btn { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.6rem 0.9rem; + color: var(--text-secondary); + background: none; + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + overflow: hidden; +} + +.appearance-button-cue { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + flex-shrink: 0; + border-radius: 999px; + background: radial-gradient(circle at 70% 25%, rgba(255, 255, 255, 0.8), transparent 24%), var(--swatch-color); + color: white; + box-shadow: + 0 0 0 2px var(--bg-white), + 0 0 0 3px color-mix(in srgb, var(--swatch-color) 35%, transparent); +} + +.appearance-button-cue svg { + filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.25)); +} + +.sidebar.collapsed .theme-toggle-btn, +.sidebar.collapsed .logout-btn { + justify-content: center; + padding: 0.6rem; +} + +.theme-toggle-btn:hover { + background: var(--bg-light); + color: var(--text-primary); +} + +.language-menu, +.appearance-menu { + position: relative; +} + +.language-menu .theme-toggle-btn, +.appearance-menu .theme-toggle-btn { + width: 100%; +} + +.language-menu-list { + position: absolute; + left: 0; + right: 0; + bottom: calc(100% + 0.35rem); + display: flex; + flex-direction: column; + gap: 0.15rem; + min-width: 160px; + padding: 0.3rem; + background: var(--bg-white); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: 0 10px 30px rgba(15, 23, 42, 0.16); + z-index: 120; +} + +.language-menu-item { + display: flex; + align-items: center; + width: 100%; + min-height: 34px; + padding: 0.45rem 0.6rem; + color: var(--text-secondary); + background: transparent; + border: 0; + border-radius: calc(var(--radius) - 2px); + font-size: 0.875rem; + font-weight: 500; + text-align: left; + cursor: pointer; + transition: all 0.2s; +} + +.language-menu-item:hover { + background: var(--bg-light); + color: var(--text-primary); +} + +.language-menu-item.active { + background: var(--primary-soft); + color: var(--primary); +} + +.sidebar.collapsed .language-menu-list { + right: auto; + left: calc(100% + 0.5rem); + bottom: 0; +} + +.appearance-menu-list { + position: absolute; + left: 0; + right: 0; + bottom: calc(100% + 0.35rem); + display: flex; + flex-direction: column; + gap: 0.85rem; + max-width: 100%; + padding: 0.85rem; + background: var(--bg-white); + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: 0 18px 45px rgba(15, 23, 42, 0.18); + z-index: 120; +} + +.appearance-menu-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0.2rem 0.1rem 0.75rem; + border-bottom: 1px solid var(--border); +} + +.appearance-menu-header strong { + display: block; + color: var(--text-primary); + font-size: 0.9375rem; + line-height: 1.2; +} + +.appearance-menu-header span { + display: block; + margin-top: 0.15rem; + color: var(--text-muted); + font-size: 0.75rem; +} + +.appearance-current-swatch { + width: 34px; + height: 34px; + flex-shrink: 0; + border-radius: 999px; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.65), transparent 42%), var(--swatch-color); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.35), + 0 0 0 4px color-mix(in srgb, var(--swatch-color) 13%, transparent); +} + +.appearance-section { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.appearance-section-label { + font-size: 0.6875rem; + font-weight: 800; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.appearance-mode-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.35rem; +} + +.appearance-mode { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.25rem; + min-height: 58px; + padding: 0.45rem; + background: var(--bg-light); + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text-secondary); + font-size: 0.75rem; +} + +.appearance-mode:hover { + color: var(--text-primary); + border-color: var(--primary); + background: var(--bg-white); +} + +.appearance-mode.active { + background: var(--primary-soft); + border-color: var(--primary); + color: var(--primary); + box-shadow: inset 0 -2px 0 var(--primary); +} + +.palette-grid { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.palette-swatch { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + width: 30px; + height: 30px; + flex: 0 0 auto; + padding: 0; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--bg-light); +} + +.palette-swatch span { + width: 20px; + height: 20px; + border-radius: 999px; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.6), transparent 45%), var(--swatch-color); + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.35); +} + +.palette-swatch:hover, +.palette-swatch.active { + border-color: var(--swatch-color); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--swatch-color) 18%, transparent); +} + +.palette-swatch.active::after { + position: absolute; + right: -1px; + bottom: -1px; + width: 10px; + height: 10px; + border: 2px solid var(--bg-white); + border-radius: 999px; + background: var(--primary); + content: ''; +} + +.sidebar.collapsed .appearance-menu-list { + right: auto; + left: calc(100% + 0.5rem); + bottom: 0; +} + +.logout-btn { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.6rem 0.9rem; + color: var(--text-secondary); + background: none; + border: 1px solid var(--border); + border-radius: var(--radius); + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + white-space: nowrap; + overflow: hidden; +} + +.logout-btn:hover { + background: rgba(239, 68, 68, 0.12); + border-color: rgba(239, 68, 68, 0.3); + color: var(--error); +} + +/* ==================== Main Content ==================== */ +.main-content { + flex: 1; + margin-left: 260px; + width: calc(100% - 260px); + background: var(--bg-light); + min-height: 100vh; + overflow-x: hidden; + transition: + margin-left 0.3s ease, + width 0.3s ease; +} + +.main-content.expanded { + margin-left: 72px; + width: calc(100% - 72px); +} + +/* ==================== Mobile Styles ==================== */ +.mobile-header { + display: none; +} + +.sidebar-overlay { + display: none; +} + +@media (max-width: 767px) { + .mobile-header { + display: flex; + align-items: center; + justify-content: space-between; + position: fixed; + top: 0; + left: 0; + right: 0; + height: 56px; + padding: 0 1rem; + background: var(--bg-white); + border-bottom: 1px solid var(--border); + z-index: 90; + } + + .mobile-menu-btn { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: transparent !important; + border: none; + color: var(--text-primary); + cursor: pointer; + border-radius: var(--radius); + transition: background 0.2s; + padding: 0; + } + + .mobile-menu-btn svg { + color: var(--text-primary); + stroke: var(--text-primary); + } + + .mobile-menu-btn:hover { + background: var(--bg-light) !important; + } + + .mobile-brand { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .mobile-brand .brand-name { + font-size: 1rem; + } + + .sidebar.mobile { + transform: translateX(-100%); + width: 280px; + box-shadow: 2px 0 20px rgba(0, 0, 0, 0.1); + } + + .sidebar.mobile.open { + transform: translateX(0); + } + + .sidebar-overlay { + display: block; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 95; + animation: fadeIn 0.2s ease; + } + + @keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + .main-content.mobile { + margin-left: 0; + width: 100%; + padding-top: 56px; + height: 100vh; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + } + + .collapse-toggle { + display: none; + } +} + +/* ==================== RTL support ==================== */ +[dir='rtl'] .sidebar { + border-right: none; + border-left: 1px solid var(--border); +} + +[dir='rtl'] .collapse-toggle { + right: auto; + left: -14px; +} + +[dir='rtl'] .collapse-toggle svg { + transform: scaleX(-1); +} + +[dir='rtl'] .language-menu-item { + text-align: right; +} + +[dir='rtl'] .sidebar.collapsed .language-menu-list { + right: calc(100% + 0.5rem); + left: auto; +} + +[dir='rtl'] .sidebar.collapsed .appearance-menu-list { + right: calc(100% + 0.5rem); + left: auto; +} + +[dir='rtl'] .main-content { + margin-left: 0; + margin-right: 260px; + transition: + margin-right 0.3s ease, + width 0.3s ease; +} + +[dir='rtl'] .main-content.expanded { + margin-right: 72px; +} + +@media (max-width: 768px) { + [dir='rtl'] .main-content, + [dir='rtl'] .main-content.mobile { + margin-right: 0; + } +} diff --git a/dashboard/src/components/Layout.tsx b/dashboard/src/components/Layout.tsx new file mode 100644 index 0000000..2217584 --- /dev/null +++ b/dashboard/src/components/Layout.tsx @@ -0,0 +1,344 @@ +import { useState, useEffect, useRef, type CSSProperties } from 'react'; +import { NavLink, Outlet } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { + LayoutDashboard, + Smartphone, + MessageSquare, + Webhook, + Key, + FileText, + ClipboardList, + LogOut, + Send, + Server, + Puzzle, + Sun, + Moon, + Monitor, + Menu, + X, + ChevronLeft, + ChevronRight, + Languages, +} from 'lucide-react'; +import { useTheme } from '../hooks/useTheme'; +import { type UserRole } from '../hooks/useRole'; +import { languageOptions, resolveSupportedLanguage, rtlLanguages, type SupportedLanguage } from '../i18n'; +import { healthApi } from '../services/api'; +import './Layout.css'; + +interface LayoutProps { + onLogout: () => void; + userRole: UserRole | null; +} + +const allNavItems = [ + { to: '/', icon: LayoutDashboard, key: 'dashboard' as const, adminOnly: false }, + { to: '/sessions', icon: Smartphone, key: 'sessions' as const, adminOnly: false }, + { to: '/chats', icon: MessageSquare, key: 'chats' as const, adminOnly: false }, + { to: '/webhooks', icon: Webhook, key: 'webhooks' as const, adminOnly: false }, + { to: '/templates', icon: ClipboardList, key: 'templates' as const, adminOnly: false }, + { to: '/api-keys', icon: Key, key: 'apiKeys' as const, adminOnly: true }, + { to: '/message-tester', icon: Send, key: 'messageTester' as const, adminOnly: false }, + // Backend /infra/* is ADMIN-only; hide the nav item from non-admins (UX + defense-in-depth). + { to: '/infrastructure', icon: Server, key: 'infrastructure' as const, adminOnly: true }, + { to: '/plugins', icon: Puzzle, key: 'plugins' as const, adminOnly: true }, + { to: '/logs', icon: FileText, key: 'logs' as const, adminOnly: false }, +]; + +const themeIcons = { light: Sun, dark: Moon, system: Monitor }; + +export function Layout({ onLogout, userRole }: LayoutProps) { + const { t, i18n } = useTranslation(); + const { theme, setTheme, palette, setPalette, paletteOptions } = useTheme(); + const ThemeIcon = themeIcons[theme]; + const themeLabel = t(`theme.${theme}`); + const activePalette = paletteOptions.find(option => option.value === palette) ?? paletteOptions[0]; + + const navItems = allNavItems.filter(item => !item.adminOnly || userRole === 'admin'); + + const [isCollapsed, setIsCollapsed] = useState(false); + const [isMobileOpen, setIsMobileOpen] = useState(false); + const [isMobile, setIsMobile] = useState(window.innerWidth < 768); + // Show the build-time version immediately, then replace it with the live running version from the + // backend so a stale-built bundle can't display the wrong number. Falls back silently on error. + const [version, setVersion] = useState(__APP_VERSION__); + const [isLanguageMenuOpen, setIsLanguageMenuOpen] = useState(false); + const [isAppearanceMenuOpen, setIsAppearanceMenuOpen] = useState(false); + const languageMenuRef = useRef(null); + const appearanceMenuRef = useRef(null); + + useEffect(() => { + const handleResize = () => { + const mobile = window.innerWidth < 768; + setIsMobile(mobile); + if (!mobile) setIsMobileOpen(false); + }; + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + useEffect(() => { + let active = true; + healthApi + .check() + .then(info => { + if (active && info?.version) setVersion(info.version); + }) + .catch(() => { + /* keep the build-time fallback */ + }); + return () => { + active = false; + }; + }, []); + + const handleNavClick = () => { + if (isMobile) setIsMobileOpen(false); + }; + + useEffect(() => { + document.body.style.overflow = isMobileOpen ? 'hidden' : ''; + return () => { + document.body.style.overflow = ''; + }; + }, [isMobileOpen]); + + useEffect(() => { + if (!isLanguageMenuOpen) return; + + const closeOnOutsideClick = (event: MouseEvent) => { + if (!languageMenuRef.current?.contains(event.target as Node)) { + setIsLanguageMenuOpen(false); + } + }; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsLanguageMenuOpen(false); + }; + + document.addEventListener('mousedown', closeOnOutsideClick); + document.addEventListener('keydown', closeOnEscape); + return () => { + document.removeEventListener('mousedown', closeOnOutsideClick); + document.removeEventListener('keydown', closeOnEscape); + }; + }, [isLanguageMenuOpen]); + + useEffect(() => { + if (!isAppearanceMenuOpen) return; + + const closeOnOutsideClick = (event: MouseEvent) => { + if (!appearanceMenuRef.current?.contains(event.target as Node)) { + setIsAppearanceMenuOpen(false); + } + }; + const closeOnEscape = (event: KeyboardEvent) => { + if (event.key === 'Escape') setIsAppearanceMenuOpen(false); + }; + + document.addEventListener('mousedown', closeOnOutsideClick); + document.addEventListener('keydown', closeOnEscape); + return () => { + document.removeEventListener('mousedown', closeOnOutsideClick); + document.removeEventListener('keydown', closeOnEscape); + }; + }, [isAppearanceMenuOpen]); + + const toggleCollapse = () => setIsCollapsed(!isCollapsed); + const toggleMobile = () => setIsMobileOpen(!isMobileOpen); + + const currentLang = resolveSupportedLanguage(i18n.resolvedLanguage || i18n.language); + const languageLabel = languageOptions.find(option => option.value === currentLang)?.compactLabel ?? 'EN'; + const changeLanguage = (language: SupportedLanguage) => { + setIsLanguageMenuOpen(false); + void i18n.changeLanguage(language); + }; + const isRtl = rtlLanguages.includes(currentLang); + + return ( +
+ {isMobile && ( +
+ +
+ OpenWA + {t('common.appName')} +
+
+
+ )} + + {isMobile && isMobileOpen &&
setIsMobileOpen(false)} />} + + + +
+ +
+
+ ); +} diff --git a/dashboard/src/components/PageHeader.css b/dashboard/src/components/PageHeader.css new file mode 100644 index 0000000..0404094 --- /dev/null +++ b/dashboard/src/components/PageHeader.css @@ -0,0 +1,87 @@ +/* ============================================= + PageHeader Component - Shared Styles + Consistent header styling for all pages + ============================================= */ + +/* Grid so the three regions can re-order responsively: on desktop the title and actions share + the top row with the subtitle beneath; on mobile the subtitle follows the title directly and + the actions drop to the bottom (description should follow the title, not the call-to-action). */ +.page-header { + display: grid; + grid-template-columns: 1fr auto; + grid-template-areas: + 'title actions' + 'subtitle subtitle'; + align-items: center; + column-gap: 1rem; + margin-bottom: 1.5rem; + width: 100%; +} + +.page-header__title-group { + grid-area: title; + display: flex; + align-items: center; + gap: 0.75rem; + min-width: 0; +} + +.page-header__title-group h1 { + margin: 0; + /* h1 styles inherited from global */ +} + +.page-header__badge { + display: inline-flex; +} + +.page-header__actions { + grid-area: actions; + justify-self: end; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.page-header__subtitle { + grid-area: subtitle; + margin: 0; + margin-top: 0.5rem; /* Consistent 8px gap from title row */ + font-size: 0.9375rem; + color: var(--text-muted); + line-height: 1.5; +} + +/* Mobile Responsive: stack as title -> subtitle -> actions */ +@media (max-width: 768px) { + .page-header { + grid-template-columns: 1fr; + grid-template-areas: + 'title' + 'subtitle' + 'actions'; + column-gap: 0; + } + + .page-header__title-group h1 { + font-size: 1.5rem; + } + + .page-header__actions { + justify-self: stretch; + margin-top: 0.25rem; + } + + .page-header__subtitle { + font-size: 0.875rem; + } +} + +@media (max-width: 480px) { + .page-header__actions > button, + .page-header__actions > .btn-primary, + .page-header__actions > .btn-secondary { + width: 100%; + justify-content: center; + } +} diff --git a/dashboard/src/components/PageHeader.tsx b/dashboard/src/components/PageHeader.tsx new file mode 100644 index 0000000..34f7553 --- /dev/null +++ b/dashboard/src/components/PageHeader.tsx @@ -0,0 +1,38 @@ +import type { ReactNode } from 'react'; +import './PageHeader.css'; + +interface PageHeaderProps { + title: string; + subtitle?: string; + badge?: ReactNode; + actions?: ReactNode; +} + +/** + * Shared page header component for consistent styling across all pages. + * + * @example + * // Simple usage + * + * + * @example + * // With badge and actions + * } + * subtitle="Overview of your WhatsApp sessions" + * actions={} + * /> + */ +export function PageHeader({ title, subtitle, badge, actions }: PageHeaderProps) { + return ( +
+
+

{title}

+ {badge && {badge}} +
+ {actions &&
{actions}
} + {subtitle &&

{subtitle}

} +
+ ); +} diff --git a/dashboard/src/components/PluginInstances.css b/dashboard/src/components/PluginInstances.css new file mode 100644 index 0000000..555770f --- /dev/null +++ b/dashboard/src/components/PluginInstances.css @@ -0,0 +1,116 @@ +.plugin-instances { + display: flex; + flex-direction: column; + gap: 1rem; +} +.plugin-instances .pi-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 1rem; +} +.plugin-instances .pi-desc { + color: var(--text-muted); + margin: 0; + flex: 1; +} +.plugin-instances .pi-loading { + display: flex; + justify-content: center; + padding: 2rem; +} +.plugin-instances .pi-empty { + text-align: center; + color: var(--text-muted); + padding: 2rem 1rem; +} +.plugin-instances .pi-list { + display: flex; + flex-direction: column; + gap: 0.5rem; +} +.plugin-instances .pi-row { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem; + border: 1px solid var(--border); + border-radius: 8px; +} +.plugin-instances .pi-main { + display: flex; + flex-direction: column; + min-width: 0; +} +.plugin-instances .pi-id { + font-weight: 600; +} +.plugin-instances .pi-scope { + font-size: 0.8rem; + color: var(--text-muted); +} +.plugin-instances .pi-url { + flex: 1; + display: flex; + align-items: center; + gap: 0.5rem; + min-width: 0; +} +.plugin-instances .pi-url code { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.plugin-instances .pi-badge { + font-size: 0.75rem; + padding: 0.15rem 0.5rem; + border-radius: 999px; + white-space: nowrap; +} +.plugin-instances .pi-badge.on { + background: var(--success-bg, #dcfce7); + color: var(--success, #16a34a); +} +.plugin-instances .pi-badge.off { + background: var(--bg-secondary); + color: var(--text-muted); +} +.plugin-instances .pi-actions { + display: flex; + gap: 0.25rem; + margin-left: auto; +} +.plugin-instances .pi-hint { + font-size: 0.8rem; + color: var(--text-muted); + margin: 0.25rem 0 0.75rem; +} +.plugin-instances .pi-secret { + display: flex; + gap: 0.5rem; + align-items: center; + margin-bottom: 0.5rem; +} +.plugin-instances .pi-secret code { + flex: 1; + padding: 0.75rem; + background: var(--bg-secondary); + border-radius: 6px; + word-break: break-all; +} +.plugin-instances .pi-error { + color: var(--danger, #e5484d); + font-size: 0.85rem; + margin-top: 0.5rem; +} +.plugin-instances .pi-confirm-icon { + display: flex; + justify-content: center; + color: var(--danger, #e5484d); + margin-bottom: 0.75rem; +} +.plugin-instances textarea { + width: 100%; + min-height: 96px; + font-family: monospace; +} diff --git a/dashboard/src/components/PluginInstances.tsx b/dashboard/src/components/PluginInstances.tsx new file mode 100644 index 0000000..1dd66d5 --- /dev/null +++ b/dashboard/src/components/PluginInstances.tsx @@ -0,0 +1,363 @@ +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Plus, Copy, Check, RefreshCw, Pencil, Trash2, X, Loader2, Power, AlertTriangle } from 'lucide-react'; +import type { InstanceView, MintedInstance } from '../services/api'; +import { + usePluginInstancesQuery, + useCreateInstanceMutation, + useRegenerateInstanceSecretMutation, + useUpdateInstanceMutation, + useDeleteInstanceMutation, +} from '../hooks/queries'; +import { isValidInstanceId, parseInstanceConfig } from '../utils/instanceForm'; +import { copyToClipboard } from '../utils/clipboard'; +import { useToast } from './Toast'; +import './PluginInstances.css'; + +const emptyForm = { instanceId: '', sessionScope: '', verifyToken: '', config: '' }; + +export function PluginInstances({ pluginId }: { pluginId: string }) { + const { t } = useTranslation(); + const toast = useToast(); + const { data: instances = [], isLoading, isError } = usePluginInstancesQuery(pluginId, true); + const createM = useCreateInstanceMutation(pluginId); + const regenM = useRegenerateInstanceSecretMutation(pluginId); + const updateM = useUpdateInstanceMutation(pluginId); + const deleteM = useDeleteInstanceMutation(pluginId); + + const [showForm, setShowForm] = useState(false); + const [form, setForm] = useState(emptyForm); + const [formError, setFormError] = useState(null); + const [minted, setMinted] = useState(null); // secret-shown-once view + const [mintedKind, setMintedKind] = useState<'created' | 'regenerated'>('created'); + const [editing, setEditing] = useState(null); + const [editForm, setEditForm] = useState({ sessionScope: '', config: '' }); + const [editError, setEditError] = useState(null); + const [confirm, setConfirm] = useState<{ type: 'delete' | 'regenerate'; inst: InstanceView } | null>(null); + const [copied, setCopied] = useState(null); + + const copy = async (text: string, id: string) => { + if (await copyToClipboard(text)) { + setCopied(id); + setTimeout(() => setCopied(null), 2000); + } + }; + + const openCreate = () => { + setForm(emptyForm); + setFormError(null); + setShowForm(true); + }; + + const submitCreate = async () => { + if (!isValidInstanceId(form.instanceId)) { + setFormError(t('plugins.instances.errors.invalidId')); + return; + } + const parsed = parseInstanceConfig(form.config); + if (!parsed.ok) { + setFormError(t('plugins.instances.errors.invalidJson')); + return; + } + try { + const created = await createM.mutateAsync({ + instanceId: form.instanceId, + sessionScope: form.sessionScope.trim() || undefined, + verifyToken: form.verifyToken.trim() || undefined, + config: parsed.value, + }); + setShowForm(false); + setForm(emptyForm); + setMintedKind('created'); + setMinted(created); + toast.success(t('plugins.instances.toasts.created'), created.instanceId); + } catch (err) { + const e = err as Error & { status?: number }; + setFormError(e.status === 409 ? t('plugins.instances.errors.duplicateId') : e.message); + } + }; + + const toggleEnabled = async (inst: InstanceView) => { + try { + await updateM.mutateAsync({ instanceId: inst.instanceId, body: { enabled: !inst.enabled } }); + toast.success(t('plugins.instances.toasts.updated'), inst.instanceId); + } catch (err) { + toast.error(t('plugins.instances.toasts.actionFailed'), (err as Error).message); + } + }; + + const openEdit = (inst: InstanceView) => { + setEditing(inst); + setEditForm({ + sessionScope: inst.sessionScope ?? '', + config: inst.config ? JSON.stringify(inst.config, null, 2) : '', + }); + setEditError(null); + }; + + const submitEdit = async () => { + if (!editing) return; + const parsed = parseInstanceConfig(editForm.config); + if (!parsed.ok) { + setEditError(t('plugins.instances.errors.invalidJson')); + return; + } + try { + await updateM.mutateAsync({ + instanceId: editing.instanceId, + // Blank → omit (leave scope unchanged); mirrors create. Sending '' would corrupt an + // all-sessions (null) instance into a literal empty scope the backend never clears. + body: { sessionScope: editForm.sessionScope.trim() || undefined, config: parsed.value ?? {} }, + }); + setEditing(null); + toast.success(t('plugins.instances.toasts.updated'), editing.instanceId); + } catch (err) { + setEditError((err as Error).message); + } + }; + + const runConfirm = async () => { + if (!confirm) return; + const { type, inst } = confirm; + setConfirm(null); + try { + if (type === 'delete') { + await deleteM.mutateAsync(inst.instanceId); + toast.success(t('plugins.instances.toasts.deleted'), inst.instanceId); + } else { + const res = await regenM.mutateAsync(inst.instanceId); + setMintedKind('regenerated'); + setMinted(res); + toast.success(t('plugins.instances.toasts.secretRegenerated'), inst.instanceId); + } + } catch (err) { + toast.error(t('plugins.instances.toasts.actionFailed'), (err as Error).message); + } + }; + + return ( +
+
+

{t('plugins.instances.description')}

+ +
+ + {isLoading ? ( +
+ +
+ ) : isError ? ( +

{t('plugins.instances.loadError')}

+ ) : instances.length === 0 ? ( +

{t('plugins.instances.empty')}

+ ) : ( +
+ {instances.map(inst => ( +
+
+ {inst.instanceId} + {inst.sessionScope || t('plugins.instances.allSessions')} +
+ {inst.ingressUrls[0] && ( +
+ {inst.ingressUrls[0].url} + +
+ )} + + {inst.enabled ? t('plugins.instances.enabled') : t('plugins.instances.disabled')} + +
+ + + + +
+
+ ))} +
+ )} + + {/* Create modal — form, or the secret-shown-once view after mint */} + {showForm && ( +
setShowForm(false)}> +
e.stopPropagation()}> +
+

{t('plugins.instances.create')}

+ +
+
+ + setForm({ ...form, instanceId: e.target.value })} + /> +

{t('plugins.instances.form.instanceIdHint')}

+ + setForm({ ...form, sessionScope: e.target.value })} + /> + + setForm({ ...form, verifyToken: e.target.value })} + /> + +