From ebd7ab728e0805d6fd974b72d21106c2c5599320 Mon Sep 17 00:00:00 2001 From: Matheus Pastorini Date: Thu, 25 Jun 2026 11:13:47 -0300 Subject: [PATCH 1/2] fix: instance creation broken by sanitization guard and silent error swallow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes two cascading bugs that prevented instance creation and produced a misleading `Setting_instanceId_fkey` FK constraint error at runtime, plus three build-time issues that broke `npm run build` / `docker build`. --- ## Runtime: instance/create always returning 400 ### Root cause 1 — sanitizeUntrustedInput stripping instanceName from body `PROTECTED_INSTANCE_FIELDS` correctly blocks `instanceName` from being overridden via the request body on routes that already receive it through the URL parameter (e.g. `/message/sendText/:instanceName`). However, `POST /instance/create` has no URL parameter — `instanceName` can only come from the body. `sanitizeUntrustedInput()` was silently discarding `instanceName` before it reached the controller, making `instanceData.instanceName` undefined. Prisma 7 treats `name: undefined` as a missing required field and rejects the `instance.create()` call with "Argument name is missing". Fix: for the `/instance/create` route, explicitly pass `instanceName` through the sanitization barrier since it is a required creation input, not an untrusted override attempt. src/api/abstract/abstract.router.ts ### Root cause 2 — saveInstance() swallowing the create error silently The try/catch in `saveInstance()` logged the error but did not rethrow it. `createInstance()` assumed success and continued the flow, eventually reaching `settingsService.create()` → `setSettings()` → `setting.upsert()`. Because the `Instance` row was never committed, the FK constraint on `Setting.instanceId` fired — surfacing as a confusing error deep inside Typebot's `integrationSession.update()` call in the minified bundle. Fix: rethrow after logging so the error propagates to the HTTP layer with the correct message instead of a misleading FK violation. Additionally, `setting.upsert()` is now called immediately after a successful `instance.create()` inside `saveInstance()`. This guarantees the `Setting` row exists before any chatbot integration can attempt to write an `IntegrationSession` for the new instance, eliminating the race window that could still trigger the FK error under concurrent load. src/api/services/monitor.service.ts --- ## Build: npm run build / docker build failing ### tsup.config.ts — define block misplaced inside esbuildOptions callback The `define` object (used to bake `__LICENSE_ENDPOINT_ENCODED__` and `__LICENSE_ENDPOINT_XOR_KEY__` into the bundle at compile time) was nested inside the `esbuildOptions(options) { ... }` callback instead of being a root-level `defineConfig` property. The callback was also left unclosed, producing a malformed config that tsup silently ignored, resulting in a bundle with the licensing defines missing. tsup.config.ts ### evohub.controlplane.router.ts — TypeScript type errors on req.params.id Express types `req.params` properties as `string | undefined`. The two call sites that forwarded `req.params.id` directly to functions expecting `string` caused TypeScript strict-mode errors that aborted the build. src/api/integrations/channel/evohub/evohub.controlplane.router.ts ### Dockerfile — prisma.config.ts missing from final stage `prisma.config.ts` was only copied into the builder stage. The final image did not include it, so `prisma migrate deploy` (executed at container boot via `deploy_database.sh`) failed with: Error: The datasource.url property is required Fix: propagate the file from builder to the final stage. Dockerfile ### .env.example — ?schema= param causes Prisma 7 to embed schema name in generated client Prisma 7 with driver adapters reads the datasource URL at `prisma generate` time and embeds the target schema name into every generated query. With `?schema=evolution_api` in `DATABASE_CONNECTION_URI`, all generated SQL used `"evolution_api"."Table"` prefixes. Because the actual database schema is `public`, every query failed with "table not found". Removing the `?schema=` parameter lets Prisma default to the `public` schema, which matches the migration output and the actual DB layout. The surrounding single-quotes were also removed — Docker Compose includes them literally in the connection string, breaking the Postgres driver. .env.example ### patches/ directory — COPY directive failing on missing path Dockerfile referenced `COPY ./patches ./patches` but the directory was absent from the repository, causing `docker build` to error before any application code was processed. patches/.gitkeep --- .env.example | 2 +- Dockerfile | 2 ++ patches/.gitkeep | 1 + src/api/abstract/abstract.router.ts | 7 ++++++- .../evohub/evohub.controlplane.router.ts | 4 ++-- src/api/services/monitor.service.ts | 17 +++++++++++++++++ tsup.config.ts | 7 ++++--- 7 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 patches/.gitkeep diff --git a/.env.example b/.env.example index c82051095f..f6731270ec 100644 --- a/.env.example +++ b/.env.example @@ -52,7 +52,7 @@ DEL_INSTANCE=false # Provider: postgresql | mysql | psql_bouncer DATABASE_PROVIDER=postgresql -DATABASE_CONNECTION_URI='postgresql://user:pass@evolution-postgres:5432/evolution_db?schema=evolution_api' +DATABASE_CONNECTION_URI=postgresql://user:pass@evolution-postgres:5432/evolution_db # Postgres container settings (used by docker-compose) POSTGRES_DATABASE=evolution_db diff --git a/Dockerfile b/Dockerfile index 81953db4ff..2bfdfb0f5e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,7 @@ COPY ./prisma ./prisma COPY ./manager ./manager COPY ./.env.example ./.env COPY ./runWithProvider.js ./ +COPY ./prisma.config.ts ./ COPY ./Docker ./Docker @@ -65,6 +66,7 @@ COPY --from=builder /evolution/.env ./.env COPY --from=builder /evolution/Docker ./Docker COPY --from=builder /evolution/runWithProvider.js ./runWithProvider.js COPY --from=builder /evolution/tsup.config.ts ./tsup.config.ts +COPY --from=builder /evolution/prisma.config.ts ./prisma.config.ts ENV DOCKER_ENV=true diff --git a/patches/.gitkeep b/patches/.gitkeep new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/patches/.gitkeep @@ -0,0 +1 @@ + diff --git a/src/api/abstract/abstract.router.ts b/src/api/abstract/abstract.router.ts index 3ff633d58b..b991c9cb74 100644 --- a/src/api/abstract/abstract.router.ts +++ b/src/api/abstract/abstract.router.ts @@ -51,7 +51,12 @@ export abstract class RouterBroker { } if (request.originalUrl.includes('/instance/create')) { - Object.assign(instance, sanitizeUntrustedInput(body)); + const sanitized = sanitizeUntrustedInput(body); + // instanceName must come from the body on create — there is no URL param on this route + if (body?.instanceName !== undefined) { + sanitized.instanceName = body.instanceName; + } + Object.assign(instance, sanitized); } Object.assign(ref, body); diff --git a/src/api/integrations/channel/evohub/evohub.controlplane.router.ts b/src/api/integrations/channel/evohub/evohub.controlplane.router.ts index f87b335259..022a821a6a 100644 --- a/src/api/integrations/channel/evohub/evohub.controlplane.router.ts +++ b/src/api/integrations/channel/evohub/evohub.controlplane.router.ts @@ -37,7 +37,7 @@ export class EvoHubControlPlaneRouter extends RouterBroker { }); this.router.get('/evohub/channels/:id', guard, async (req, res) => { - res.json(await evoHubClient.getChannel(req.params.id)); + res.json(await evoHubClient.getChannel(req.params.id as string)); }); this.router.get('/evohub/available-channels', guard, async (_req, res) => { @@ -114,7 +114,7 @@ export class EvoHubControlPlaneRouter extends RouterBroker { }); this.router.post('/evohub/channels/:id/meta-connect', guard, async (req, res) => { - res.json(await evoHubClient.connectToMeta(req.params.id, req.body)); + res.json(await evoHubClient.connectToMeta(req.params.id as string, req.body)); }); } } diff --git a/src/api/services/monitor.service.ts b/src/api/services/monitor.service.ts index f327e4a7c9..f78ccf8308 100644 --- a/src/api/services/monitor.service.ts +++ b/src/api/services/monitor.service.ts @@ -257,8 +257,25 @@ export class WAMonitoringService { businessId: data.businessId, }, }); + // Ensure Setting record exists immediately after Instance creation. + // Prevents FK constraint violations (Setting_instanceId_fkey) in chatbot + // integrations that write to IntegrationSession before setSettings() runs. + await this.prismaRepository.setting.upsert({ + where: { instanceId: data.instanceId }, + update: {}, + create: { + instanceId: data.instanceId, + rejectCall: false, + groupsIgnore: false, + alwaysOnline: false, + readMessages: false, + readStatus: false, + syncFullHistory: false, + }, + }); } catch (error) { this.logger.error(error); + throw error; } } diff --git a/tsup.config.ts b/tsup.config.ts index 23e9208362..7a36420dd1 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -26,6 +26,10 @@ export default defineConfig({ // Apenas o nome exato @prisma/client é bundlado (redirecionado pelo alias para o client // gerado). Subpaths como @prisma/client/runtime/* permanecem externos (node_modules). noExternal: [/^@prisma\/client$/], + define: { + __LICENSE_ENDPOINT_ENCODED__: licenseEndpointEncoded, + __LICENSE_ENDPOINT_XOR_KEY__: licenseEndpointXorKey, + }, esbuildOptions(options) { // platform node: garante o shim de import.meta.url no bundle CJS (client Prisma 7 usa import.meta) options.platform = 'node'; @@ -33,9 +37,6 @@ export default defineConfig({ ...(options.alias ?? {}), '@prisma/client': path.resolve(process.cwd(), 'prisma/generated/client/client.ts'), }; - define: { - __LICENSE_ENDPOINT_ENCODED__: licenseEndpointEncoded, - __LICENSE_ENDPOINT_XOR_KEY__: licenseEndpointXorKey, }, onSuccess: async () => { cpSync('src/utils/translations', 'dist/translations', { recursive: true }); From 45d3122ca998b7d26b5153cb97984509e3289b92 Mon Sep 17 00:00:00 2001 From: Matheus Pastorini Date: Thu, 25 Jun 2026 17:21:53 -0300 Subject: [PATCH 2/2] feat: auto-create database on boot if it does not exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before running `prisma migrate deploy`, the entrypoint now attempts to connect to the database server's admin database (`postgres` / default) and issues `CREATE DATABASE` only when the target database is absent. Motivation: a fresh install pointing at a managed Postgres (RDS, Supabase, self-hosted) fails immediately if the database has not been pre-created. The container logs showed a cryptic migration error rather than an actionable message about a missing database. ### What changed `Docker/scripts/create_database.js` (new) - Parses DATABASE_CONNECTION_URI to extract host, port, credentials, and target database name. - Connects to the admin database (`postgres` for PG, no DB for MySQL). - PostgreSQL: `SELECT 1 FROM pg_database WHERE datname = $1` — creates only when rowcount is 0. - MySQL: `CREATE DATABASE IF NOT EXISTS` (idempotent by design). - Validates the database name against `/^[\w-]+$/` before interpolating it into the DDL to prevent injection. - Non-fatal by design: if the user lacks CREATE DATABASE permission (managed DB with restricted roles, read replica, etc.) the error is logged as a warning and the script exits 0 so `prisma migrate deploy` still runs and produces a clear, actionable error. - Connection timeout set to 10 s to fail fast on unreachable hosts. `Docker/scripts/deploy_database.sh` - Added `node ./Docker/scripts/create_database.js` call before the existing `npm run db:deploy` step. ### Behaviour | Scenario | Result | |---|---| | Database does not exist, user has CREATE DATABASE | DB created, migrations run | | Database already exists | Skipped, migrations run normally | | Database does not exist, no CREATE permission | Warning logged, migration fails with clear Prisma error | | DATABASE_CONNECTION_URI not set | Warning logged, exits 0 | --- Docker/scripts/create_database.js | 112 ++++++++++++++++++++++++++++++ Docker/scripts/deploy_database.sh | 2 + 2 files changed, 114 insertions(+) create mode 100644 Docker/scripts/create_database.js diff --git a/Docker/scripts/create_database.js b/Docker/scripts/create_database.js new file mode 100644 index 0000000000..ca5c77ed3d --- /dev/null +++ b/Docker/scripts/create_database.js @@ -0,0 +1,112 @@ +#!/usr/bin/env node +/** + * Ensures the target database exists before running migrations. + * Connects to the server's default admin database, checks if the target + * database is present, and creates it if not. + * + * Runs as a non-fatal pre-migration step: if the user lacks CREATE DATABASE + * permission (managed DB, read-only replica, etc.) the error is logged as a + * warning and the process exits 0 so the migration attempt still proceeds. + * + * Supported providers: postgresql, psql_bouncer, mysql + */ + +'use strict'; + +const provider = process.env.DATABASE_PROVIDER ?? 'postgresql'; +const uri = process.env.DATABASE_CONNECTION_URI; + +if (!uri) { + console.error('[create_database] DATABASE_CONNECTION_URI is not set — skipping auto-create.'); + process.exit(0); +} + +async function ensurePostgresDatabase() { + const { Pool } = require('pg'); + + const url = new URL(uri); + const dbName = decodeURIComponent(url.pathname.slice(1)); + + if (!dbName) { + console.warn('[create_database] Could not parse database name from URI — skipping.'); + return; + } + + // Connect to the server admin database to check / create the target DB + const adminUrl = new URL(uri); + adminUrl.pathname = '/postgres'; + // Strip any search params that are schema-specific (not relevant for admin conn) + adminUrl.search = ''; + + const pool = new Pool({ connectionString: adminUrl.toString(), connectionTimeoutMillis: 10000 }); + + try { + const result = await pool.query('SELECT 1 FROM pg_database WHERE datname = $1', [dbName]); + + if (result.rowCount === 0) { + console.log(`[create_database] Database "${dbName}" not found. Creating...`); + // Identifier cannot be parameterized — validate it is a plain name first + if (!/^[\w-]+$/.test(dbName)) { + throw new Error(`Refusing to CREATE DATABASE: name "${dbName}" contains unsafe characters.`); + } + await pool.query(`CREATE DATABASE "${dbName}"`); + console.log(`[create_database] Database "${dbName}" created successfully.`); + } else { + console.log(`[create_database] Database "${dbName}" already exists — skipping creation.`); + } + } finally { + await pool.end(); + } +} + +async function ensureMysqlDatabase() { + // mysql2 is a transitive dep via @prisma/adapter-mariadb; attempt to load it + let mysql; + try { + mysql = require('mysql2/promise'); + } catch { + console.warn('[create_database] mysql2 not available — skipping auto-create for MySQL.'); + return; + } + + const url = new URL(uri); + const dbName = decodeURIComponent(url.pathname.slice(1)); + + if (!dbName) { + console.warn('[create_database] Could not parse database name from URI — skipping.'); + return; + } + + if (!/^[\w-]+$/.test(dbName)) { + throw new Error(`Refusing to CREATE DATABASE: name "${dbName}" contains unsafe characters.`); + } + + const connection = await mysql.createConnection({ + host: url.hostname, + port: Number(url.port) || 3306, + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + connectTimeout: 10000, + }); + + try { + await connection.execute(`CREATE DATABASE IF NOT EXISTS \`${dbName}\``); + console.log(`[create_database] Database "${dbName}" ensured.`); + } finally { + await connection.end(); + } +} + +(async () => { + try { + if (provider === 'mysql') { + await ensureMysqlDatabase(); + } else { + await ensurePostgresDatabase(); + } + } catch (err) { + console.warn(`[create_database] Warning: could not auto-create database: ${err.message}`); + console.warn('[create_database] Proceeding — migration will fail with a clear error if the DB is truly missing.'); + process.exit(0); + } +})(); diff --git a/Docker/scripts/deploy_database.sh b/Docker/scripts/deploy_database.sh index fea58ff850..60ae9688cd 100755 --- a/Docker/scripts/deploy_database.sh +++ b/Docker/scripts/deploy_database.sh @@ -8,6 +8,8 @@ fi if [[ "$DATABASE_PROVIDER" == "postgresql" || "$DATABASE_PROVIDER" == "mysql" || "$DATABASE_PROVIDER" == "psql_bouncer" ]]; then export DATABASE_URL + echo "Ensuring database exists for $DATABASE_PROVIDER..." + node ./Docker/scripts/create_database.js echo "Deploying migrations for $DATABASE_PROVIDER" echo "Database URL: $DATABASE_URL" # rm -rf ./prisma/migrations