-
Notifications
You must be signed in to change notification settings - Fork 5.1k
fix(api): add support for WhatsApp Business tokens exceeding 255 char… #2310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(api): add support for WhatsApp Business tokens exceeding 255 char… #2310
Conversation
…acters - Add string truncation validation to prevent database column size errors - Implement full token cache system for WhatsApp Business instances - Auto-save full token to cache on authentication requests - Update instance in memory with full token when detected - Add token full length support in monitor service - Adjust token comparison in auth guard and websocket controller - Fix duplicate token check for full length tokens This fixes the issue where WhatsApp Business API tokens (>255 chars) were truncated in the database, causing authentication failures and API errors. The solution uses a hybrid cache system to store full tokens while keeping the database compatible with the 255 char limit.
Reviewer's GuideAdds hybrid caching support so WhatsApp Business tokens longer than 255 characters are stored and reused from cache while only the first 255 chars remain in the database, plus general defensive truncation for other string fields and adjustments to async instance initialization and token-based authentication paths. Sequence diagram for WhatsApp Business token-based HTTP authentication with hybrid cachesequenceDiagram
actor Client
participant API as ExpressMiddleware
participant AuthGuard as AuthGuard_apikey
participant DB as PrismaInstanceRepo
participant Cache as CacheService
participant Monitor as WAMonitoringService
participant WA as BusinessStartupService
Client->>API: HTTP request with header apikey=fullToken
API->>AuthGuard: Invoke apikey middleware
AuthGuard->>DB: findUnique(name=instanceName)
DB-->>AuthGuard: Instance(name, token=truncatedToken, integration)
AuthGuard->>AuthGuard: keyToCompare = first255(fullToken)
AuthGuard->>AuthGuard: Compare keyToCompare with instance.token
alt Token_matches_and_integration_is_WABA
AuthGuard->>Cache: set(instance:instanceName:fullToken, fullToken, ttl=0)
AuthGuard->>Monitor: Read waInstances[instanceName]
alt Instance_in_memory_and_has_setInstance
AuthGuard->>WA: setInstance(instanceId, instanceName, integration, token=fullToken, number, businessId)
WA->>Cache: set(instance:instanceName:fullToken, fullToken, ttl=0)
WA-->>AuthGuard: fullToken stored and in-memory updated
else No_in_memory_instance
AuthGuard-->>AuthGuard: Skip_in_memory_update
end
AuthGuard-->>API: next()
else Token_does_not_match
AuthGuard-->>API: Respond_401
end
API-->>Client: 200_or_401_response
Sequence diagram for WhatsApp Business instance creation and full token cachingsequenceDiagram
actor Client
participant API as InstanceController
participant Monitor as WAMonitoringService
participant Factory as InstanceFactory
participant WA as BusinessStartupService
participant DB as PrismaInstanceRepo
participant Cache as CacheService
Client->>API: POST /instance/create (token=fullToken)
API->>Monitor: saveInstance(data)
Monitor->>Monitor: truncate(instanceName, hash, other_fields)
Monitor->>DB: instance.create(id, name, token=truncatedHash, other_fields)
DB-->>Monitor: persisted_instance
Monitor-->>API: return
API->>Factory: createInstance(integration=WHATSAPP_BUSINESS)
Factory-->>API: BusinessStartupService_instance
API->>WA: setInstance(instanceId, instanceName, integration, token=fullToken, number, businessId)
WA->>WA: fullToken = fullToken
WA->>Cache: set(instance:instanceName:fullToken, fullToken, ttl=0)
WA-->>API: instance_ready_with_full_token
API->>Monitor: waInstances[instanceName] = WA
API-->>Client: 201 instance_created
Updated class diagram for WhatsApp Business token handling and monitoringclassDiagram
class ChannelStartupService {
- configService
- eventEmitter
- prismaRepository
- chatwootCache
- cache
- instanceId
- instance
- localSettings
+ setInstance(instanceId, instanceName, integration, token, number, businessId, ownerJid)
+ setSettings(data)
}
class BusinessStartupService {
- fullToken : string
+ stateConnection
+ phoneNumber
+ mobile : boolean
+ get token() string
+ setInstance(instanceId, instanceName, integration, token, number, businessId, ownerJid)
+ get connectionStatus()
}
class WAMonitoringService {
- configService
- prismaRepository
- cache
- logger
+ waInstances : Record
+ saveInstance(data)
+ setInstance(instanceId, instanceName, integration, token, number, businessId, ownerJid, connectionStatus)
+ loadInstancesFromRedis()
+ loadInstancesFromDatabasePostgres()
+ loadInstancesFromProvider(instanceId)
}
class InstanceController {
- waMonitor : WAMonitoringService
- prismaRepository
- configService
+ createInstance(instanceData)
}
class AuthService {
- prismaRepository
+ isDuplicateInstanceToken(token) bool
}
class AuthGuardApikey {
- prismaRepository
- cache
- waMonitor : WAMonitoringService
- logger
+ apikey(req, res, next)
}
class WebsocketController {
- prismaRepository
- logger
+ handleConnection(socket, callback)
}
ChannelStartupService <|-- BusinessStartupService
WAMonitoringService --> BusinessStartupService : manages_instances
InstanceController --> WAMonitoringService : uses_for_lifecycle
AuthGuardApikey --> WAMonitoringService : updates_in_memory_instance
AuthGuardApikey --> BusinessStartupService : async_setInstance
AuthService --> PrismaInstanceRepo : queries_tokens
WebsocketController --> PrismaInstanceRepo : queries_tokens
class PrismaInstanceRepo {
+ findUnique(where)
+ findFirst(where)
+ findMany(where)
+ create(data)
}
class CacheService {
+ get(key)
+ set(key, value, ttl)
}
BusinessStartupService --> CacheService : cache_fullToken
WAMonitoringService --> CacheService : load_fullToken_on_boot
AuthGuardApikey --> CacheService : store_fullToken_on_auth
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes - here's some feedback:
- The
truncate()helper is defined inline in multiple services; consider extracting it (and the max-length values) into a shared utility or constants module to avoid duplication and ensure column limits stay consistent. - The logic around
setInstancebeing sometimes async (e.g.,instance as anyandinstanceof Promisechecks) is a bit fragile; it would be clearer to normalize the interface (e.g., always returning a Promise and alwaysawaiting it) instead of relying on runtime type checks. - The hard-coded
255limit for token comparison appears in several places (auth guard, websocket controller, auth service); centralizing this as a single constant tied to the DB schema would reduce the risk of mismatches if the column size changes.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `truncate()` helper is defined inline in multiple services; consider extracting it (and the max-length values) into a shared utility or constants module to avoid duplication and ensure column limits stay consistent.
- The logic around `setInstance` being sometimes async (e.g., `instance as any` and `instanceof Promise` checks) is a bit fragile; it would be clearer to normalize the interface (e.g., always returning a Promise and always `await`ing it) instead of relying on runtime type checks.
- The hard-coded `255` limit for token comparison appears in several places (auth guard, websocket controller, auth service); centralizing this as a single constant tied to the DB schema would reduce the risk of mismatches if the column size changes.
## Individual Comments
### Comment 1
<location> `src/api/services/monitor.service.ts:297-306` </location>
<code_context>
- number: instanceData.number,
- businessId: instanceData.businessId,
- });
+ // Para WhatsApp Business, setInstance é async e precisa ser aguardado
+ if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
+ await (instance as any).setInstance({
</code_context>
<issue_to_address>
**suggestion:** The Promise detection around setInstance can be simplified and made more robust.
You can drop the `instanceof Promise` check and always `await` the result of `setInstance`:
```ts
const maybePromise = (instance as any).setInstance({ ... });
await maybePromise;
```
This also handles non-native thenables and matches the controller’s `await (instance as any).setInstance(...)` pattern.
</issue_to_address>
### Comment 2
<location> `src/api/services/monitor.service.ts:351-354` </location>
<code_context>
return;
}
+ // Para WhatsApp Business, tenta carregar token completo do cache
+ let token = instanceData.token;
+ if (instanceData.integration === Integration.WHATSAPP_BUSINESS) {
+ const cacheKey = `instance:${instanceData.name}:fullToken`;
+ const fullToken = await this.cache.get(cacheKey);
+ if (fullToken) {
</code_context>
<issue_to_address>
**issue (bug_risk):** Cache key uses `instanceData.name` while the rest of this block relies on `instanceName`, which may cause key mismatches.
The cache key here uses `instanceData.name`, while nearby code and other usages build `instance:${instanceName}:fullToken` (with `instanceName` derived from `k.split(':')[2]` or `instance.instanceName`). If `name` and `instanceName` ever differ, tokens written under one key won’t be readable with the other. Please standardize on a single field (likely `instanceName`) for all cache key construction for this token.
</issue_to_address>
### Comment 3
<location> `src/api/integrations/channel/meta/whatsapp.business.service.ts:61-69` </location>
<code_context>
+ }
+
+ // Override setInstance para armazenar/carregar token completo
+ public async setInstance(instance: any) {
+ super.setInstance(instance);
+
+ // Se o token fornecido é maior que 255, é o token completo - armazena imediatamente
+ if (instance.token && instance.token.length > 255) {
+ this.fullToken = instance.token;
+ const cacheKey = `instance:${instance.instanceName}:fullToken`;
+ await this.cache.set(cacheKey, instance.token, 0);
+ this.logger.log(`Stored full token in cache for ${instance.instanceName}`);
+ } else {
+ // Tenta carregar token completo do cache
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider making the async contract of setInstance explicit and consistent with the base class.
Here `setInstance` is now `async` and some callers are awaiting it, but the base `ChannelStartupService.setInstance` is still effectively synchronous. This split contract can be confusing and lead to timing issues around when `fullToken` is available.
I’d suggest either:
1) Making the async contract explicit on both base and override (e.g. `Promise<void>`) so it’s clear this is part of the instance lifecycle, or
2) Clearly documenting that only this subclass performs async cache IO and ensuring all relevant call sites consistently `await` it when they depend on cache state (e.g. `token` getter, connection startup).
Suggested implementation:
```typescript
// Override setInstance para armazenar/carregar token completo
public async setInstance(instance: any): Promise<void> {
// Garante que qualquer lógica assíncrona definida na classe base seja respeitada
await super.setInstance(instance);
// Se o token fornecido é maior que 255, é o token completo - armazena imediatamente
```
Para tornar o contrato assíncrono explícito e consistente com a classe base, também será necessário:
1. Atualizar a assinatura de `setInstance` na classe base (provavelmente `ChannelStartupService` ou similar), por exemplo em `src/api/integrations/channel/...`:
- Mudar de `public setInstance(instance: any) { ... }` para `public async setInstance(instance: any): Promise<void> { ... }`.
- Se o corpo atual é totalmente síncrono, ele pode permanecer igual; apenas a assinatura passa a ser `async`/`Promise<void>`.
2. Verificar todos os locais onde `setInstance(...)` é chamado:
- Para todos os call sites que dependem de estado inicializado por `setInstance` (como acesso a `token` ou início de conexão), garantir que a chamada seja `await this.setInstance(...)` e que o contexto seja assíncrono.
- Se existirem pontos onde `setInstance` é chamado mas o resultado não é relevante (fire-and-forget), decidir explicitamente se:
- deve ser aguardado com `await`, ou
- deve ser chamado sem `await`, mas com comentários deixando claro que a inicialização é assíncrona e não é necessária imediatamente.
3. Atualizar qualquer interface/abstração que declare `setInstance` (por exemplo, uma interface de serviço de canal) para usar a assinatura `setInstance(instance: any): Promise<void>;` para manter o contrato de tipo consistente.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| // Para WhatsApp Business, setInstance é async e precisa ser aguardado | ||
| if (instanceData.integration === Integration.WHATSAPP_BUSINESS) { | ||
| const setInstanceResult = (instance as any).setInstance({ | ||
| instanceId: instanceData.instanceId, | ||
| instanceName: instanceData.instanceName, | ||
| integration: instanceData.integration, | ||
| token: instanceData.token, | ||
| number: instanceData.number, | ||
| businessId: instanceData.businessId, | ||
| ownerJid: instanceData.ownerJid, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: The Promise detection around setInstance can be simplified and made more robust.
You can drop the instanceof Promise check and always await the result of setInstance:
const maybePromise = (instance as any).setInstance({ ... });
await maybePromise;This also handles non-native thenables and matches the controller’s await (instance as any).setInstance(...) pattern.
| // Para WhatsApp Business, tenta carregar token completo do cache | ||
| let token = instanceData.token; | ||
| if (instanceData.integration === Integration.WHATSAPP_BUSINESS) { | ||
| const cacheKey = `instance:${instanceData.name}:fullToken`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (bug_risk): Cache key uses instanceData.name while the rest of this block relies on instanceName, which may cause key mismatches.
The cache key here uses instanceData.name, while nearby code and other usages build instance:${instanceName}:fullToken (with instanceName derived from k.split(':')[2] or instance.instanceName). If name and instanceName ever differ, tokens written under one key won’t be readable with the other. Please standardize on a single field (likely instanceName) for all cache key construction for this token.
| public async setInstance(instance: any) { | ||
| super.setInstance(instance); | ||
|
|
||
| // Se o token fornecido é maior que 255, é o token completo - armazena imediatamente | ||
| if (instance.token && instance.token.length > 255) { | ||
| this.fullToken = instance.token; | ||
| const cacheKey = `instance:${instance.instanceName}:fullToken`; | ||
| await this.cache.set(cacheKey, instance.token, 0); | ||
| this.logger.log(`Stored full token in cache for ${instance.instanceName}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): Consider making the async contract of setInstance explicit and consistent with the base class.
Here setInstance is now async and some callers are awaiting it, but the base ChannelStartupService.setInstance is still effectively synchronous. This split contract can be confusing and lead to timing issues around when fullToken is available.
I’d suggest either:
- Making the async contract explicit on both base and override (e.g.
Promise<void>) so it’s clear this is part of the instance lifecycle, or - Clearly documenting that only this subclass performs async cache IO and ensuring all relevant call sites consistently
awaitit when they depend on cache state (e.g.tokengetter, connection startup).
Suggested implementation:
// Override setInstance para armazenar/carregar token completo
public async setInstance(instance: any): Promise<void> {
// Garante que qualquer lógica assíncrona definida na classe base seja respeitada
await super.setInstance(instance);
// Se o token fornecido é maior que 255, é o token completo - armazena imediatamentePara tornar o contrato assíncrono explícito e consistente com a classe base, também será necessário:
-
Atualizar a assinatura de
setInstancena classe base (provavelmenteChannelStartupServiceou similar), por exemplo emsrc/api/integrations/channel/...:- Mudar de
public setInstance(instance: any) { ... }parapublic async setInstance(instance: any): Promise<void> { ... }. - Se o corpo atual é totalmente síncrono, ele pode permanecer igual; apenas a assinatura passa a ser
async/Promise<void>.
- Mudar de
-
Verificar todos os locais onde
setInstance(...)é chamado:- Para todos os call sites que dependem de estado inicializado por
setInstance(como acesso atokenou início de conexão), garantir que a chamada sejaawait this.setInstance(...)e que o contexto seja assíncrono. - Se existirem pontos onde
setInstanceé chamado mas o resultado não é relevante (fire-and-forget), decidir explicitamente se:- deve ser aguardado com
await, ou - deve ser chamado sem
await, mas com comentários deixando claro que a inicialização é assíncrona e não é necessária imediatamente.
- deve ser aguardado com
- Para todos os call sites que dependem de estado inicializado por
-
Atualizar qualquer interface/abstração que declare
setInstance(por exemplo, uma interface de serviço de canal) para usar a assinaturasetInstance(instance: any): Promise<void>;para manter o contrato de tipo consistente.
Replace template string interpolation in logger calls with object-based logging to prevent format string injection vulnerabilities detected by CodeQL. - Use object-based logging instead of template strings - Prevents external control of format strings in logs - Maintains same logging functionality with better security
…t foreign key constraint error
Correção: Suporte a Tokens do WhatsApp Business com Mais de 255 Caracteres
📋 Resumo
Este PR implementa uma solução completa para suportar tokens de acesso do WhatsApp Business (Meta API) que excedem 255 caracteres. A solução utiliza um sistema de cache híbrido para armazenar o token completo, enquanto o banco de dados continua armazenando apenas os primeiros 255 caracteres para compatibilidade.
🎯 Tipo de Mudança
🔍 Problema Identificado
O token de acesso do WhatsApp Business (Meta API) pode ter mais de 255 caracteres, mas o campo
tokenna tabelaInstancedo banco de dados está limitado a 255 caracteres (VarChar(255)). Isso causava dois problemas:Falha na autenticação: Ao tentar autenticar usando o token da instância no header
apikey, o sistema não reconhecia o token porque ele foi truncado no banco.Erro na API do Meta: Quando o token truncado era usado nas chamadas da API do Meta, ocorria o erro:
Problema após reiniciar a API: Quando a API era reiniciada, o cache local (node-cache) era limpo, fazendo com que o token completo não fosse encontrado e a instância usasse o token truncado, causando falhas nas requisições.
💡 Solução Implementada
A solução utiliza um sistema de cache híbrido para armazenar o token completo, enquanto o banco de dados continua armazenando apenas os primeiros 255 caracteres (para compatibilidade e autenticação).
Componentes da Solução
Cache para Token Completo: O token completo é armazenado no cache (Redis ou Node-cache) com a chave
instance:{instanceName}:fullTokene sem expiração (TTL = 0).Validação e Truncamento: Strings são validadas e truncadas antes de serem salvas no banco de dados para evitar erros de tamanho de coluna.
Autenticação Inteligente: O guard de autenticação compara apenas os primeiros 255 caracteres do token fornecido com o token do banco, mas salva automaticamente o token completo no cache quando detectado.
Atualização Automática: Quando uma requisição é feita com o token completo após reiniciar a API, o sistema automaticamente salva o token no cache e atualiza a instância em memória.
🔧 Arquivos Modificados
1.
src/api/services/channel.service.tsMudanças:
truncate()para truncar stringsmsgCallewavoipToken(limite de 100 caracteres)Motivo: Previne erros de violação de tamanho de coluna no banco de dados.
2.
src/api/services/monitor.service.tsMudanças:
truncate()para truncar stringsinstanceName: 255 caracteres (com validação de campo obrigatório)ownerJid: 100 caracteresprofileName: 100 caracteresprofilePicUrl: 500 caracteresnumber: 100 caracteresintegration: 100 caracterestoken: 255 caracteresclientName: 100 caracteresbusinessId: 100 caracteresloadInstancesFromDatabasePostgres,loadInstancesFromRediseloadInstancesFromProviderpara tentar carregar o token completo do cache para instâncias WhatsApp BusinesssetInstancepara lidar com o método async do BusinessStartupServiceinstanceNamenão seja vazioawaitdesnecessário emget<Database>()Motivo: Garante que dados longos sejam automaticamente ajustados e que o token completo seja carregado do cache quando disponível.
3.
src/api/guards/auth.guard.tsMudanças:
Motivo: Permite autenticação com tokens truncados no banco, mas salva automaticamente o token completo quando fornecido, resolvendo o problema após reiniciar a API.
4.
src/api/controllers/instance.controller.tsMudanças:
setInstancequando a integração é WhatsApp Business (agora é async)setInstanceem vez do hash truncadoMotivo: Garante que o token completo seja armazenado no cache ao criar a instância.
5.
src/api/integrations/channel/meta/whatsapp.business.service.tsMudanças:
fullTokenpara armazenar o token completotokenpara retornar o token completo quando disponívelsetInstance(agora async) para:Motivo: Garante que o serviço use sempre o token completo quando disponível, em vez do token truncado do banco.
6.
src/api/integrations/event/websocket/websocket.controller.tsMudanças:
Motivo: Mantém consistência na autenticação via WebSocket.
7.
src/api/services/auth.service.tsMudanças:
Motivo: Evita falsos positivos na verificação de tokens duplicados.
🔍 Detalhes Técnicos
Função Helper
truncate()Esta função:
nullse o valor fornullouundefinedmaxLengthespecificadoSistema de Cache para Token Completo
Chave do Cache:
instance:{instanceName}:fullTokenTTL: 0 (sem expiração)
Comportamento:
Fluxo de Autenticação
apikey✅ Benefícios
🧪 Como Testar
Cenário 1: Criar Nova Instância com Token Completo
Resultado Esperado: Instância criada com sucesso, token completo salvo no cache.
Cenário 2: Enviar Mensagem Após Reiniciar API
Full token not found in cache for WABA, using truncated tokenResultado Esperado:
Stored full token in cache for WABA from requestUpdated full token in memory for WABAPENDING)Cenário 3: Requisições Subsequentes
Após o cenário 2, faça mais requisições:
Resultado Esperado: Mensagem enviada com sucesso sem necessidade de salvar token novamente.
Cenário 4: Verificar Cache Manualmente (Redis)
redis-cli GET "instance:WABA:fullToken"Resultado Esperado: Deve retornar o token completo.
📝 Notas Importantes
Limitação do Banco: O campo
tokenno banco continua limitado a 255 caracteres. Esta é uma limitação do schema do Prisma e não deve ser alterada sem uma migração adequada.Cache: O token completo é armazenado no cache (Redis ou Node-cache) com a chave
instance:{instanceName}:fullTokene sem expiração (TTL = 0).Compatibilidade: A solução é compatível com instâncias antigas (que não têm token completo no cache), mas elas precisarão fazer pelo menos uma requisição com o token completo após reiniciar a API.
Outros Canais: Esta correção é específica para
WHATSAPP-BUSINESS. Outros canais (Baileys, Evolution) não são afetados.Node-cache vs Redis:
🔄 Migração de Instâncias Existentes
Instâncias criadas antes desta correção não terão o token completo no cache. Duas opções:
Opção 1: Recriar a Instância (Recomendado)
Opção 2: Atualizar Token Manualmente no Cache
Redis:
Node-cache: Não é possível atualizar manualmente. Use a Opção 1 ou faça uma requisição com o token completo.
🐛 Troubleshooting
Problema: Token ainda não funciona após recriar
Solução:
Problema: Erro 401 ao usar token da instância
Solução:
Problema: Token expirado
Solução: Este é um problema diferente - o token do Meta expirou. Gere um novo token no Facebook Developers e recrie a instância.
Problema: "Full token not found in cache" após reiniciar
Solução: Isso é esperado. Faça uma requisição com o token completo e o sistema salvará automaticamente no cache.
📊 Logs de Referência
Logs Esperados ao Carregar Instância
Logs Esperados ao Autenticar com Token Completo
Logs Esperados ao Criar Instância
Logs Esperados ao Carregar Token do Cache
✅ Checklist de Revisão
🔗 Referências
Summary by Sourcery
Handle WhatsApp Business access tokens longer than 255 characters by truncating database fields while storing and using full tokens via cache, and align related authentication and startup flows with this behavior.
Bug Fixes:
Enhancements: