Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 4880265088 chore: additional changes for Google reCAPTCHA verification timeout during Check 2026-03-13 16:20:01 +00:00
Sonarly Claude Code a1d148694e chore: improve monitoring for Google reCAPTCHA verification timeout during Check
**`captcha.guard.ts`** — Added a `Logger` instance and a `warn`-level log when the captcha error indicates provider unreachability (`captcha-provider-unreachable` prefix). This ensures:

1. Network-level captcha failures are logged at `warn` level (not `error`) — they're transient infrastructure issues, not application bugs, so they shouldn't trigger error-level alerts
2. The error code (ETIMEDOUT, ENETUNREACH, etc.) is included in the log message for debugging
3. The existing `MetricsService.incrementCounter` call already captures the error in the `InvalidCaptcha` metric attributes, so the new log complements (not duplicates) the metric — logs give immediate visibility in pod logs, metrics give aggregate dashboard views

Previously, these failures would surface as unhandled `AggregateError` exceptions in Sentry with no application-level context. Now they're captured as structured warn logs + metric attributes, and Sentry will see a `CaptchaException` (which is a handled, expected error type) instead of a raw network error.
2026-03-13 16:19:58 +00:00
Sonarly Claude Code 24fab28025 Google reCAPTCHA verification timeout during CheckUserExists query
https://sonarly.com/issue/4574?type=bug

The twenty-server cannot reach Google's reCAPTCHA verification endpoint (`https://www.google.com/recaptcha/api/siteverify`) from its AWS eu-central-1 pod, causing the `CheckUserExists` GraphQL query to fail with an unhandled `AggregateError [ETIMEDOUT]`.

Fix: **Problem:** `GoogleRecaptchaDriver.validate()` and `TurnstileDriver.validate()` make HTTP POST requests to external captcha providers with no timeout and no error handling. When the provider is unreachable (ETIMEDOUT/ENETUNREACH), the raw `AggregateError` bubbles up through the NestJS guard chain as an unhandled 500 error.

**Fix (3 changes):**

1. **`captcha.module.ts`** — Added `timeout: 10_000` (10 seconds) to both captcha HTTP clients. This bounds the maximum wait time instead of relying on OS-level TCP timeout (~75-120s). This follows the team's existing pattern (webhook job uses `timeout: 5_000`); captcha gets a slightly higher timeout since it blocks user login.

2. **`google-recaptcha.driver.ts`** and **`turnstile.driver.ts`** — Wrapped the HTTP POST in try/catch. Network errors are caught and returned as `{ success: false, error: 'captcha-provider-unreachable: ETIMEDOUT' }` instead of throwing. This uses the existing `CaptchaValidateResult` contract, so the guard's existing failure path handles it correctly — the user sees "Invalid Captcha, please try another device" instead of a raw 500 error.

The error is NOT silently swallowed — it flows through the normal captcha failure path which increments the `InvalidCaptcha` metric counter with the error code in attributes, and then throws a `CaptchaException`.
2026-03-13 16:19:56 +00:00
8 changed files with 131 additions and 96 deletions
@@ -49,31 +49,31 @@ npx create-twenty-app@latest my-app --minimal
Von hier aus können Sie:
```bash filename="Terminal"
# Eine neue Entität zu Ihrer Anwendung hinzufügen (geführt)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Die Funktionsprotokolle Ihrer Anwendung überwachen
# Watch your application's function logs
yarn twenty function:logs
# Eine Funktion anhand ihres Namens ausführen
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Die Pre-Installationsfunktion ausführen
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Die Post-Installationsfunktion ausführen
# Execute the post-install function
yarn twenty function:execute --postInstall
# Die Anwendung für die Verteilung erstellen
# Build the app for distribution
yarn twenty app:build
# Die Anwendung auf npm oder einen Twenty-Server veröffentlichen
# Publish the app to npm or a Twenty server
yarn twenty app:publish
# Die Anwendung aus dem aktuellen Arbeitsbereich deinstallieren
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Hilfe zu Befehlen anzeigen
# Display commands' help
yarn twenty help
```
@@ -1246,23 +1246,23 @@ Hauptpunkte:
Ein minimales End-to-End-Beispiel, das Objekte, Logikfunktionen, Frontend-Komponenten und mehrere Trigger demonstriert, finden Sie [hier](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Erstellen Ihrer App
## Building your app
Sobald Sie Ihre App mit `app:dev` entwickelt haben, verwenden Sie `app:build`, um sie in ein verteilbares Paket zu kompilieren.
Once you've developed your app with `app:dev`, use `app:build` to compile it into a distributable package.
```bash filename="Terminal"
# Die App erstellen (Ausgabe nach .twenty/output/)
# Build the app (output goes to .twenty/output/)
yarn twenty app:build
# Build ausführen und ein Tarball (.tgz) für die Verteilung erstellen
# Build and create a tarball (.tgz) for distribution
yarn twenty app:build --tarball
```
Der Build-Prozess:
The build process:
1. **Parst und validiert das Manifest** — liest alle `defineX()`-Entitäten aus Ihren Quelldateien und validiert die Manifeststruktur.
2. **Kompiliert Logikfunktionen und Front-Komponenten** — bündelt TypeScript-Quellcode in ESM `.mjs`-Dateien mit esbuild.
3. **Erzeugt Checksummen** — berechnet MD5-Hashes für jede erstellte Datei, die im Manifest als `builtHandlerChecksum` / `builtComponentChecksum` gespeichert werden.
1. **Parses and validates the manifest** — reads all `defineX()` entities from your source files and validates the manifest structure.
2. **Compiles logic functions and front components** — bundles TypeScript sources into ESM `.mjs` files using esbuild.
3. **Generates checksums** — computes MD5 hashes for each built file, stored in the manifest as `builtHandlerChecksum` / `builtComponentChecksum`.
4. **Generates the typed API client** — introspects the GraphQL schema and generates typed `CoreApiClient` and `MetadataApiClient` clients.
5. **Runs a TypeScript type check** — runs `tsc --noEmit` to catch type errors before publishing.
6. **Rebuilds with the generated client** — performs a second compilation pass so the generated client types are included.
@@ -49,31 +49,31 @@ npx create-twenty-app@latest my-app --minimal
Da qui puoi:
```bash filename="Terminal"
# Aggiungi una nuova entità alla tua applicazione (guidata)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Monitora i log delle funzioni della tua applicazione
# Watch your application's function logs
yarn twenty function:logs
# Esegui una funzione per nome
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Esegui la funzione di pre-installazione
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Esegui la funzione post-installazione
# Execute the post-install function
yarn twenty function:execute --postInstall
# Compila l'app per la distribuzione
# Build the app for distribution
yarn twenty app:build
# Pubblica l'app su npm o su un server Twenty
# Publish the app to npm or a Twenty server
yarn twenty app:publish
# Disinstalla l'applicazione dallo spazio di lavoro corrente
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Mostra l'aiuto dei comandi
# Display commands' help
yarn twenty help
```
@@ -1246,23 +1246,23 @@ Punti chiave:
Esplora un esempio minimale end-to-end che dimostra oggetti, funzioni logiche, componenti front-end e trigger multipli [qui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Compilazione della tua app
## Building your app
Una volta che hai sviluppato la tua app con `app:dev`, usa `app:build` per compilarla in un pacchetto distribuibile.
Once you've developed your app with `app:dev`, use `app:build` to compile it into a distributable package.
```bash filename="Terminal"
# Compila l'app (l'output va in .twenty/output/)
# Build the app (output goes to .twenty/output/)
yarn twenty app:build
# Compila e crea un tarball (.tgz) per la distribuzione
# Build and create a tarball (.tgz) for distribution
yarn twenty app:build --tarball
```
Il processo di compilazione:
The build process:
1. **Analizza e convalida il manifest** — legge tutte le entità `defineX()` dai tuoi file sorgente e convalida la struttura del manifest.
2. **Compila le funzioni di logica e i componenti front-end** — raggruppa i sorgenti TypeScript in file ESM `.mjs` usando esbuild.
3. **Genera i checksum** — calcola gli hash MD5 per ogni file compilato, memorizzati nel manifest come `builtHandlerChecksum` / `builtComponentChecksum`.
1. **Parses and validates the manifest** — reads all `defineX()` entities from your source files and validates the manifest structure.
2. **Compiles logic functions and front components** — bundles TypeScript sources into ESM `.mjs` files using esbuild.
3. **Generates checksums** — computes MD5 hashes for each built file, stored in the manifest as `builtHandlerChecksum` / `builtComponentChecksum`.
4. **Generates the typed API client** — introspects the GraphQL schema and generates typed `CoreApiClient` and `MetadataApiClient` clients.
5. **Runs a TypeScript type check** — runs `tsc --noEmit` to catch type errors before publishing.
6. **Rebuilds with the generated client** — performs a second compilation pass so the generated client types are included.
@@ -49,31 +49,31 @@ npx create-twenty-app@latest my-app --minimal
A partir daqui você pode:
```bash filename="Terminal"
# Adicionar uma nova entidade à sua aplicação (assistido)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Acompanhar os logs das funções da sua aplicação
# Watch your application's function logs
yarn twenty function:logs
# Executar uma função pelo nome
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Executar a função de pré-instalação
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Executar a função de pós-instalação
# Execute the post-install function
yarn twenty function:execute --postInstall
# Compilar a aplicação para distribuição
# Build the app for distribution
yarn twenty app:build
# Publicar a aplicação no npm ou em um servidor Twenty
# Publish the app to npm or a Twenty server
yarn twenty app:publish
# Desinstalar a aplicação do espaço de trabalho atual
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Exibir a ajuda dos comandos
# Display commands' help
yarn twenty help
```
@@ -1247,23 +1247,23 @@ Pontos-chave:
Explore um exemplo mínimo de ponta a ponta que demonstra objetos, funções de lógica, componentes de front-end e vários gatilhos [aqui](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Compilando seu app
## Building your app
Depois de desenvolver seu app com `app:dev`, use `app:build` para compilá-lo em um pacote distribuível.
Once you've developed your app with `app:dev`, use `app:build` to compile it into a distributable package.
```bash filename="Terminal"
# Compilar o app (a saída vai para .twenty/output/)
# Build the app (output goes to .twenty/output/)
yarn twenty app:build
# Compilar e criar um tarball (.tgz) para distribuição
# Build and create a tarball (.tgz) for distribution
yarn twenty app:build --tarball
```
O processo de build:
The build process:
1. **Analisa e valida o manifesto** — lê todas as entidades `defineX()` dos seus arquivos de código-fonte e valida a estrutura do manifesto.
2. **Compila funções de lógica e componentes de front-end** — empacota o código-fonte TypeScript em arquivos ESM `.mjs` usando o esbuild.
3. **Gera checksums** — calcula hashes MD5 para cada arquivo gerado, armazenados no manifesto como `builtHandlerChecksum` / `builtComponentChecksum`.
1. **Parses and validates the manifest** — reads all `defineX()` entities from your source files and validates the manifest structure.
2. **Compiles logic functions and front components** — bundles TypeScript sources into ESM `.mjs` files using esbuild.
3. **Generates checksums** — computes MD5 hashes for each built file, stored in the manifest as `builtHandlerChecksum` / `builtComponentChecksum`.
4. **Generates the typed API client** — introspects the GraphQL schema and generates typed `CoreApiClient` and `MetadataApiClient` clients.
5. **Runs a TypeScript type check** — runs `tsc --noEmit` to catch type errors before publishing.
6. **Rebuilds with the generated client** — performs a second compilation pass so the generated client types are included.
@@ -49,31 +49,31 @@ npx create-twenty-app@latest my-app --minimal
Отсюда вы можете:
```bash filename="Terminal"
# Добавить новую сущность в ваше приложение (с мастером)
# Add a new entity to your application (guided)
yarn twenty entity:add
# Просматривать логи функций вашего приложения
# Watch your application's function logs
yarn twenty function:logs
# Выполнить функцию по имени
# Execute a function by name
yarn twenty function:execute -n my-function -p '{"name": "test"}'
# Выполнить предустановочную функцию
# Execute the pre-install function
yarn twenty function:execute --preInstall
# Выполнить послеустановочную функцию
# Execute the post-install function
yarn twenty function:execute --postInstall
# Собрать приложение для распространения
# Build the app for distribution
yarn twenty app:build
# Опубликовать приложение в npm или на сервер Twenty
# Publish the app to npm or a Twenty server
yarn twenty app:publish
# Удалить приложение из текущего рабочего пространства
# Uninstall the application from the current workspace
yarn twenty app:uninstall
# Показать справку по командам
# Display commands' help
yarn twenty help
```
@@ -1246,23 +1246,23 @@ uploadFile(
Ознакомьтесь с минимальным сквозным примером, демонстрирующим объекты, логические функции, фронт-компоненты и несколько триггеров, [здесь](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
## Сборка вашего приложения
## Building your app
После того как вы разработали приложение с помощью `app:dev`, используйте `app:build`, чтобы скомпилировать его в распространяемый пакет.
Once you've developed your app with `app:dev`, use `app:build` to compile it into a distributable package.
```bash filename="Terminal"
# Собрать приложение (результат сохраняется в .twenty/output/)
# Build the app (output goes to .twenty/output/)
yarn twenty app:build
# Собрать и создать tarball (.tgz) для распространения
# Build and create a tarball (.tgz) for distribution
yarn twenty app:build --tarball
```
Процесс сборки:
The build process:
1. **Разбирает и проверяет манифест** — читает все сущности `defineX()` из ваших исходных файлов и проверяет структуру манифеста.
2. **Компилирует логические функции и фронтенд-компоненты** — упаковывает исходники TypeScript в ESM-файлы `.mjs` с помощью esbuild.
3. **Генерирует контрольные суммы** — вычисляет хэши MD5 для каждого собранного файла, сохраняемые в манифесте как `builtHandlerChecksum` / `builtComponentChecksum`.
1. **Parses and validates the manifest** — reads all `defineX()` entities from your source files and validates the manifest structure.
2. **Compiles logic functions and front components** — bundles TypeScript sources into ESM `.mjs` files using esbuild.
3. **Generates checksums** — computes MD5 hashes for each built file, stored in the manifest as `builtHandlerChecksum` / `builtComponentChecksum`.
4. **Generates the typed API client** — introspects the GraphQL schema and generates typed `CoreApiClient` and `MetadataApiClient` clients.
5. **Runs a TypeScript type check** — runs `tsc --noEmit` to catch type errors before publishing.
6. **Rebuilds with the generated client** — performs a second compilation pass so the generated client types are included.
@@ -2,6 +2,7 @@ import {
type CanActivate,
type ExecutionContext,
Injectable,
Logger,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
@@ -17,6 +18,8 @@ import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.
@Injectable()
export class CaptchaGuard implements CanActivate {
private readonly logger = new Logger(CaptchaGuard.name);
constructor(
private captchaService: CaptchaService,
private metricsService: MetricsService,
@@ -31,20 +34,26 @@ export class CaptchaGuard implements CanActivate {
if (result.success) {
return true;
} else {
await this.metricsService.incrementCounter({
key: MetricsKeys.InvalidCaptcha,
eventId: token || '',
...(result.error ? { attributes: { error: result.error } } : {}),
});
}
throw new CaptchaException(
'Invalid Captcha, please try another device',
CaptchaExceptionCode.INVALID_CAPTCHA,
{
userFriendlyMessage: msg`Invalid Captcha, please try another device`,
},
if (result.error?.startsWith('captcha-provider-unreachable')) {
this.logger.warn(
`Captcha provider unreachable: ${result.error}`,
);
}
await this.metricsService.incrementCounter({
key: MetricsKeys.InvalidCaptcha,
eventId: token || '',
...(result.error ? { attributes: { error: result.error } } : {}),
});
throw new CaptchaException(
'Invalid Captcha, please try another device',
CaptchaExceptionCode.INVALID_CAPTCHA,
{
userFriendlyMessage: msg`Invalid Captcha, please try another device`,
},
);
}
}
@@ -33,6 +33,7 @@ export class CaptchaModule {
config.options,
secureHttpClientService.getHttpClient({
baseURL: 'https://www.google.com/recaptcha/api/siteverify',
timeout: 10_000,
}),
);
case CaptchaDriverType.TURNSTILE:
@@ -41,6 +42,7 @@ export class CaptchaModule {
secureHttpClientService.getHttpClient({
baseURL:
'https://challenges.cloudflare.com/turnstile/v0/siteverify',
timeout: 10_000,
}),
);
default:
@@ -27,14 +27,26 @@ export class GoogleRecaptchaDriver implements CaptchaDriver {
response: token,
});
const response = await this.httpService.post('', formData);
const responseData = response.data as CaptchaServerResponse;
try {
const response = await this.httpService.post('', formData);
const responseData = response.data as CaptchaServerResponse;
return {
success: responseData.success,
...(!responseData.success && {
error: responseData['error-codes']?.[0] ?? 'unknown-error',
}),
};
return {
success: responseData.success,
...(!responseData.success && {
error: responseData['error-codes']?.[0] ?? 'unknown-error',
}),
};
} catch (error: unknown) {
const errorCode =
error instanceof Error && 'code' in error
? String(error.code)
: 'unknown-network-error';
return {
success: false,
error: `captcha-provider-unreachable: ${errorCode}`,
};
}
}
}
@@ -26,15 +26,27 @@ export class TurnstileDriver implements CaptchaDriver {
secret: this.secretKey,
response: token,
});
const response = await this.httpService.post('', formData);
const responseData = response.data as CaptchaServerResponse;
try {
const response = await this.httpService.post('', formData);
const responseData = response.data as CaptchaServerResponse;
return {
success: responseData.success,
...(!responseData.success && {
error: responseData['error-codes']?.[0] ?? 'unknown-error',
}),
};
return {
success: responseData.success,
...(!responseData.success && {
error: responseData['error-codes']?.[0] ?? 'unknown-error',
}),
};
} catch (error: unknown) {
const errorCode =
error instanceof Error && 'code' in error
? String(error.code)
: 'unknown-network-error';
return {
success: false,
error: `captcha-provider-unreachable: ${errorCode}`,
};
}
}
}