@omnizapi/connect
Version:
Omnizapi SDK for browser applications
321 lines (231 loc) • 33 kB
Markdown
# @omnizapi/connect
SDK Omnizapi para conectar canais Meta WhatsApp, Messenger, Instagram, Telegram e Z-API WhatsApp em aplicacoes web. Coleta o resultado da autorizacao; seu backend valida e finaliza a conexao. Nao envie secret keys ao frontend.
Versao atual: **1.1.1**. As mudancas desta versao estao no [changelog](https://unpkg.com/browse/@omnizapi/connect@1.1.1/CHANGELOG.md), disponivel com o pacote publicado.
## Instalacao e importacao
eu
```bash
npm install @omnizapi/connect
```
```ts
import Omnizapi, { Environment, OmnizapiError } from "@omnizapi/connect";
// Alternativa: import { Omnizapi } from "@omnizapi/connect";
import type {
OmnizapiClient,
OmnizapiClientProps,
ConnectionOptions,
ConnectionResult,
ConnectionSuccess,
ConnectionFailure,
ConnectionFailureReason,
OmnizapiErrorCode,
ChannelType,
} from "@omnizapi/connect";
```
CommonJS: `const { Omnizapi } = require("@omnizapi/connect")`.
No navegador, o bundle expoe o global `Omnizapi`:
```html
<script src="https://unpkg.com/@omnizapi/connect@1.1.1/dist/omnizapi.min.js"></script>
```
O CDN usa uma versao fixa; homologue sua integracao antes do deploy. Imports ESM/CommonJS e `newClient` sao seguros em SSR; `connect` exige navegador e rejeita com `BROWSER_REQUIRED` no servidor. O bundle de script e exclusivo do navegador.
## Uso
Crie/reutilize o cliente com `Omnizapi.newClient({ publicKey: "sua-public-key" })` dentro do `try` do handler de clique, para que `INVALID_CONFIGURATION` tambem seja tratado pela UI.
Dispare `client.connect({ channelId })` diretamente no clique, sem outro `await` antes, nunca durante renderizacao, montagem ou importacao. Os exemplos completos incluem UI, `try/catch/finally`, tratamento de `reason` e envio ao backend:
| Exemplo | Uso |
| --- | --- |
| [TypeScript basico](https://unpkg.com/browse/@omnizapi/connect@1.1.1/examples/basic.ts) | Botao e mensagem de status no DOM |
| [HTML com CDN](https://unpkg.com/browse/@omnizapi/connect@1.1.1/examples/vanilla.html) | Pagina sem bundler |
| [React / Next.js](https://unpkg.com/browse/@omnizapi/connect@1.1.1/examples/react.tsx) | `'use client'`, UI de conexao por componente |
| [Angular](https://unpkg.com/browse/@omnizapi/connect@1.1.1/examples/angular.ts) | Componente com guarda de navegador para SSR |
| [Vue](https://unpkg.com/browse/@omnizapi/connect@1.1.1/examples/vue.vue) | SFC com guarda de navegador para SSR |
| [Opcoes](https://unpkg.com/browse/@omnizapi/connect@1.1.1/examples/with-options.ts) | SANDBOX, URLs explicitas, logo, fluxo padrao/direto e timeout |
| [Erros](https://unpkg.com/browse/@omnizapi/connect@1.1.1/examples/error-handling.ts) | Mensagens para todos os motivos e codigos tipados |
Os links abrem o codigo-fonte dos exemplos da versao 1.1.1, incluido no pacote npm. Nao sao demos hospedadas: adapte as chaves, o canal e o backend na sua aplicacao.
Os arquivos `examples/error-handling.ts` e `examples/react.tsx` demonstram `AbortSignal`. No React, o cliente fica em `useRef`, e so e criado no `try` do clique; o cleanup de `useEffect` aborta a tentativa/requisicao pendente sem atualizar a UI desmontada. O exemplo de erros usa `Record<OmnizapiErrorCode, string>` exaustivo e permite cancelar apenas a autorizacao, nao a finalizacao no backend. Nao ha botao para limpar a quarentena nativa.
Os exemplos usam `/api/connect-channel` como rota **da sua aplicacao**, nao fornecida por este pacote. Ela deve autenticar a sessao, validar o resultado e responder com HTTP 2xx somente quando a finalizacao tiver sido confirmada. Os exemplos aguardam `fetch` e verificam `response.ok` antes de exibir sucesso. Adapte a autenticacao e a protecao CSRF ao seu backend. Nao registre resultados, tokens, codigos OAuth ou erros brutos que possam conter URLs sensiveis.
## API
### `Omnizapi.newClient(props): OmnizapiClient`
`OmnizapiClientProps`:
| Campo | Tipo | Descricao |
| --- | --- | --- |
| `publicKey` | `string` | Obrigatorio; chave publica Omnizapi |
| `logo` | `string` | URL opcional da logo no popup proxy |
| `environment` | `Environment` | Padrao `Environment.PRODUCTION` |
| `apiUrl` | `string` | Sobrescreve a URL base da API |
| `frontUrl` | `string` | Sobrescreve a URL base do frontend de conexao |
| Ambiente | API padrao | Frontend padrao |
| --- | --- | --- |
| `PRODUCTION` | `https://api.omni.z-api.io` | `https://app.omni.z-api.io` |
| `STAGING` | `https://api.staging.omni.z-api.io` | `https://app.staging.omni.z-api.io` |
| `SANDBOX` | Exige URL configurada | Exige URL configurada |
Precedencia por URL: **props > `OMNIZAPI_API_URL` / `OMNIZAPI_FRONT_URL` no build do SDK > padroes do ambiente**. Variaveis do processo da aplicacao nao reconfiguram um bundle ja compilado; para isso use props. SANDBOX exige ambas as URLs, via props ou build. URLs base devem ser HTTP(S), sem credenciais, query ou fragmento; use HTTPS em producao. Configuracao invalida pode lancar `OmnizapiError` com `INVALID_CONFIGURATION` na criacao do cliente.
### `client.connect(options): Promise<ConnectionResult>`
`ConnectionOptions`:
| Campo | Tipo | Descricao |
| --- | --- | --- |
| `channelId` | `string` | Obrigatorio; canal a conectar |
| `direct` | `boolean` | Solicita o Meta WhatsApp Embedded Signup nativo na propria pagina, sem o popup proxy. Com `direct` o SDK **nao reserva** janela no clique, porque o dialogo da Meta abre a sua e navegadores concedem um popup por clique. Use so em pagina onde o clique e dedicado a conexao (o frontend de conexao faz isso em `/oauth/proxy-cloud-api`); em outros canais, ou quando a pagina nao pode rodar o nativo, a janela do proxy e aberta na navegacao e pode ser bloqueada. Sem `direct`, todo Meta WhatsApp usa a janela reservada, inclusive com `appCustom` e na origem do frontend de conexao |
| `getQRCode` | `() => Promise<{ connected?: boolean; value?: string }>` | **Obrigatorio para `Z_API_WHATSAPP`**: devolve o QR do canal, buscado no seu backend; veja [QR code do WhatsApp Web](#qr-code-do-whatsapp-web). Sem ele, `connect` rejeita com `INVALID_OPTIONS` antes de a janela navegar, e a janela reservada no clique e fechada. Ignorado nos outros canais |
| `timeoutMs` | `number` | Prazo total positivo e finito em ms, ate `2147483647`; padrao **600000 (10 minutos)** |
| `signal` | `AbortSignal` | Opcional; permite interromper a tentativa via `AbortController` |
HTTP e carregamento do SDK Facebook tem limite de **30 segundos**, respeitando o prazo total restante da tentativa. O POST ao seu backend nao faz parte desse prazo e precisa de politica propria de timeout.
Ha **uma tentativa ativa por pagina**, inclusive entre clientes diferentes. Outra chamada concorrente rejeita com `CONNECTION_IN_PROGRESS`; nao implemente retry automatico para abrir popups.
O SDK reserva um popup `about:blank` durante o clique, para qualquer canal sem `direct`, antes de aguardar a configuracao do canal; apos validar a resposta, navega a mesma janela. A janela provisoria e fechada ao encerrar a tentativa. Com `direct` nao ha reserva: o dialogo nativo da Meta usa o popup que o clique concede. Fechar a janela reservada antes da navegacao nao cancela a tentativa: o fechamento so resulta em `popup_closed` quando o fluxo ja depende da janela (apos a navegacao) ou quando ela e encontrada fechada na hora de navegar. Ainda trate `POPUP_BLOCKED` e permita nova tentativa por clique. A reserva nao transfere o gesto ao `FB.login`: o fluxo nativo, apos carregar a configuracao/SDK Meta, ainda exige homologacao externa com navegadores reais.
**Cancelamento:** passe um sinal novo por tentativa. Se o sinal ja estiver abortado ou for abortado enquanto `connect` estiver pendente, a Promise rejeita com `OmnizapiError.code === "ABORTED"`, nao resolve com `reason: "cancelled"`. O SDK encerra requisicoes pendentes, timers e listeners da tentativa, fecha o popup que controla e restaura seus globais temporarios sem sobrescrever alteracoes do host, nem expor `signal.reason`. Scripts/callbacks de terceiros nao sao removidos; a observacao de um script Meta externo compartilhado pode continuar ate carregar, falhar ou atingir o limite de 30 segundos.
Abort nao desfaz uma autorizacao na Meta, nao fecha necessariamente o dialogo nativo e nao remove a quarentena de um login nativo ainda sem callback. Depois que `connect` termina, abortar esse sinal nao cancela automaticamente seu POST; o React passa o sinal tambem ao `fetch`, mas nem isso desfaz operacoes ja recebidas pelo servidor. Confira o estado no backend antes de repetir uma finalizacao sem confirmacao.
O retorno e uma uniao discriminada por `success`, nao uma confirmacao de persistencia no seu backend:
| Tipo | Campos |
| --- | --- |
| `ConnectionSuccess` | `success: true`, `channelId: string`, `type: ChannelType`, `coexistence: boolean`; `code`, `wabaId`, `phoneId`, `redirectUri`, `botToken`, `pageId`, `accessToken`: `string \| null` |
| `ConnectionFailure` | `success: false`, `reason: ConnectionFailureReason`, `channelId: string`, `type: ChannelType \| null`; `code`, `wabaId`, `phoneId`: `null` |
`ChannelType`: `META_MESSENGER`, `META_INSTAGRAM`, `META_WHATSAPP`, `BOT_TELEGRAM`, `Z_API_WHATSAPP`. Nem todos os canais retornam as mesmas credenciais. O resultado de sucesso inclui `channelId`: envie-o junto ao backend e confira sua vinculacao a sessao/tenant no servidor.
Resultados negativos resolvem a Promise e nao significam sempre cancelamento:
| `reason` | Significado |
| --- | --- |
| `cancelled` | Fluxo cancelado |
| `popup_closed` | Popup fechado antes da conclusao |
| `not_completed` | Fluxo nao concluido |
| `invalid_channel` | ID de canal vazio ou canal rejeitado pelo fluxo |
| `origin_not_allowed` | Origem da aplicacao nao autorizada para o canal (veja [Origens liberadas do canal](#origens-liberadas-do-canal)) |
| `unsupported_channel` | Tipo de canal nao suportado |
Uma pagina servida com `Cross-Origin-Opener-Policy: same-origin` faz o navegador cortar a referencia da janela popup; o SDK reporta `popup_closed` (ou `POPUP_BLOCKED`), indistinguivel de um fechamento pelo usuario. Nao ha como diferenciar os dois casos via JavaScript: permita nova tentativa por clique e, se possivel, ajuste a politica da pagina de conexao.
Falhas tecnicas rejeitam com `OmnizapiError`; use `instanceof OmnizapiError` e `error.code`, nao o texto da mensagem:
| Codigo | Significado |
| --- | --- |
| `BROWSER_REQUIRED` | Operacao exige navegador |
| `CONNECTION_IN_PROGRESS` | Tentativa ativa nesta pagina ou login Meta nativo anterior incompleto; veja os limites abaixo |
| `INVALID_CONFIGURATION` | Props, ambiente ou APIs de navegador invalidos; conflitos de app/inicializacao do SDK Meta degradam para o fluxo proxy, sem rejeitar |
| `INVALID_OPTIONS` | Opcoes da operacao invalidas, como `channelId` com tipo incorreto, timeout ou sinal invalido, ou `Z_API_WHATSAPP` sem `getQRCode` |
| `NETWORK_ERROR` | Falha de transporte |
| `HTTP_ERROR` | Resposta HTTP sem sucesso |
| `INVALID_RESPONSE` | Resposta da API invalida |
| `POPUP_BLOCKED` | Navegador impediu a abertura do popup |
| `TIMEOUT` | Prazo de operacao excedido |
| `ABORTED` | Tentativa interrompida pelo `AbortSignal` da aplicacao, com mensagem segura |
| `SDK_LOAD_FAILED` | Falha ao carregar/inicializar SDK do provedor |
| `PROVIDER_ERROR` | Erro reportado pelo provedor |
### `client.getListMessengerPages(channelId, code, signal?): Promise<unknown>`
Consulta as paginas Messenger usando o codigo OAuth e o canal. Valide o retorno antes de acessa-lo: o schema externo ainda nao foi confirmado, portanto o SDK nao publica DTO de pagina. Nao use cast para inventar esse contrato, nem registre o codigo ou a resposta. A chamada tambem pode rejeitar com erro tecnico e tem timeout HTTP de 30 segundos. O terceiro argumento aceita um `AbortSignal` opcional; um sinal abortado (antes ou durante a chamada) rejeita com `OmnizapiError.code === "ABORTED"`, e um sinal invalido rejeita com `INVALID_OPTIONS`.
## QR code do WhatsApp Web
Para `Z_API_WHATSAPP`, a janela de conexao da plataforma pede o QR a sua pagina, e o SDK chama o seu `getQRCode`. A busca do QR usa a **Secret Key**, entao acontece no seu backend; a chave nunca vai para o navegador.
No navegador:
```js
const result = await client.connect({
channelId,
getQRCode: async () => {
const response = await fetch(`/api/omni/channels/${encodeURIComponent(channelId)}/qr-code`, { credentials: "include" });
const body = await response.json();
// Recusa com status e mensagem: a janela mostra a mensagem quando o status e 4xx.
if (!response.ok) throw { status: response.status, message: body.message ?? null };
// Sem value e sem connected: true, a janela entende que o QR ainda nao esta pronto.
return { connected: body.connected === true, value: body.qrcode ?? undefined };
},
});
```
No seu backend, confira a sessao do usuario e se o canal pertence ao tenant dele **antes** de chamar a API. Um endpoint que entrega o QR de qualquer canal permite que alguem ligue o proprio numero a um canal alheio.
```js
app.get("/api/omni/channels/:id/qr-code", requireSessionAndChannelOwnership, async (req, res) => {
const response = await fetch(
`https://api.omni.z-api.io/v1/channels/${encodeURIComponent(req.params.id)}/zapi/qr-code/image`,
{ headers: { Authorization: `Bearer ${process.env.OMNI_SECRET_KEY}` } },
);
const body = await response.json();
if (!response.ok) {
// O erro da API vem em error.errors[0]; repasse um texto, e nao o objeto.
return res.status(response.status).json({ message: body?.error?.errors?.[0]?.description ?? null });
}
res.json({ qrcode: body.qrcode, connected: body.connected });
});
```
Como o SDK le a recusa do `getQRCode`:
- `status`: `status` ou `response.status`, inteiro entre 100 e 599;
- `message`: `response.data.message`, `response.data.error` ou `response.data.detail`; sem `response`, o proprio `message`. O texto tecnico do axios ("Request failed with status code 422") nao e usado quando ha `response`. Ate 300 caracteres;
- a janela mostra a mensagem nas recusas 4xx; as demais falhas contam como passageiras, e tres seguidas mostram o erro de rede.
A janela manda o pedido para as origens exatas liberadas do canal (menu Seguranca do painel) e, quando a lista tem curinga, para a origem do `referrer` se ela casar com alguma entrada (regra em [Origens liberadas do canal](#origens-liberadas-do-canal)); nunca para a entrada com `*` nem para `*`. Se nao conseguir ler as origens, usa a origem do `referrer`. Com `Referrer-Policy: no-referrer` na sua pagina e a sua origem coberta so por curinga (ou as origens ilegiveis), a janela nao tem para onde mandar o pedido, e a conexao termina em erro de rede.
## Origens liberadas do canal
O `connect` so segue quando a origem da pagina (`window.location.origin`) e a do `frontUrl` ou casa com alguma entrada da lista de dominios do canal (menu Seguranca do painel, `allowOrigins` do `sdk-info`). Cada entrada e lida assim:
| Entrada | Casa com | Nao casa com |
| --- | --- | --- |
| `https://app.cliente.com` | so `https://app.cliente.com` (caminho ignorado; `:443` e o mesmo que sem porta) | `https://cliente.com`, `https://x.app.cliente.com`, `http://app.cliente.com`, `https://app.cliente.com:8443` |
| `https://*.cliente.com` | qualquer subdominio, de qualquer profundidade, com o mesmo esquema e a mesma porta: `https://app.cliente.com`, `https://a.b.cliente.com` | o proprio `https://cliente.com`, `https://evilcliente.com`, `https://cliente.com.evil.com`, `http://app.cliente.com`, `https://app.cliente.com:8443` |
| `cliente.com`, `*.cliente.com` (sem esquema, como a tela sugere) | o mesmo que `https://cliente.com` e `https://*.cliente.com` | qualquer origem `http://`; para `http` (ex.: `http://localhost:3000`) escreva o esquema |
- O host e comparado sem diferenciar maiusculas de minusculas.
- O `*` so vale como primeiro rotulo inteiro, seguido de pelo menos dois rotulos. `*`, `*.com`, `a*.cliente.com`, `app.*.cliente.com` e `*.*.cliente.com` nao casam com nada.
- O `*` so e interpretado na autoridade; no caminho, na query ou no fragmento ele e ignorado junto com o resto (`https://app.cliente.com/*` vale como `https://app.cliente.com`).
- So `http` e `https` sao aceitos (`ftp://`, `ws://`, `javascript:` nao casam com nada); entrada com `@` na autoridade (`https://x@app.cliente.com`, `https://@app.cliente.com`) ou que nao e URL nao casa com nada. Uma entrada invalida nao invalida as outras.
- Limite conhecido: o SDK nao conhece a lista de sufixos publicos. `*.com.br` e aceito e libera qualquer site `.com.br`; o mesmo vale para sufixos onde qualquer pessoa cria um subdominio, como `*.vercel.app`, `*.github.io`, `*.herokuapp.com`, `*.netlify.app` ou `*.pages.dev`: liberar um deles libera o site de qualquer pessoa nessa hospedagem. Cadastre sempre o seu dominio (`*.cliente.com.br`) ou a origem exata do seu projeto (`https://meu-projeto.vercel.app`).
**Instagram e Messenger no seu site exigem a 1.1.1.** A pagina de retorno do OAuth so responde para fora do painel quando o `state` traz a `pk` e a origem do `currentUrl` casa com a lista acima. Com a 1.1.0 ou anterior (sem `pk` no `state`), ou com a lista ilegivel ou vazia, ela nao responde e a tentativa termina por fechamento da janela ou timeout. Dentro do proprio painel nada muda.
Ate a 1.1.0 a comparacao era por origem exata: entrada com `*` nunca casava e entrada sem esquema era descartada, e a tentativa terminava em `origin_not_allowed`.
## Protocolo de popup
Este contrato exige deploy coordenado do frontend/proxy e backend externos; nenhum desses servicos e alterado neste repositorio. Nao ha fallback para mensagens sem os identificadores exigidos.
1. O SDK gera `attemptId` por tentativa. Popups abertos diretamente no `frontUrl` recebem `attemptId` na query; as rotas `/connect/telegram` e `/connect/whatsapp-web` tambem recebem `pk` (a chave publica) na query, e `/connect/whatsapp-web` recebe `qr=message`, permitindo ao frontend consultar as origens autorizadas do canal e restringir o `targetOrigin` da resposta. No OAuth Messenger/Instagram, o `state` JSON e `{ currentUrl, attemptId, channelId, pk }`; a `pk` (chave publica, a mesma do `sdk-info`) existe desde a 1.1.1 e permite a pagina de retorno ler as origens liberadas do canal e so responder ao `currentUrl` que casar com elas; o proxy deve preserva-lo e vincula-lo a sessao autorizada, ao canal e ao tenant. `state` e query nao sao prova de autorizacao por si so.
2. A pagina final deve estar na origem exata de `frontUrl` e emitir da **propria janela popup** aberta pelo SDK, nao de iframe, outra janela ou backend. Ela responde ao `window.opener` usando `postMessage` com `type: 'after-connect'`, `attemptId`, `channelId`, `success` e as credenciais aplicaveis. `attemptId` e `channelId` devem corresponder exatamente a tentativa ativa.
3. O SDK verifica a origem exata do frontend, `event.source` igual ao popup ativo e os dois identificadores. O emissor usa `targetOrigin` igual a origem exata da aplicacao autorizada, nunca `*` e nunca uma origem derivada de query/state nao validados. Quando a aplicacao esta liberada por curinga (`https://*.cliente.com`), a origem exata e a do `referrer` da janela, aceita so se casar com a entrada.
4. Sucesso dos proxies Meta exige `code`; Telegram exige `botToken`. `Z_API_WHATSAPP` pode retornar sucesso autenticado sem credenciais, mas o backend deve confirmar a conexao real para aquele canal/tenant. Para negativo, envie `success: false` e o `reason` tipado aplicavel, sem credenciais.
5. Com `qr=message`, a pagina `/connect/whatsapp-web` pede o QR ao `window.opener` com `{ type: 'qr-code-request', attemptId, channelId, requestId }`, um pedido por vez. O SDK confere a origem exata do frontend, `event.source` igual ao popup ativo, os dois identificadores e um `requestId` em texto nao vazio de ate 64 caracteres; pedido que nao confere e ignorado. Com uma chamada ao `getQRCode` em andamento, o pedido novo recebe o resultado dela, sem chamar de novo. A resposta vai so para a origem exata do frontend: `{ type: 'qr-code-response', attemptId, channelId, requestId, connected, value }`, ou `{ ..., error: { status, message } }` quando o `getQRCode` rejeita ou devolve algo invalido. A pagina confere `event.source` igual ao `window.opener`, a origem e os tres identificadores.
**Privacidade de `state`: `currentUrl` continua sendo `window.location.href` COMPLETO, incluindo caminho, query e fragmento.** Esta rodada nao reduz esse valor a origem: a compatibilidade dos proxies com uma URL minimizada ainda e desconhecida. Tokens ou outros dados sensiveis na query/fragmento da pagina podem assim ser copiados para o `state` OAuth e expostos ao provedor, proxy, historico ou logs de URLs. Evite iniciar a conexao em paginas com esses dados e nao registre `state` nem URLs completas. Fica pendente verificar com os responsaveis pelos proxies se aceitam apenas a origem antes de propor qualquer mudanca coordenada desse contrato.
Mensagens com origem, janela, identificadores ou formato incorretos sao ignoradas; a tentativa continua aguardando conclusao, fechamento ou timeout. Uma mensagem de sucesso sem a credencial exigida resulta em `not_completed`.
Trecho ilustrativo **no frontend externo**, apos o servidor validar a sessao e a autorizacao da tentativa. `authorizedSession` representa dados validados pelo servidor, nao um DTO deste SDK; nao a construa lendo parametros da URL sem validacao:
```js
const { applicationOrigin, attemptId, channelId } = authorizedSession;
// applicationOrigin e uma origem exata previamente autorizada para esta sessao.
// code foi validado para o fluxo Meta desta mesma tentativa.
if (window.opener) {
window.opener.postMessage(
{ type: "after-connect", attemptId, channelId, success: true, code },
applicationOrigin,
);
}
```
O Meta Embedded Signup **nativo** nao fornece `attemptId` nem uma referencia customizada da janela emissora (`event.source`) para correlacao. Ele aceita eventos `WA_EMBEDDED_SIGNUP` de qualquer origem HTTPS em `facebook.com` ou subdominio (www, web, business), como a Meta orienta, e o callback `FB.login`. Os eventos `FINISH`, `FINISH_ONLY_WABA` e `FINISH_WHATSAPP_BUSINESS_APP_ONBOARDING` concluem a conexao; `CANCEL` e `ERROR` a encerram. Nao se pode garantir o isolamento de eventos nativos tardios entre tentativas; essas verificacoes nao sao garantia absoluta de seguranca.
Um login nativo fica em quarentena ate a Meta responder o callback `FB.login` (com codigo, cancelamento ou erro) ou ate a tentativa ter sucesso. O callback, inclusive tardio, libera apenas a quarentena da tentativa a que pertence; nao libera a de outra tentativa. Fechar/cancelar o dialogo ou receber um evento de cancelamento nao garante que esse callback ja tenha chegado. Se um login terminar por timeout, abort ou cancelamento **sem callback**, novas tentativas nativas continuam em `CONNECTION_IN_PROGRESS`: aguarde o callback ou **recarregue a pagina** para tentar outro login nativo. Um callback recebido de cancelamento nao exige reload. Um `FB.login` que falha ao iniciar (excecao sincrona, sem abrir dialogo e sem callback futuro) libera a quarentena imediatamente, sem exigir reload. Essa restricao reduz o risco de atribuir a uma nova tentativa a resposta tardia de um dialogo anterior; nao se aplica aos popups proxy.
Durante uma tentativa, apenas `__omnizapiPublicKey`, `__omnizapiCurrentChannel` e `__omnizapiGetQrCode` sao publicados temporariamente no `window` da aplicacao, e removidos ao fim. So uma pagina na mesma origem consegue le-los: e o caminho da pagina de conexao anterior a `qr=message`, mantido para o painel da plataforma. De outra origem, o QR viaja por mensagem (item 5 do protocolo), porque uma funcao no `window` da aplicacao nao atravessa origens nem e serializada por `postMessage`.
## CSP da pagina que roda o login nativo
O `FB.login` conclui por um iframe oculto de `staticxx.facebook.com` na propria pagina. Se a sua pagina tem Content-Security-Policy e usa `direct: true`, libere:
```
script-src https://connect.facebook.net
frame-src https://www.facebook.com https://web.facebook.com https://business.facebook.com https://staticxx.facebook.com
```
Sem o `frame-src` acima, o dialogo da Meta abre, o usuario conclui, e o dialogo fica preso em `dialog/oauth/business/cancel` sem devolver o resultado. Sem `direct`, o login roda no frontend de conexao da plataforma e a sua pagina nao precisa de nada disso.
## App da Meta proprio (`appCustom`)
Por padrao o canal usa o app da Meta da plataforma, e a Meta so enxerga o `frontUrl` (`app.hubmessage.io` ou `app.omni.z-api.io`): o login do WhatsApp e o retorno do OAuth do Messenger e do Instagram acontecem na janela da plataforma. Nesse caso o seu dominio nao precisa de nada na Meta, so na allowlist de origens do canal.
Quando o canal tem app proprio (`appCustom` no `sdk-info`), o login do WhatsApp Meta (Embedded Signup) continua na janela da plataforma (`/oauth/proxy-cloud-api`), mas com o **seu** `appId` e `configId`, e o OAuth do Messenger e do Instagram tambem volta para a plataforma. Entao o **seu** app da Meta precisa conhecer o dominio da plataforma e, se voce usar `direct` na sua pagina, tambem o seu:
| Onde no app da Meta | O que cadastrar |
| --- | --- |
| Facebook Login > Configuracoes > Dominios permitidos para o SDK do JavaScript | O `frontUrl` da plataforma (ex.: `https://app.omni.z-api.io`) e, se voce usar `direct`, todas as origens onde o seu site chama `connect` |
| Facebook Login > Configuracoes > URIs de redirecionamento do OAuth validos | `${frontUrl}/oauth/proxy-messenger` e `${frontUrl}/oauth/proxy-instagram` da marca que voce usa (ex.: `https://app.omni.z-api.io/oauth/proxy-messenger`) |
| Configuracoes basicas > Dominios do app | O dominio da plataforma e, com `direct`, o do seu site |
| WhatsApp > Embedded Signup | A configuracao cujo id e o `configId` cadastrado na plataforma; o app precisa ter os produtos WhatsApp, Messenger e Instagram habilitados conforme os canais que vai conectar |
Sem o dominio do seu site nos dominios do SDK, a Meta pode recusar o `FB.login`. `PROVIDER_ERROR` depende de a Meta reportar um erro ao SDK; nao e possivel garantir esse codigo para toda falha de dominio. Sem os redirects corretos, o OAuth do Messenger e do Instagram pode ser bloqueado pela Meta. Sem a origem do seu site na allowlist do canal, o SDK resolve com `origin_not_allowed` antes de navegar para a autorizacao; uma janela provisoria ja pode ter sido reservada e sera fechada.
**Inicializacao Meta:** o SDK inicializa uma vez e reutiliza o mesmo `appId` somente quando essa inicializacao foi rastreada pelo proprio SDK. Nao reinicializa silenciosamente o `FB` compartilhado com outro app: quando a pagina nao pode rodar o nativo com seguranca — `FB` do host com inicializacao desconhecida, app conflitante, `appId` retornado pela API que nao e uma string numerica valida ou inicializacao que nao pode ser observada — a tentativa **degrada para o fluxo proxy** na janela reservada, em vez de rejeitar com `INVALID_CONFIGURATION`. Se a janela reservada tiver sido fechada ou bloqueada, a tentativa termina com `popup_closed` ou `POPUP_BLOCKED`. A quarentena de login nativo (`CONNECTION_IN_PROGRESS`) nao degrada: ela protege a atribuicao de callbacks tardios da Meta.
Com o fallback, um `FB` preexistente do host (inclusive de outro app) nao impede mais a conexao: o login acontece na janela da plataforma. Para canais com `appCustom`, isso exige que o **seu** app da Meta conheca o dominio da plataforma, conforme a tabela acima; sem os redirects e dominios corretos, a Meta pode bloquear o fluxo proxy do seu app. Nao reinicialize o `FB` por fora depois que o SDK rastrear seu app. Nao presuma que `direct: false` force proxy nem que `direct: true` garanta nativo: `appCustom`, a origem do frontend e agora a disponibilidade do nativo na pagina tambem decidem o fluxo.
Em SDKs carregados pela biblioteca, chamadas externas ao `FB.init` atual sao encaminhadas normalmente, mas invalidam o app conhecido: uma conexao nativa posterior degrada para o proxy em vez de usar o app errado. Referencias ao `init` original salvas antes dessa observacao nao podem ser monitoradas. Se um script externo expirar sem falha conclusiva, seu carregamento ainda pode terminar: novos SDKs nao recebem proveniencia propria ate recarregar a pagina. O script do host nao e removido.
## Desenvolvimento e verificacao
Use **Node.js 22**, conforme `.nvmrc`, e as dependencias do lockfile:
```bash
nvm use
npm ci
npm run check
```
`npm run check` executa lint, testes unitarios, build limpo e smoke tests do pacote. Ele limpa os overrides `OMNIZAPI_API_URL` / `OMNIZAPI_FRONT_URL` no ambiente dos subprocessos, sem alterar o ambiente do chamador. A verificacao de release exige os padroes de producao Omnizapi (`https://api.omni.z-api.io` e `https://app.omni.z-api.io`), sem URLs customizadas embutidas no artefato. O lifecycle `prepack` protege automaticamente `npm pack` e `npm publish`; nao use `--ignore-scripts` para contornar essa protecao na preparacao de uma release. A CI faz verificacoes, nao publicacao automatica.
Para os testes locais de navegador, prepare antes os binarios dos tres engines:
```bash
npm exec playwright install chromium firefox webkit
npm run test:browser
```
`test:browser` cobre Chromium, Firefox e WebKit usando o artefato ja compilado: execute `npm run check` antes, pois esse teste nao recompila o SDK. Testes com provedores simulados nao homologam o SDK Meta, os proxies, credenciais ou canais reais; o fluxo nativo e os servicos externos continuam exigindo teste de integracao.
`npm run build` continua permitindo builds customizados com variaveis explicitas `OMNIZAPI_API_URL` e `OMNIZAPI_FRONT_URL`. Isso nao substitui o build com defaults de producao exigido pelo guard de release. As props `apiUrl` / `frontUrl` continuam sendo o modo de configurar o consumidor sem recompilar o SDK.
O hook de versao `scripts/sync-version.mjs` sincroniza os links UNPKG e scripts CDN do README/exemplo vanilla em futuras execucoes aprovadas de `npm version <nova-versao> --no-git-tag-version`. Isso nao publica o pacote.
## Checklist de deploy
- [ ] Antes de uma publicacao futura aprovada, executar as verificacoes e conferir exports default/named `Omnizapi`, tipos na raiz e `dist/omnizapi.min.js`; manter imports, versoes CDN e configuracao dos consumidores coordenados.
- [ ] Conferir os defaults de producao no artefato de release. Para consumidores customizados, configurar `apiUrl` / `frontUrl` nas props; conferir staging e ambas as URLs de sandbox. Variaveis no build ficam restritas a builds customizados explicitos.
- [ ] Manter na API as rotas existentes `GET /instances/:channelId/sdk-info` e `POST /v1/channels/:channelId/list-messenger-pages`, com autenticacao por chave publica, schema validado e sem exposicao de secrets.
- [ ] Manter no frontend `/oauth/proxy-cloud-api`, `/oauth/proxy-messenger`, `/oauth/proxy-instagram`, `/connect/telegram` e `/connect/whatsapp-web`, inclusive ao usar uma URL base com prefixo.
- [ ] Conferir a logo customizada e o asset padrao `${frontUrl}/logos/SVG/Ativo%2016.svg`, acessiveis por HTTPS e com identidade Omnizapi.
- [ ] Autorizar as origens exatas da aplicacao por canal e configurar CORS da API para essas origens, metodos e headers (`Authorization`, `Content-Type`). Validacao client-side nao substitui autorizacao no servidor.
- [ ] Cadastrar os redirects Meta exatos `${frontUrl}/oauth/proxy-messenger` e `${frontUrl}/oauth/proxy-instagram`, alem de dominios/configuracoes do Embedded Signup e app IDs corretos.
- [ ] Implementar preservacao de `state`, sessao autorizada e protocolo `after-connect` acima em todas as rotas; confirmar origem final, janela emissora, `attemptId`, `channelId` e `targetOrigin` exatos. Conferir politicas de navegador/COOP que possam remover `opener`.
- [ ] Revisar exposicao de query/fragmento em `state.currentUrl`, que permanece completo. Verificar com os proxies a compatibilidade com origem apenas antes de qualquer futura minimizacao; nao assumir suporte.
- [ ] Validar no backend sessao, CSRF, canal/tenant e credenciais especificas do provedor antes de finalizar; trocar/armazenar tokens somente no servidor, limitar repeticao/replay e nao aceitar `success: true` do navegador como prova suficiente.
- [ ] Homologar abort, cancelamento com/sem callback Meta (inclusive tardio), fechamento/bloqueio de popup, concorrencia, timeout, conflitos de app ID (que degradam para o fluxo proxy), mensagens de origem/janela/tentativa incorretas e falhas HTTP; validar em servicos reais o gesto do login nativo, a integracao QR e cada canal, inclusive Z-API. Os exemplos nao substituem essa homologacao.
## Licenca
MIT - Copyright (c) 2026 FourPixel IT. Autor: FourPixel IT. Texto completo no arquivo local `LICENSE` e na [licenca do pacote](https://unpkg.com/browse/@omnizapi/connect@1.1.1/LICENSE) apos a publicacao correspondente.