mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 09:34:46 +08:00
Merge PR #914: test(branding): a catraca passa a pegar host de terceiro, não só a marca (@webtecnica)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
---
|
||||
impacto: nada_mudou
|
||||
secao: corrigido
|
||||
titulo: O domínio de quem manda o PR não viaja mais dentro da imagem
|
||||
---
|
||||
|
||||
Nada muda na sua VPS: nenhum endereço de terceiro foi encontrado no código que
|
||||
embarca hoje, e nada foi trocado em produção. O que muda é o portão do
|
||||
repositório. A catraca que já barrava o nome antigo do produto — "DeskcommCRM"
|
||||
escrito na tela de quem instalou com a marca dele — passou a barrar também
|
||||
domínio de terceiro: site pessoal, endereço de teste ou visão de alguém
|
||||
enterrado numa parte do código que vai para a imagem. Quando isso acontece, o PR
|
||||
é reprovado com o endereço e o arquivo na mensagem. Foi por esse buraco que a
|
||||
identificação enviada à OpenRouter levava o endereço pessoal de quem escreveu o
|
||||
PR: o consumo de cada instalação ficava creditado a um site que não é o seu nem
|
||||
o nosso.
|
||||
|
||||
Crédito: @webtecnica.
|
||||
+455
-6
@@ -366,11 +366,40 @@ const RAIZES_VARRIDAS = ["app", "components", "hooks", "lib", "workers"] as cons
|
||||
* procurar `//` em qualquer posição descartaria `"https://deskcomm.app"`, que é
|
||||
* exatamente um vazamento de verdade.
|
||||
*/
|
||||
function marcasNoTexto(fonte: string): string[] {
|
||||
const achadas: string[] = [];
|
||||
/**
|
||||
* As linhas de um fonte que CONTAM — a regra de comentário em um lugar só.
|
||||
*
|
||||
* Linha que ABRE com `//`, `*` ou `/*` é comentário e não conta: comentário não
|
||||
* chega ao usuário, e contá-lo encheria a lista de entradas inertes até ninguém
|
||||
* mais ler as que importam. O teste olha só o início da linha DE PROPÓSITO —
|
||||
* procurar `//` em qualquer posição descartaria `"https://deskcomm.app"`, que é
|
||||
* exatamente um vazamento de verdade.
|
||||
*
|
||||
* NÃO se reaproveita onde a fonte não é TypeScript: o comentário de HTML
|
||||
* (`<!-- -->`) atravessa linhas e o de TOML abre com `#`. A varredura de
|
||||
* `supabase/templates/*.html` e `config.toml` tem as próprias funções de
|
||||
* limpeza, logo abaixo, e o porquê está escrito lá.
|
||||
*/
|
||||
function linhasQueContam(fonte: string): string[] {
|
||||
const uteis: string[] = [];
|
||||
for (const linha of fonte.split("\n")) {
|
||||
const inicio = linha.trimStart();
|
||||
if (inicio.startsWith("//") || inicio.startsWith("*") || inicio.startsWith("/*")) continue;
|
||||
uteis.push(linha);
|
||||
}
|
||||
return uteis;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrai as marcas de um texto, uma por ocorrência.
|
||||
*
|
||||
* Casa `deskcomm` em qualquer caixa e leva junto o identificador inteiro em volta
|
||||
* (`x-deskcomm-signature`, `support@deskcomm.com.br`), porque é o identificador —
|
||||
* não a palavra solta — que distingue contrato de fio de vazamento de marca.
|
||||
*/
|
||||
function marcasNoTexto(fonte: string): string[] {
|
||||
const achadas: string[] = [];
|
||||
for (const linha of linhasQueContam(fonte)) {
|
||||
for (const casada of linha.matchAll(/[\w@.-]*deskcomm[\w@.-]*/gi)) {
|
||||
// Pontuação encostada (o ponto final de "no DeskcommCRM.") não faz parte
|
||||
// do identificador e faria a lista mudar por causa de uma vírgula.
|
||||
@@ -380,13 +409,109 @@ function marcasNoTexto(fonte: string): string[] {
|
||||
return achadas.sort();
|
||||
}
|
||||
|
||||
function arquivosVarridos(dir: string): string[] {
|
||||
/**
|
||||
* Host dentro de URL. Exigir rótulo final de LETRAS (`\.[a-z]{2,}`) é o que
|
||||
* dispensa, de uma vez e sem linha de allowlist nenhuma, `http://.../`,
|
||||
* `http://localhost:3000`, `http://waha:3000` e TODO IP literal — `127.0.0.1`,
|
||||
* `10.0.0.5`, `169.254.1.1`: nenhum deles termina em TLD alfabético.
|
||||
*/
|
||||
const HOST_EM_URL = /https?:\/\/([a-z0-9][a-z0-9.-]*\.[a-z]{2,})/gi;
|
||||
|
||||
/**
|
||||
* Host NU, e só quando é o CONTEÚDO de uma string (`"meet.google.com"`).
|
||||
*
|
||||
* Fora de aspas ele é identificador, e a forma "em qualquer contexto" foi medida
|
||||
* antes de ser descartada: casa `logger.info(` (37 linhas), `auth.organization_id`
|
||||
* e `message.received` — 339 linhas e 160 "hosts" na superfície que embarca,
|
||||
* quase todos falsos. A lista de TLDs é a parte heurística desta forma, e ela é
|
||||
* explícita de propósito: um `[a-z]{2,}` genérico é o que produz os 160.
|
||||
*
|
||||
* O `(?![a-z0-9_-])` depois do TLD é carregador, e as duas metades dele vêm de
|
||||
* medição, não de gosto:
|
||||
* - sem ele, `auth.organization_id` casa como `auth.org`;
|
||||
* - sem o `_`, os nomes de evento de auditoria casam em bloco — medido na
|
||||
* `main`: `onboarding.ai_configured`, `ai.org_memory_published`,
|
||||
* `channel.ai_access_updated`, `conversation.ai_paused`,
|
||||
* `lgpd.store_redact_received` e `env.APP_NAME` (6 falsos, todos com `_`
|
||||
* logo depois do "TLD"). `_` não existe em nome de host, então barrá-lo não
|
||||
* esconde host nenhum.
|
||||
*/
|
||||
const HOST_NU_EM_STRING =
|
||||
/["'`](?:https?:\/\/)?((?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+(?:com\.br|com|net|org|io|app|dev|ai|co|cloud|br|me|tv|edu|gov|info|xyz|site|online|shop|store))(?![a-z0-9_-])/gi;
|
||||
|
||||
/**
|
||||
* Host que não é terceiro porque não resolve para ninguém: TLD reservado para
|
||||
* documentação e teste (RFC 6761) e o próprio `example.com` com os irmãos
|
||||
* (RFC 2606). `exemplo`/`ejemplo` NÃO entram aqui de propósito — `mi-gateway.ejemplo.com`
|
||||
* está no código que embarca como amostra de campo, e amostra se DECLARA
|
||||
* (categoria `AMOSTRA`), não se ignora em silêncio.
|
||||
*/
|
||||
const TLDS_RESERVADOS = /\.(test|invalid|local|localhost|example)$/i;
|
||||
const EXAMPLE_RFC2606 = /(^|\.)example\.(com|net|org)$/i;
|
||||
|
||||
/**
|
||||
* Extrai os hosts de terceiro de um texto — distintos, em minúsculas, em ordem.
|
||||
*
|
||||
* Distintos (e não uma por ocorrência, como `marcasNoTexto`) porque a lista de
|
||||
* hosts declarados é por HOST, não por arquivo: o segundo call site de um
|
||||
* endpoint já declarado não pede linha nova, e é isso que impede a catraca de
|
||||
* virar churn de allowlist a cada arquivo que passa a chamar `api.openai.com`
|
||||
* (medido: ele aparece em 5 arquivos, `graph.facebook.com` em 6).
|
||||
*
|
||||
* Duas formas, as duas medidas na calibração da #287: URL (`HOST_EM_URL`) e host
|
||||
* nu em string (`HOST_NU_EM_STRING`). A terceira — host nu em QUALQUER contexto —
|
||||
* está medida e rejeitada acima.
|
||||
*/
|
||||
function hostsNoTexto(fonte: string): string[] {
|
||||
const achados = new Set<string>();
|
||||
for (const linha of linhasQueContam(fonte)) {
|
||||
for (const m of linha.matchAll(HOST_EM_URL)) {
|
||||
const host = m[1];
|
||||
if (host) achados.add(host.toLowerCase());
|
||||
}
|
||||
for (const m of linha.matchAll(HOST_NU_EM_STRING)) {
|
||||
const host = m[1];
|
||||
if (host) achados.add(host.toLowerCase());
|
||||
}
|
||||
}
|
||||
return [...achados].filter((h) => !TLDS_RESERVADOS.test(h) && !EXAMPLE_RFC2606.test(h)).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Os arquivos varridos de uma raiz.
|
||||
*
|
||||
* As duas opções existem porque as varreduras divergem em DOIS pontos, e cada
|
||||
* divergência tem motivo medido — não é preferência:
|
||||
*
|
||||
* - `app/design` fica FORA da catraca de marca (lá o nome do produto é o ASSUNTO
|
||||
* da página) e DENTRO da de host, cujo critério é outro: "embarca na imagem".
|
||||
* `app/design` embarca. Medido: 0 hosts lá hoje, então entrar custa zero.
|
||||
* - os `*.test.ts(x)` ficam FORA da catraca de host: fixture não embarca (o
|
||||
* `Dockerfile` copia `.next/standalone`, `.next/static` e `public/`), e com
|
||||
* eles dentro os hosts a declarar subiam de 21 para 61 — `localhost` em 44
|
||||
* arquivos, `crm.exemplo.com` em 12 linhas de fixture. Allowlist de fixture
|
||||
* afoga o sinal que a catraca existe para dar.
|
||||
*/
|
||||
function arquivosVarridos(
|
||||
dir: string,
|
||||
opcoes: { comDesign?: boolean; comTeste?: boolean } = {},
|
||||
): string[] {
|
||||
// `comTeste` default TRUE de propósito: a catraca de marca conta os arquivos
|
||||
// de teste (três deles estão em MARCA_CONGELADA como dívida declarada). Quem
|
||||
// quer fixture fora — a de host — passa `comTeste: false` explicitamente.
|
||||
const comDesign = opcoes.comDesign ?? false;
|
||||
const comTeste = opcoes.comTeste ?? true;
|
||||
const alvos: string[] = [];
|
||||
for (const entrada of fs.readdirSync(path.join(RAIZ, dir), { withFileTypes: true })) {
|
||||
const rel = path.posix.join(dir, entrada.name);
|
||||
if (rel.startsWith("app/design")) continue;
|
||||
if (entrada.isDirectory()) alvos.push(...arquivosVarridos(rel));
|
||||
else if (rel.endsWith(".ts") || rel.endsWith(".tsx")) alvos.push(rel);
|
||||
if (!comDesign && rel.startsWith("app/design")) continue;
|
||||
if (entrada.isDirectory()) {
|
||||
alvos.push(...arquivosVarridos(rel, opcoes));
|
||||
continue;
|
||||
}
|
||||
if (!rel.endsWith(".ts") && !rel.endsWith(".tsx")) continue;
|
||||
if (!comTeste && /\.(test|spec)\.tsx?$/.test(rel)) continue;
|
||||
alvos.push(rel);
|
||||
}
|
||||
return alvos;
|
||||
}
|
||||
@@ -630,3 +755,327 @@ describe("catraca de marca no que o GoTrue renderiza", () => {
|
||||
expect(encontradoAqui.has("supabase/templates/recovery.html")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* A TERCEIRA varredura: host de terceiro.
|
||||
*
|
||||
* A catraca acima é cega para URL sem a marca. O caso medido é o #266: um
|
||||
* `HTTP-Referer` com o domínio pessoal do contribuidor foi parar na chamada de
|
||||
* TODO self-hoster, e a varredura da época só olhava `deskcomm` — o `X-Title`
|
||||
* ficava coberto pela marca, o domínio sozinho passava verde. Host de terceiro
|
||||
* no código que embarca é a mesma falha por outro nome: o domínio de alguém
|
||||
* viaja na imagem que o cliente instala, e ninguém vê.
|
||||
*
|
||||
* Tudo aqui foi MEDIDO na `main` antes de virar régua — catraca nova se calibra
|
||||
* antes de virar catraca, senão ela nasce ruidosa e o time aprende a ignorá-la:
|
||||
*
|
||||
* - superfície: `app|components|hooks|lib|workers`, o que o `Dockerfile` copia
|
||||
* para a imagem (`.next/standalone`, `.next/static`, `public/`) — a MESMA da
|
||||
* catraca de marca, mais `app/design` (embarca) e menos os `*.test.ts(x)`
|
||||
* (fixture não embarca). 1717 arquivos.
|
||||
* - régua crua sobre a `main`: 21 hosts, NENHUM vazamento — todos endpoint de
|
||||
* fornecedor, painel de fornecedor, amostra de campo ou identificador de fio.
|
||||
* Cada um está declarado abaixo com categoria e motivo, e é o julgamento
|
||||
* deles que esta catraca passa a defender.
|
||||
* - o que a régua NÃO pega, de propósito e com medição: host nu FORA de aspas
|
||||
* (339 linhas e 160 "hosts" na superfície, quase todos identificador
|
||||
* pontuado — `logger.info(`, `auth.organization_id`, `message.received`) e
|
||||
* host com TLD fora da lista explícita de `HOST_NU_EM_STRING`. As duas são a
|
||||
* fronteira conhecida desta catraca; a alternativa foi medida e é pior.
|
||||
* - lista por HOST, não por arquivo: o segundo call site de um endpoint já
|
||||
* declarado não pede linha nova (`api.openai.com` aparece em 5 arquivos,
|
||||
* `graph.facebook.com` em 6). `FORNECEDOR` pode crescer — provider novo é
|
||||
* provider novo. `CONSOLE`, `AMOSTRA`, `PLATAFORMA` e `PROTOCOLO` são
|
||||
* conjunto FECHADO fixado por nome, porque é nessas que a pressa tentaria
|
||||
* declarar um vazamento para seguir em frente.
|
||||
*/
|
||||
type CategoriaDeHost =
|
||||
/** Endpoint do fornecedor: PARA ONDE o código fala. Pode crescer. */
|
||||
| "FORNECEDOR"
|
||||
/** Painel/documentação do fornecedor: onde o usuário busca a credencial DELE. */
|
||||
| "CONSOLE"
|
||||
/** Amostra de formato em campo de formulário. Chega à tela — por isso se declara. */
|
||||
| "AMOSTRA"
|
||||
/** Host de plataforma ACEITO na entrada (validação), não destino de chamada. */
|
||||
| "PLATAFORMA"
|
||||
/** Identificador de fio que gravamos; quem reconhece é código de fora. */
|
||||
| "PROTOCOLO";
|
||||
|
||||
type EntradaDeHost = { categoria: CategoriaDeHost; motivo: string };
|
||||
|
||||
const HOSTS_DECLARADOS: Record<string, EntradaDeHost> = {
|
||||
// ── destino de chamada: o código fala com eles, sempre foi assim ──────────
|
||||
"api.openai.com": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo:
|
||||
"endpoint da API da OpenAI (embeddings da busca e transcrição de áudio). É o destino do request: trocar pelo domínio do revendedor faria a chamada não chegar a lugar nenhum.",
|
||||
},
|
||||
"api.anthropic.com": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo:
|
||||
"endpoint da API da Anthropic (contagem de tokens e a prova de credencial da tela de configuração). Mesma razão: é o destino, não texto de interface.",
|
||||
},
|
||||
"openrouter.ai": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo:
|
||||
"endpoint da OpenRouter nos três caminhos que falam com ela (runtime, catálogo de modelos e prova de crédito). O `HTTP-Referer` da atribuição NÃO mora aqui — sai de env (OPENROUTER_APP_URL), e quem o defende é tests/unit/openrouter-atribuicao.test.ts.",
|
||||
},
|
||||
"generativelanguage.googleapis.com": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo:
|
||||
"endpoint da API Gemini (visão e tradução). Domínio do fornecedor, sem alternativa que não seja proxy nosso.",
|
||||
},
|
||||
"graph.facebook.com": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo:
|
||||
"endpoint da Graph API do WhatsApp Cloud — 6 arquivos: envio de template, sincronização de modelos, validação de credencial, conversões e insights. É contrato da Meta, não escolha nossa.",
|
||||
},
|
||||
"www.googleapis.com": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo:
|
||||
"endpoint das APIs do Google que a agenda usa (calendar/v3, oauth2/v4, userinfo). O projeto é do cliente; o domínio é do fornecedor.",
|
||||
},
|
||||
"oauth2.googleapis.com": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo:
|
||||
"endpoint de token do OAuth do Google — o par de `accounts.google.com` no fluxo de autorização da agenda.",
|
||||
},
|
||||
"accounts.google.com": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo:
|
||||
"tela de consentimento do OAuth do Google: é para lá que o usuário é REDIRECIONADO para autorizar a agenda. Endereço do fornecedor por definição.",
|
||||
},
|
||||
"api.tiendanube.com": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo: "endpoint da API da Nuvemshop/Tiendanube (ordens e catálogo do e-commerce do cliente).",
|
||||
},
|
||||
"www.tiendanube.com": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo:
|
||||
"base de autorização do OAuth da Nuvemshop — para onde o lojista é mandado autorizar o aplicativo. Sem ela, a integração não conecta.",
|
||||
},
|
||||
"zernio.com": {
|
||||
categoria: "FORNECEDOR",
|
||||
motivo:
|
||||
"endpoint padrão do adapter do canal de mensagens, com override por ZERNIO_API_BASE_URL. Fixo de propósito: instalação que não configura nada tem de funcionar.",
|
||||
},
|
||||
// ── painel do fornecedor: texto de tela apontando para o endereço DELE ────
|
||||
"platform.openai.com": {
|
||||
categoria: "CONSOLE",
|
||||
motivo:
|
||||
"painel onde o usuário gera a PRÓPRIA chave da OpenAI. O endereço é do fornecedor e é a informação que a tela tem de dar — mandar para outro lugar seria pior.",
|
||||
},
|
||||
"console.anthropic.com": {
|
||||
categoria: "CONSOLE",
|
||||
motivo: "painel de chaves da Anthropic. Mesmo caso: é de onde a credencial do usuário sai.",
|
||||
},
|
||||
"aistudio.google.com": {
|
||||
categoria: "CONSOLE",
|
||||
motivo: "Google AI Studio — onde o usuário cria a chave do Gemini.",
|
||||
},
|
||||
"partners.tiendanube.com": {
|
||||
categoria: "CONSOLE",
|
||||
motivo:
|
||||
"portal de parceiros da Nuvemshop, onde o operador registra o aplicativo e pega client id e secret. Endereço da plataforma, não nosso.",
|
||||
},
|
||||
// ── amostra de formato: mostra o que digitar, não é destino ──────────────
|
||||
"meusistema.com": {
|
||||
categoria: "AMOSTRA",
|
||||
motivo:
|
||||
"placeholder do campo de URL do webhook de saída: mostra o FORMATO do que digitar. A doutrina já trata endereço de rede em tela como exemplo técnico (tests/unit/i18n-espanhol-cobre-a-tela.test.ts).",
|
||||
},
|
||||
"tusitio.com": {
|
||||
categoria: "AMOSTRA",
|
||||
motivo:
|
||||
"placeholder do campo de URL de redirecionamento do webhook: mesma natureza — amostra de formato, não destino que o produto busca.",
|
||||
},
|
||||
"mi-gateway.ejemplo.com": {
|
||||
categoria: "AMOSTRA",
|
||||
motivo:
|
||||
"placeholder do campo de base URL de gateway OpenAI-compatible na tela de provedores: amostra do formato aceito.",
|
||||
},
|
||||
"000000000000-xxxxxxxx.apps.googleusercontent.com": {
|
||||
categoria: "AMOSTRA",
|
||||
motivo:
|
||||
"placeholder do campo de client id do Google: exibe o FORMATO do identificador (e o host que ele carrega) para quem vai criar a credencial no console.",
|
||||
},
|
||||
// ── entrada conferida e identificador de fio ─────────────────────────────
|
||||
"meet.google.com": {
|
||||
categoria: "PLATAFORMA",
|
||||
motivo:
|
||||
"host do Google Meet aceito na validação do link de reunião (`meetVideoUrl`): é entrada que o produto CONFERE, não endereço que ele busca. Sem a linha, qualquer host passaria por link de reunião.",
|
||||
},
|
||||
"deskcomm.app": {
|
||||
categoria: "PROTOCOLO",
|
||||
motivo:
|
||||
"sufixo do iCalUID gravado no Google Calendar do cliente (lib/agenda/google/evento.ts). Identificador de fio que reconhecemos meses depois — já congelado como PROTOCOLO pela catraca de marca.",
|
||||
},
|
||||
};
|
||||
|
||||
describe("catraca de host de terceiro no código que embarca", () => {
|
||||
const alvos = RAIZES_VARRIDAS.flatMap((raiz) =>
|
||||
arquivosVarridos(raiz, { comDesign: true, comTeste: false }),
|
||||
);
|
||||
/** host → arquivos onde aparece, para a falha apontar onde ele mora. */
|
||||
const encontrados = new Map<string, string[]>();
|
||||
for (const arquivo of alvos) {
|
||||
for (const host of hostsNoTexto(fs.readFileSync(path.join(RAIZ, arquivo), "utf8"))) {
|
||||
encontrados.set(host, [...(encontrados.get(host) ?? []), arquivo]);
|
||||
}
|
||||
}
|
||||
|
||||
it("a varredura alcança o código que embarca — senão o resto não prova nada", () => {
|
||||
expect(alvos.length).toBeGreaterThan(800);
|
||||
expect(alvos.filter((f) => f.endsWith(".tsx")).length).toBeGreaterThan(200);
|
||||
expect(alvos.filter((f) => /\.(test|spec)\.tsx?$/.test(f))).toEqual([]);
|
||||
// Canário: `api.openai.com` está no código desde antes desta catraca. Se o
|
||||
// extrator quebrar (regex, regra de comentário, forma da string), o conjunto
|
||||
// esvazia, TODOS os casos abaixo passam e a catraca vira decoração — foi
|
||||
// assim que a varredura antiga ficava verde enquanto o domínio vazava.
|
||||
expect(
|
||||
encontrados.has("api.openai.com"),
|
||||
"canário: o extrator parou de achar o que sempre achou",
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("pega host de terceiro no formato do #266 — o caso que a régua antiga deixava passar", () => {
|
||||
// O contrafactual da issue, com o `X-Title` fora de propósito: a catraca de
|
||||
// marca pega o X-Title (é a marca) e não pegava o domínio sozinho.
|
||||
expect(
|
||||
hostsNoTexto('headers: { "HTTP-Referer": "https://crm.do-contribuidor.com.br" }'),
|
||||
).toEqual(["crm.do-contribuidor.com.br"]);
|
||||
expect(hostsNoTexto('const BASE = "https://api.do-contribuidor.com.br/v1";')).toEqual([
|
||||
"api.do-contribuidor.com.br",
|
||||
]);
|
||||
expect(hostsNoTexto("const BASE = `https://api.do-contribuidor.com.br/v1`;")).toEqual([
|
||||
"api.do-contribuidor.com.br",
|
||||
]);
|
||||
});
|
||||
|
||||
it("pega host NU quando ele é o conteúdo de uma string — e não identificador pontuado", () => {
|
||||
expect(hostsNoTexto('url.hostname === "meet.google.com"')).toEqual(["meet.google.com"]);
|
||||
expect(hostsNoTexto('placeholder="000000000000-xxxxxxxx.apps.googleusercontent.com"')).toEqual([
|
||||
"000000000000-xxxxxxxx.apps.googleusercontent.com",
|
||||
]);
|
||||
// Os falsos medidos da forma "em qualquer contexto" — é por causa deles que
|
||||
// o host nu só vale dentro de aspas, e é o `(?![a-z0-9_-])` que impede
|
||||
// `auth.organization_id` de casar como `auth.org`.
|
||||
expect(hostsNoTexto("logger.info(evento);")).toEqual([]);
|
||||
expect(hostsNoTexto('audit("auth.organization_id", { org });')).toEqual([]);
|
||||
expect(hostsNoTexto('emit("message.received", payload);')).toEqual([]);
|
||||
expect(hostsNoTexto('localStorage.getItem("deskcomm.show_ai_citations")')).toEqual([]);
|
||||
// Os 6 falsos que a PRIMEIRA versão da régua produziu na `main`: nomes de
|
||||
// evento de auditoria cujo prefixo termina num "TLD" (`ai`, `org`, `app`,
|
||||
// `store`) seguido de `_`. Nenhum deles é host, e é o `_` no lookahead que
|
||||
// os separa de um host de verdade.
|
||||
expect(hostsNoTexto('action: "onboarding.ai_configured",')).toEqual([]);
|
||||
expect(hostsNoTexto('action: "ai.org_memory_published",')).toEqual([]);
|
||||
expect(hostsNoTexto('action: "channel.ai_access_updated",')).toEqual([]);
|
||||
expect(hostsNoTexto('action: "conversation.ai_paused",')).toEqual([]);
|
||||
expect(hostsNoTexto('action: "lgpd.store_redact_received",')).toEqual([]);
|
||||
expect(hostsNoTexto("`env.APP_NAME` cru")).toEqual([]);
|
||||
});
|
||||
|
||||
it("host que não resolve — loopback, IP, serviço do Docker, sentinela — não pede linha", () => {
|
||||
expect(hostsNoTexto('const u = "http://localhost:3000/api";')).toEqual([]);
|
||||
expect(hostsNoTexto('fetch("http://waha:3000/api");')).toEqual([]);
|
||||
expect(hostsNoTexto('const s = "http://127.0.0.1:54321";')).toEqual([]);
|
||||
expect(hostsNoTexto('const s = "https://10.0.0.5/x";')).toEqual([]);
|
||||
expect(hostsNoTexto('const s = "https://169.254.169.254/latest/meta-data";')).toEqual([]);
|
||||
expect(hostsNoTexto('const s = "http://.../";')).toEqual([]);
|
||||
expect(hostsNoTexto('const s = "https://crm.exemplo.test";')).toEqual([]);
|
||||
expect(hostsNoTexto('const s = "https://x.example.com/v1";')).toEqual([]);
|
||||
});
|
||||
|
||||
it("não conta host em comentário, e não confunde o `//` da URL com comentário", () => {
|
||||
expect(hostsNoTexto(" // o endpoint é https://api.openai.com/v1")).toEqual([]);
|
||||
expect(hostsNoTexto(" * https://api.openai.com/v1/docs")).toEqual([]);
|
||||
expect(hostsNoTexto('const u = "https://api.openai.com/v1";')).toEqual(["api.openai.com"]);
|
||||
});
|
||||
|
||||
it("nenhum host de terceiro fora da lista declarada", () => {
|
||||
const novos = [...encontrados.keys()].filter((h) => !(h in HOSTS_DECLARADOS));
|
||||
expect(
|
||||
novos,
|
||||
"Host de terceiro hardcoded no código que embarca (a imagem que o cliente instala).\n" +
|
||||
"Se é endereço do fornecedor (o código FALA com ele) ou painel dele (o usuário busca a\n" +
|
||||
"credencial lá), declare em HOSTS_DECLARADOS com categoria e motivo. Se é amostra de\n" +
|
||||
"formato de campo, declare como AMOSTRA — ela chega à tela, então se declara em vez de\n" +
|
||||
"se ignorar. E se é o domínio de alguém — o seu, o de um contribuidor, o do cliente de\n" +
|
||||
"vocês, o de um serviço de teste —, tire do código: foi exatamente o #266.\n\n" +
|
||||
novos
|
||||
.map((h) => {
|
||||
const onde = encontrados.get(h) ?? [];
|
||||
const lista = onde.slice(0, 4).join(", ") + (onde.length > 4 ? ", …" : "");
|
||||
return ` ${h} → ${lista}`;
|
||||
})
|
||||
.join("\n"),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("a lista não guarda host que saiu do código", () => {
|
||||
const obsoletos = Object.keys(HOSTS_DECLARADOS).filter((h) => !encontrados.has(h));
|
||||
expect(
|
||||
obsoletos,
|
||||
"Estes hosts não aparecem mais no código que embarca — apague a linha de HOSTS_DECLARADOS:\n" +
|
||||
obsoletos.map((h) => ` ${h}`).join("\n"),
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("toda entrada declara categoria válida e explica o porquê", () => {
|
||||
const categorias: CategoriaDeHost[] = [
|
||||
"FORNECEDOR",
|
||||
"CONSOLE",
|
||||
"AMOSTRA",
|
||||
"PLATAFORMA",
|
||||
"PROTOCOLO",
|
||||
];
|
||||
for (const [host, entrada] of Object.entries(HOSTS_DECLARADOS)) {
|
||||
expect(categorias, `${host}: categoria desconhecida`).toContain(entrada.categoria);
|
||||
expect(
|
||||
entrada.motivo.length,
|
||||
`${host}: motivo curto demais para servir de rastro`,
|
||||
).toBeGreaterThan(40);
|
||||
}
|
||||
});
|
||||
|
||||
it("só FORNECEDOR pode crescer — o resto é conjunto fechado", () => {
|
||||
// A pressa descreveria o vazamento como AMOSTRA (ou chamaria um domínio
|
||||
// pessoal de CONSOLE) e seguiria em frente. Estas categorias não são
|
||||
// destino de chamada, então crescem só por decisão explícita: a lista
|
||||
// abaixo é fixada por NOME, e mexer nela é editar teste, no diff.
|
||||
const fechadas = Object.entries(HOSTS_DECLARADOS)
|
||||
.filter(([, entrada]) => entrada.categoria !== "FORNECEDOR")
|
||||
.map(([host]) => host)
|
||||
.sort();
|
||||
expect(
|
||||
fechadas,
|
||||
"Categoria fechada ganhou host novo. Se o código FALA com ele, é FORNECEDOR; se não, " +
|
||||
"o crescimento tem de ser uma decisão escrita aqui — não mais uma linha na lista.",
|
||||
).toEqual([
|
||||
"000000000000-xxxxxxxx.apps.googleusercontent.com",
|
||||
"aistudio.google.com",
|
||||
"console.anthropic.com",
|
||||
"deskcomm.app",
|
||||
"meet.google.com",
|
||||
"meusistema.com",
|
||||
"mi-gateway.ejemplo.com",
|
||||
"partners.tiendanube.com",
|
||||
"platform.openai.com",
|
||||
"tusitio.com",
|
||||
]);
|
||||
});
|
||||
|
||||
it("o filtro de fixture e a fronteira do app/design funcionam", () => {
|
||||
const comTeste = arquivosVarridos("hooks", { comTeste: true });
|
||||
const semTeste = arquivosVarridos("hooks", { comTeste: false });
|
||||
expect(semTeste.length).toBeGreaterThan(0);
|
||||
expect(comTeste.length).toBeGreaterThan(semTeste.length);
|
||||
expect(semTeste.filter((f) => /\.(test|spec)\.tsx?$/.test(f))).toEqual([]);
|
||||
// `app/design` entra na varredura de host (embarca) e fica fora da de marca.
|
||||
expect(
|
||||
arquivosVarridos("app", { comDesign: true }).some((f) => f.startsWith("app/design")),
|
||||
).toBe(true);
|
||||
expect(arquivosVarridos("app").some((f) => f.startsWith("app/design"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user