@fontdata/ai-harness 2.5.1 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ import { identificadoresDoSql } from '../somente-leitura.js';
2
+ import { classificarColuna, classificarNumero, encontrarNoTexto } from './detectar.js';
3
+ import { mascararValor, OCULTO } from './mascarar.js';
4
+ function lerRegras(entradas) {
5
+ return entradas
6
+ .map((entrada) => entrada.trim().toLowerCase().split('.'))
7
+ .filter((partes) => partes.every((p) => p !== ''))
8
+ .map((partes) => partes.length === 1 ? { tabela: null, coluna: partes[0] } : { tabela: partes.at(-2), coluna: partes.at(-1) });
9
+ }
10
+ function casa(regras, nome, origem) {
11
+ const coluna = (origem ? origem.coluna : nome).toLowerCase();
12
+ const tabela = origem ? (origem.tabela.toLowerCase().split('.').at(-1) ?? '') : null;
13
+ return regras.some((r) => r.coluna === coluna && (r.tabela === null || r.tabela === tabela));
14
+ }
15
+ function identificadores(sql, dialeto) {
16
+ try {
17
+ return identificadoresDoSql(sql, dialeto).map((id) => id.toLowerCase());
18
+ }
19
+ catch {
20
+ // Sem tokenização, qualquer palavra do texto conta como identificador: mascara a mais, nunca a menos.
21
+ return Array.from(sql.matchAll(/[\p{L}_][\p{L}\p{N}_$]*/gu), (m) => m[0].toLowerCase());
22
+ }
23
+ }
24
+ function colunaPessoalCitada(opcoes, mascarar) {
25
+ const extras = new Set(mascarar.map((r) => r.coluna));
26
+ for (const id of identificadores(opcoes.sql, opcoes.dialeto)) {
27
+ const categoria = classificarColuna(id);
28
+ if (categoria)
29
+ return categoria;
30
+ if (extras.has(id))
31
+ return 'mascarar_colunas';
32
+ }
33
+ return null;
34
+ }
35
+ function ehBinario(valor) {
36
+ return valor instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(valor));
37
+ }
38
+ function mascararStringLivre(texto, contador) {
39
+ const inicio = texto.trimStart()[0];
40
+ if (inicio === '{' || inicio === '[') {
41
+ let objeto;
42
+ try {
43
+ objeto = JSON.parse(texto);
44
+ }
45
+ catch {
46
+ objeto = undefined;
47
+ }
48
+ if (objeto !== undefined)
49
+ return JSON.stringify(mascararEstrutura(objeto, contador));
50
+ }
51
+ const trechos = encontrarNoTexto(texto);
52
+ if (trechos.length === 0)
53
+ return texto;
54
+ let saida = '';
55
+ let posicao = 0;
56
+ for (const t of trechos) {
57
+ saida += texto.slice(posicao, t.inicio) + String(mascararValor(texto.slice(t.inicio, t.fim), t.categoria));
58
+ posicao = t.fim;
59
+ }
60
+ contador.valores += trechos.length;
61
+ return saida + texto.slice(posicao);
62
+ }
63
+ function mascararPorValor(valor, testarNumero, contador) {
64
+ if (typeof valor === 'string')
65
+ return mascararStringLivre(valor, contador);
66
+ if (typeof valor === 'number' || typeof valor === 'bigint') {
67
+ if (testarNumero && classificarNumero(valor) !== null) {
68
+ contador.valores += 1;
69
+ return OCULTO;
70
+ }
71
+ return valor;
72
+ }
73
+ if (ehBinario(valor)) {
74
+ contador.valores += 1;
75
+ return OCULTO;
76
+ }
77
+ if (valor instanceof Date)
78
+ return valor;
79
+ if (valor !== null && typeof valor === 'object')
80
+ return mascararEstrutura(valor, contador);
81
+ return valor;
82
+ }
83
+ function mascararEstrutura(valor, contador) {
84
+ if (Array.isArray(valor))
85
+ return valor.map((item) => mascararPorValor(item, true, contador));
86
+ if (valor === null || typeof valor !== 'object' || valor instanceof Date || ehBinario(valor)) {
87
+ return mascararPorValor(valor, true, contador);
88
+ }
89
+ const saida = {};
90
+ for (const [chave, item] of Object.entries(valor)) {
91
+ const categoria = classificarColuna(chave);
92
+ if (categoria === null) {
93
+ saida[chave] = mascararPorValor(item, true, contador);
94
+ }
95
+ else {
96
+ const mascarado = mascararValor(item, categoria);
97
+ if (mascarado !== item)
98
+ contador.valores += 1;
99
+ saida[chave] = mascarado;
100
+ }
101
+ }
102
+ return saida;
103
+ }
104
+ function tratamentoDaColuna(nome, origem, mascarar, liberar) {
105
+ if (casa(mascarar, nome, origem))
106
+ return { tipo: 'ocultar', categoria: 'mascarar_colunas' };
107
+ const semOrigem = !origem;
108
+ if (casa(liberar, nome, origem))
109
+ return { tipo: 'valor', calculada: false, numero: semOrigem };
110
+ const categoria = classificarColuna(origem ? origem.coluna : nome);
111
+ if (categoria)
112
+ return { tipo: 'categoria', categoria };
113
+ return { tipo: 'valor', calculada: origem === null, numero: semOrigem };
114
+ }
115
+ /**
116
+ * Mascara os dados pessoais de um resultado pela origem da coluna, pelo nome, pelas regras do projeto e pelo
117
+ * valor. Não altera o resultado recebido; com `ativo: false`, devolve-o sem mudança.
118
+ */
119
+ export function mascararResultado(resultado, opcoes) {
120
+ if (!opcoes.ativo)
121
+ return { resultado, colunasMascaradas: [], valoresEmTexto: 0 };
122
+ const mascarar = lerRegras(opcoes.regras.mascarar_colunas);
123
+ const liberar = lerRegras(opcoes.regras.liberar_colunas);
124
+ const tratamentos = resultado.colunas.map((nome, i) => tratamentoDaColuna(nome, resultado.origens === undefined ? undefined : resultado.origens[i], mascarar, liberar));
125
+ let citada;
126
+ const citacao = () => {
127
+ if (citada === undefined)
128
+ citada = colunaPessoalCitada(opcoes, mascarar);
129
+ return citada;
130
+ };
131
+ const marcadas = new Map();
132
+ const contador = { valores: 0 };
133
+ const linhas = resultado.linhas.map((linha) => linha.map((valor, i) => {
134
+ const t = tratamentos[i] ?? { tipo: 'valor', calculada: false, numero: true };
135
+ if (t.tipo === 'ocultar') {
136
+ marcadas.set(i, t.categoria);
137
+ return valor === null || valor === undefined ? valor : OCULTO;
138
+ }
139
+ if (t.tipo === 'categoria') {
140
+ marcadas.set(i, t.categoria);
141
+ return mascararValor(valor, t.categoria);
142
+ }
143
+ if (t.calculada && typeof valor === 'string') {
144
+ const categoria = citacao();
145
+ if (categoria !== null) {
146
+ marcadas.set(i, categoria);
147
+ return OCULTO;
148
+ }
149
+ }
150
+ return mascararPorValor(valor, t.numero, contador);
151
+ }));
152
+ const copia = { ...resultado, colunas: [...resultado.colunas], linhas };
153
+ if (resultado.origens !== undefined)
154
+ copia.origens = resultado.origens.map((o) => (o ? { ...o } : o));
155
+ const colunasMascaradas = [...marcadas.keys()]
156
+ .sort((a, b) => a - b)
157
+ .map((i) => ({ coluna: resultado.colunas[i] ?? String(i), categoria: marcadas.get(i) }));
158
+ return { resultado: copia, colunasMascaradas, valoresEmTexto: contador.valores };
159
+ }
160
+ /** Categoria com que a coluna `tabela.coluna` sai mascarada pelo nome ou pelas regras do projeto, ou `null`. */
161
+ export function categoriaDaColuna(tabela, coluna, regras) {
162
+ const t = tratamentoDaColuna(coluna, { tabela, coluna }, lerRegras(regras.mascarar_colunas), lerRegras(regras.liberar_colunas));
163
+ return t.tipo === 'valor' ? null : t.categoria;
164
+ }
165
+ /** Aplica a detecção por valor a um texto solto, como a mensagem de erro do banco. */
166
+ export function mascararTexto(texto) {
167
+ return mascararStringLivre(texto, { valores: 0 });
168
+ }
@@ -0,0 +1,210 @@
1
+ const PARTES_ISOLADAS = [
2
+ ['cpf', ['cpf']],
3
+ ['cnpj', ['cnpj']],
4
+ ['documento', ['rg', 'pis', 'nis', 'pasep', 'cnh', 'ctps', 'passaporte', 'passport', 'documento']],
5
+ ['email', ['email', 'mail']],
6
+ ['telefone', ['telefone', 'celular', 'fone', 'phone', 'tel', 'whatsapp', 'fax']],
7
+ ['endereco', ['endereco', 'logradouro', 'rua', 'bairro', 'complemento', 'address', 'street']],
8
+ ['cep', ['cep', 'zip', 'postal']],
9
+ ['nascimento', ['nascimento', 'nasc', 'birth', 'birthday']],
10
+ ['nome', ['sobrenome']],
11
+ ['financeiro', ['salario', 'renda', 'salary', 'income', 'agencia', 'iban', 'pix']],
12
+ ['cartao', ['cartao', 'card', 'cvv', 'cvc']],
13
+ ['ip', ['ip']],
14
+ ['segredo', ['senha', 'password', 'passwd', 'pwd', 'hash', 'token', 'secret', 'apikey']],
15
+ [
16
+ 'sensivel',
17
+ [
18
+ 'saude', 'diagnostico', 'cid', 'doenca', 'prontuario', 'biometria', 'digital', 'face', 'religiao', 'etnia',
19
+ 'raca', 'sindical', 'deficiencia', 'genetico',
20
+ ],
21
+ ],
22
+ ];
23
+ const PARTES_DE_PESSOA = [
24
+ 'completo', 'cliente', 'pessoa', 'mae', 'pai', 'social', 'responsavel', 'contato', 'usuario', 'funcionario',
25
+ 'paciente', 'aluno', 'titular', 'motorista', 'condutor', 'socio',
26
+ ];
27
+ /** Cada combinação exige uma parte de cada lista. */
28
+ const COMBINACOES = [
29
+ ['documento', [['titulo'], ['eleitor']]],
30
+ ['email', [['e'], ['mail']]],
31
+ ['ip', [['ip'], ['address']]],
32
+ ['segredo', [['api'], ['key']]],
33
+ ['sensivel', [['orientacao'], ['sexual']]],
34
+ ['financeiro', [['conta'], ['corrente', 'bancaria', 'numero']]],
35
+ ['nome', [['first', 'last', 'full'], ['name']]],
36
+ ['nome', [['nome', 'name'], PARTES_DE_PESSOA]],
37
+ ];
38
+ const TERMOS_FORTES = [
39
+ ['cpf', 'cpf'],
40
+ ['cnpj', 'cnpj'],
41
+ ['email', 'email'],
42
+ ['telefone', 'telefone'],
43
+ ['celular', 'telefone'],
44
+ ['nascimento', 'nascimento'],
45
+ ['nasc', 'nascimento'],
46
+ ['senha', 'segredo'],
47
+ ['password', 'segredo'],
48
+ ];
49
+ const MARCAS_DIACRITICAS = /\p{M}/gu;
50
+ const MINUSCULA_MAIUSCULA = /([\p{Ll}\p{N}])(\p{Lu})/gu;
51
+ const SIGLA_PALAVRA = /(\p{Lu})(\p{Lu}\p{Ll})/gu;
52
+ const LETRA_DIGITO = /(\p{L})(\p{N})/gu;
53
+ const DIGITO_LETRA = /(\p{N})(\p{L})/gu;
54
+ const SEPARADORES = /[^\p{L}\p{N}]+/u;
55
+ export function partesDoNome(nome) {
56
+ return nome
57
+ .normalize('NFD')
58
+ .replace(MARCAS_DIACRITICAS, '')
59
+ .replace(MINUSCULA_MAIUSCULA, '$1 $2')
60
+ .replace(SIGLA_PALAVRA, '$1 $2')
61
+ .replace(LETRA_DIGITO, '$1 $2')
62
+ .replace(DIGITO_LETRA, '$1 $2')
63
+ .toLowerCase()
64
+ .split(SEPARADORES)
65
+ .filter((p) => p !== '');
66
+ }
67
+ export function classificarColuna(nome) {
68
+ const partes = partesDoNome(nome);
69
+ const tem = new Set(partes);
70
+ for (const [categoria, listas] of COMBINACOES) {
71
+ if (listas.every((lista) => lista.some((p) => tem.has(p))))
72
+ return categoria;
73
+ }
74
+ if (partes.length === 1 && (partes[0] === 'nome' || partes[0] === 'name'))
75
+ return 'nome';
76
+ for (const [categoria, lista] of PARTES_ISOLADAS) {
77
+ if (lista.some((p) => tem.has(p)))
78
+ return categoria;
79
+ }
80
+ for (const [termo, categoria] of TERMOS_FORTES) {
81
+ if (partes.some((p) => p.includes(termo)))
82
+ return categoria;
83
+ }
84
+ return null;
85
+ }
86
+ const NAO_DIGITO = /\D/g;
87
+ const DIGITOS_REPETIDOS = /^(\d)\1*$/;
88
+ function soDigitos(texto) {
89
+ return texto.replace(NAO_DIGITO, '');
90
+ }
91
+ function digitoVerificador(digitos, pesos) {
92
+ let soma = 0;
93
+ for (let i = 0; i < pesos.length; i++)
94
+ soma += Number(digitos[i]) * (pesos[i] ?? 0);
95
+ const resto = soma % 11;
96
+ return resto < 2 ? 0 : 11 - resto;
97
+ }
98
+ export function cpfValido(cpf) {
99
+ const d = soDigitos(cpf);
100
+ if (d.length !== 11 || DIGITOS_REPETIDOS.test(d))
101
+ return false;
102
+ return (digitoVerificador(d, [10, 9, 8, 7, 6, 5, 4, 3, 2]) === Number(d[9]) &&
103
+ digitoVerificador(d, [11, 10, 9, 8, 7, 6, 5, 4, 3, 2]) === Number(d[10]));
104
+ }
105
+ export function cnpjValido(cnpj) {
106
+ const d = soDigitos(cnpj);
107
+ if (d.length !== 14 || DIGITOS_REPETIDOS.test(d))
108
+ return false;
109
+ return (digitoVerificador(d, [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]) === Number(d[12]) &&
110
+ digitoVerificador(d, [6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]) === Number(d[13]));
111
+ }
112
+ export function luhnValido(numero) {
113
+ const d = soDigitos(numero);
114
+ if (d.length < 2)
115
+ return false;
116
+ let soma = 0;
117
+ for (let i = 0; i < d.length; i++) {
118
+ let n = Number(d[d.length - 1 - i]);
119
+ if (i % 2 === 1) {
120
+ n *= 2;
121
+ if (n > 9)
122
+ n -= 9;
123
+ }
124
+ soma += n;
125
+ }
126
+ return soma % 10 === 0;
127
+ }
128
+ const SEQUENCIA_DE_DIGITOS = /\d+(?:(?:[.\-/]|\s+)\d+)*/g;
129
+ const GRUPO_DE_DIGITOS = /\d+/g;
130
+ const MAX_DIGITOS = 19;
131
+ const CPF_FORMATADO = /^\d{3}\.\d{3}\.\d{3}-\d{2}$/;
132
+ const CNPJ_FORMATADO = /^\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}$/;
133
+ const EMAIL = /[\p{L}\p{M}\p{N}._%+-]+@[\p{L}\p{M}\p{N}-]+(?:\.[\p{L}\p{M}\p{N}-]+)*\.[\p{L}\p{M}]{2,}/gu;
134
+ const TELEFONE = /(?<![\d+])(?:\+55\s?)?(?:\(\d{2}\)\s?|\d{2}[\s-])9?\d{4}[\s-]?\d{4}(?!\d)/g;
135
+ /** `seq` é um trecho de grupos inteiros com no máximo `MAX_DIGITOS` dígitos. */
136
+ function classificarSequencia(seq) {
137
+ if (CPF_FORMATADO.test(seq))
138
+ return 'cpf';
139
+ if (CNPJ_FORMATADO.test(seq))
140
+ return 'cnpj';
141
+ const d = soDigitos(seq);
142
+ if (d.length === 11 && cpfValido(d))
143
+ return 'cpf';
144
+ if (d.length === 14 && cnpjValido(d))
145
+ return 'cnpj';
146
+ if (d.length >= 13 && luhnValido(d))
147
+ return 'cartao';
148
+ return null;
149
+ }
150
+ /** Candidatos em cada trecho contíguo de grupos inteiros da sequência, sem quebrar um grupo por dentro. */
151
+ function trechosDeDigitos(texto) {
152
+ const achados = [];
153
+ for (const m of texto.matchAll(SEQUENCIA_DE_DIGITOS)) {
154
+ const grupos = Array.from(m[0].matchAll(GRUPO_DE_DIGITOS), (g) => ({ inicio: g.index, fim: g.index + g[0].length }));
155
+ for (let i = 0; i < grupos.length; i++) {
156
+ let digitos = 0;
157
+ for (let j = i; j < grupos.length; j++) {
158
+ const a = grupos[i];
159
+ const b = grupos[j];
160
+ if (!a || !b)
161
+ break;
162
+ digitos += b.fim - b.inicio;
163
+ if (digitos > MAX_DIGITOS)
164
+ break;
165
+ const categoria = classificarSequencia(m[0].slice(a.inicio, b.fim));
166
+ if (categoria)
167
+ achados.push({ inicio: m.index + a.inicio, fim: m.index + b.fim, categoria });
168
+ }
169
+ }
170
+ }
171
+ return achados;
172
+ }
173
+ function trechosDe(texto, re, categoria) {
174
+ return Array.from(texto.matchAll(re), (m) => ({ inicio: m.index, fim: m.index + m[0].length, categoria }));
175
+ }
176
+ function prioridade(categoria) {
177
+ return categoria === 'telefone' ? 1 : 0;
178
+ }
179
+ /**
180
+ * Trechos com dado pessoal, em ordem e sem sobreposição. Entre dois que se cruzam, telefone perde para os
181
+ * demais; na mesma prioridade, vence o mais longo e, empatado, o que começa antes.
182
+ */
183
+ export function encontrarNoTexto(texto) {
184
+ const candidatos = [
185
+ ...trechosDe(texto, EMAIL, 'email'),
186
+ ...trechosDe(texto, TELEFONE, 'telefone'),
187
+ ...trechosDeDigitos(texto),
188
+ ].sort((a, b) => prioridade(a.categoria) - prioridade(b.categoria) || b.fim - b.inicio - (a.fim - a.inicio) || a.inicio - b.inicio);
189
+ const escolhidos = [];
190
+ for (const t of candidatos) {
191
+ if (escolhidos.every((e) => t.fim <= e.inicio || t.inicio >= e.fim))
192
+ escolhidos.push(t);
193
+ }
194
+ return escolhidos.sort((a, b) => a.inicio - b.inicio);
195
+ }
196
+ /** Testa um inteiro não negativo como CPF (9 a 11 dígitos), CNPJ (12 a 14) ou cartão (13 a 19, Luhn). */
197
+ export function classificarNumero(valor) {
198
+ if (typeof valor === 'number' && !Number.isSafeInteger(valor))
199
+ return null;
200
+ if (valor < 0)
201
+ return null;
202
+ const d = valor.toString();
203
+ if (d.length >= 9 && d.length <= 11 && cpfValido(d.padStart(11, '0')))
204
+ return 'cpf';
205
+ if (d.length >= 12 && d.length <= 14 && cnpjValido(d.padStart(14, '0')))
206
+ return 'cnpj';
207
+ if (d.length >= 13 && d.length <= 19 && luhnValido(d))
208
+ return 'cartao';
209
+ return null;
210
+ }
@@ -0,0 +1,114 @@
1
+ export const OCULTO = '[OCULTO]';
2
+ const CONECTIVOS = new Set(['da', 'de', 'do', 'das', 'dos', 'e']);
3
+ function manterIntervalo(texto, inicio, fim) {
4
+ let posicao = 0;
5
+ return texto.replace(/\d/g, (digito) => {
6
+ posicao += 1;
7
+ return posicao >= inicio && posicao <= fim ? digito : '*';
8
+ });
9
+ }
10
+ function mascararCpf(texto) {
11
+ if (!/^(\d{3}\.\d{3}\.\d{3}-\d{2}|\d{11})$/.test(texto))
12
+ return OCULTO;
13
+ return manterIntervalo(texto, 4, 9);
14
+ }
15
+ function mascararCnpj(texto) {
16
+ if (!/^(\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}|\d{14})$/.test(texto))
17
+ return OCULTO;
18
+ return manterIntervalo(texto, 3, 12);
19
+ }
20
+ function mascararDocumento(texto) {
21
+ if (!/^[A-Za-z0-9./\s-]+$/.test(texto))
22
+ return OCULTO;
23
+ const total = texto.replace(/[^A-Za-z0-9]/g, '').length;
24
+ if (total < 4)
25
+ return OCULTO;
26
+ let posicao = 0;
27
+ return texto.replace(/[A-Za-z0-9]/g, (caractere) => {
28
+ posicao += 1;
29
+ return posicao > total - 2 ? caractere : '*';
30
+ });
31
+ }
32
+ function mascararEmail(texto) {
33
+ const partes = /^([^\s@]+)@([^\s@]+)$/.exec(texto);
34
+ if (!partes)
35
+ return OCULTO;
36
+ return `${[...partes[1]][0]}***@${partes[2]}`;
37
+ }
38
+ function mascararTelefone(texto) {
39
+ if (!/^\+?[\d\s()-]+$/.test(texto))
40
+ return OCULTO;
41
+ let digitos = texto.replace(/\D/g, '');
42
+ if (texto.startsWith('+')) {
43
+ if (!digitos.startsWith('55'))
44
+ return OCULTO;
45
+ digitos = digitos.slice(2);
46
+ }
47
+ if (digitos.length !== 10 && digitos.length !== 11)
48
+ return OCULTO;
49
+ return `(${digitos.slice(0, 2)}) *****-${digitos.slice(-4)}`;
50
+ }
51
+ function mascararCep(texto) {
52
+ if (!/^\d{5}-\d{3}$/.test(texto))
53
+ return OCULTO;
54
+ return `${texto.slice(0, 2)}***-***`;
55
+ }
56
+ function mascararNome(texto) {
57
+ if (!/\p{L}/u.test(texto))
58
+ return OCULTO;
59
+ const iniciais = texto
60
+ .split(/\s+/)
61
+ .filter((palavra) => palavra !== '' && !CONECTIVOS.has(palavra.toLowerCase()))
62
+ .map((palavra) => `${[...palavra][0]}***`);
63
+ return iniciais.length === 0 ? OCULTO : iniciais.join(' ');
64
+ }
65
+ function mascararNascimento(valor) {
66
+ if (valor instanceof Date) {
67
+ const ano = valor.getFullYear();
68
+ return Number.isNaN(ano) ? OCULTO : `${String(ano).padStart(4, '0')}-**-**`;
69
+ }
70
+ const partes = /^(\d{4})-\d{2}-\d{2}(T.*)?$/.exec(valor);
71
+ return partes ? `${partes[1]}-**-**` : OCULTO;
72
+ }
73
+ function mascararCartao(texto) {
74
+ if (!/^[\d\s-]+$/.test(texto))
75
+ return OCULTO;
76
+ const digitos = texto.replace(/\D/g, '');
77
+ if (digitos.length < 13 || digitos.length > 19)
78
+ return OCULTO;
79
+ return `**** **** **** ${digitos.slice(-4)}`;
80
+ }
81
+ function mascararIp(texto) {
82
+ const octetos = texto.split('.');
83
+ const valido = octetos.length === 4 && octetos.every((o) => /^\d{1,3}$/.test(o) && Number(o) <= 255);
84
+ return valido ? `${octetos[0]}.${octetos[1]}.*.*` : OCULTO;
85
+ }
86
+ const MASCARAS = {
87
+ cpf: mascararCpf,
88
+ cnpj: mascararCnpj,
89
+ documento: mascararDocumento,
90
+ email: mascararEmail,
91
+ telefone: mascararTelefone,
92
+ endereco: () => OCULTO,
93
+ cep: mascararCep,
94
+ nascimento: mascararNascimento,
95
+ nome: mascararNome,
96
+ financeiro: () => OCULTO,
97
+ cartao: mascararCartao,
98
+ ip: mascararIp,
99
+ segredo: () => OCULTO,
100
+ sensivel: () => OCULTO,
101
+ };
102
+ /**
103
+ * Mascara um valor já classificado, mantendo o formato quando possível.
104
+ * `null` e `undefined` passam intactos; valor não textual ou fora do formato vira `[OCULTO]`.
105
+ */
106
+ export function mascararValor(valor, categoria) {
107
+ if (valor === null || valor === undefined)
108
+ return valor;
109
+ if (valor instanceof Date && categoria === 'nascimento')
110
+ return mascararNascimento(valor);
111
+ if (typeof valor !== 'string')
112
+ return OCULTO;
113
+ return MASCARAS[categoria](valor.trim());
114
+ }
@@ -0,0 +1,101 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { createInterface } from 'node:readline';
3
+ import { raizDoProjeto } from './conexao.js';
4
+ import { executarFerramenta, FERRAMENTAS, INSTRUCOES } from './ferramentas.js';
5
+ const VERSOES_PROTOCOLO = ['2024-11-05', '2025-03-26', '2025-06-18'];
6
+ const VERSAO_PADRAO = '2025-06-18';
7
+ class FalhaRpc extends Error {
8
+ code;
9
+ constructor(code, message) {
10
+ super(message);
11
+ this.code = code;
12
+ }
13
+ }
14
+ function versaoPacote() {
15
+ try {
16
+ const pkg = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
17
+ return pkg.version;
18
+ }
19
+ catch {
20
+ return '0.0.0';
21
+ }
22
+ }
23
+ function isObject(value) {
24
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
25
+ }
26
+ async function responder(metodo, params, contexto) {
27
+ const parametros = isObject(params) ? params : {};
28
+ switch (metodo) {
29
+ case 'initialize': {
30
+ const pedida = parametros.protocolVersion;
31
+ return {
32
+ protocolVersion: typeof pedida === 'string' && VERSOES_PROTOCOLO.includes(pedida) ? pedida : VERSAO_PADRAO,
33
+ capabilities: { tools: {} },
34
+ serverInfo: { name: 'ai-harness-banco', version: versaoPacote() },
35
+ instructions: INSTRUCOES,
36
+ };
37
+ }
38
+ case 'ping':
39
+ return {};
40
+ case 'tools/list':
41
+ return { tools: FERRAMENTAS };
42
+ case 'tools/call': {
43
+ if (typeof parametros.name !== 'string')
44
+ throw new FalhaRpc(-32602, 'tools/call exige params.name');
45
+ return executarFerramenta(parametros.name, parametros.arguments ?? {}, contexto);
46
+ }
47
+ default:
48
+ throw new FalhaRpc(-32601, `Método não encontrado: ${metodo}`);
49
+ }
50
+ }
51
+ /** Atende JSON-RPC 2.0 com uma mensagem por linha; a saída recebe só respostas JSON-RPC. */
52
+ export function iniciarServidorMcp(opcoes) {
53
+ const contexto = opcoes.contexto ?? { raiz: raizDoProjeto() };
54
+ const enviar = (id, corpo) => {
55
+ opcoes.saida.write(`${JSON.stringify({ jsonrpc: '2.0', id, ...corpo })}\n`);
56
+ };
57
+ const atender = async (linha) => {
58
+ if (!linha.trim())
59
+ return;
60
+ let mensagem;
61
+ try {
62
+ mensagem = JSON.parse(linha);
63
+ }
64
+ catch {
65
+ enviar(null, { error: { code: -32700, message: 'JSON inválido' } });
66
+ return;
67
+ }
68
+ if (!isObject(mensagem)) {
69
+ enviar(null, { error: { code: -32600, message: 'Requisição inválida' } });
70
+ return;
71
+ }
72
+ const id = mensagem.id;
73
+ if (id === undefined)
74
+ return;
75
+ const idValido = typeof id === 'string' || typeof id === 'number' ? id : null;
76
+ if (typeof mensagem.method !== 'string') {
77
+ if (!('result' in mensagem) && !('error' in mensagem)) {
78
+ enviar(idValido, { error: { code: -32600, message: 'Requisição inválida' } });
79
+ }
80
+ return;
81
+ }
82
+ try {
83
+ enviar(idValido, { result: await responder(mensagem.method, mensagem.params, contexto) });
84
+ }
85
+ catch (falha) {
86
+ const code = falha instanceof FalhaRpc ? falha.code : -32603;
87
+ const message = falha instanceof FalhaRpc ? falha.message : 'Erro interno do servidor';
88
+ enviar(idValido, { error: { code, message } });
89
+ }
90
+ };
91
+ return new Promise((resolver, rejeitar) => {
92
+ let fila = Promise.resolve();
93
+ const leitor = createInterface({ input: opcoes.entrada, crlfDelay: Infinity });
94
+ leitor.on('line', (linha) => {
95
+ fila = fila.then(() => atender(linha));
96
+ });
97
+ leitor.on('close', () => {
98
+ fila.then(resolver, rejeitar);
99
+ });
100
+ });
101
+ }