@codeharbor-br/atualia-sdk
v0.1.2
Published
Cliente oficial do atualia: chama endpoints de proxy e consulta as collections do cliente com client_id + secret_key.
Maintainers
Readme
@codeharbor-br/atualia-sdk
Cliente oficial do atualia: chama as APIs liberadas para o seu app e consulta as collections dele.
Vem em duas formas — uma lib para usar em código e uma CLI para usar no terminal (ou por um agente de IA).
Se o que você quer é o app que roda dentro do painel do atualia, comece por App embarcado no painel: ali não existe credencial nenhuma a configurar.
npm install @codeharbor-br/atualia-sdkOs dois transportes
Não é ambiente, é onde o código roda:
| | com credencial | embarcado no painel |
| --- | --- | --- |
| montagem | criarCliente({...}) | criarClienteEmbarcado() |
| autenticação | assinatura HMAC, sempre | nenhuma: a sessão de quem está logado |
| onde roda | servidor, CLI, job | iframe do app dentro do painel |
| configuração | url, app, client_id, secret_key | nada |
A superfície é a mesma nos dois (api, collection, proxy), de propósito: o mesmo
código de tela funciona nos dois.
Não existe modo de browser com credencial. Existiu: client_id público e a origem da
página conferida contra uma lista cadastrada no app, para o app exibido fora do painel.
Esse caso deixou de existir, e com ele o modo — hoje /api/sdk/** recusa qualquer chamada
sem assinatura, e assinar exige a secret, que não cabe num bundle.
Cada credencial tem teto por minuto, e ele conta execução de API e acesso a dado. Listar APIs, ler documentação e ler esquema é livre — é o que um agente faz para descobrir o que existe antes de agir.
Credenciais
A credencial pertence a um app: no painel, abra o app e vá em Acesso. A secret aparece uma única vez.
O escopo é o app — a credencial alcança as collections e as APIs daquele app, e mais nada. Assim, vazar a credencial de um app não expõe os outros.
A secret_key é credencial de servidor, e é a única forma de chamar: sem ela não há
assinatura, e sem assinatura a API recusa. Num bundle de front-end ela fica legível para
qualquer visitante, então não vá por ali — dentro do painel o app usa
criarClienteEmbarcado(), que não precisa de credencial. O SDK avisa no console se
detectar uma secret rodando em browser.
export ATUALIA_URL=https://atualia-api.codeharbor.com.br
export ATUALIA_APP_ID=agenda # id ou slug do app
export ATUALIA_CLIENT_ID=ak_...
export ATUALIA_SECRET_KEY=sk_...No terminal dá para pular isso: atualia login pergunta os quatro campos um a um,
testa a credencial e grava em ~/.atualia/config.json (permissão 600). Variável de
ambiente sempre vence o arquivo, para CI e container não dependerem do que está na
máquina.
Como a autenticação funciona
Cada chamada é assinada (HMAC-SHA256) com a secret, que nunca trafega. A assinatura cobre método, caminho, timestamp, um nonce e o hash do corpo — então uma requisição capturada não pode ser reenviada nem alterada.
Consequências práticas:
- relógio importa: mais de 5 minutos de diferença com o servidor derruba a chamada. O erro diz isso explicitamente;
- cada chamada é única: repetir a mesma assinatura é recusado (
noncerepetido); - não dá para reproduzir com
curlcopiado — é o que faz da lib o caminho de uso, e não um detalhe de conveniência.
secretKey é obrigatório: uma chamada sem assinatura é recusada com 401, e a mensagem
explica que o caminho do browser é criarClienteEmbarcado().
App embarcado no painel
É o caso normal, e nele não existe credencial:
import { criarClienteEmbarcado } from '@codeharbor-br/atualia-sdk'
// Sem baseUrl, sem appId, sem client_id: nada a configurar.
export const atualia = criarClienteEmbarcado()
// A superfície é a mesma do cliente com credencial:
const { corpo } = await atualia.api.call('core-sql-all', { body: { sql: 'SELECT ...' } })
const { itens } = await atualia.collection('pacientes').limit(20).get()Como funciona: o app roda num iframe dentro do painel e manda cada pedido ao painel por
postMessage. Quem executa é o painel, com a sessão de quem está logado. Três
consequências:
- não há o que vazar: o bundle é público e não carrega segredo nenhum;
- fora do painel o app não funciona: sem alguém logado, ninguém executa por ele;
- o alcance é o do app: o painel resolve o app pela sessão, não por algo que o app mande.
O painel também conta ao app onde ele está:
const { app, appNome, cliente, clienteNome, usuario } = await atualia.pronto()
const cancelar = atualia.aoTrocarContexto((c) => setCliente(c.clienteNome))E o app se recusa a rodar fora do painel:
import { estaEmbarcado, hostQueEmbarca } from '@codeharbor-br/atualia-sdk'
const paineis = import.meta.env.VITE_ATUALIA_PAINEL.split(',')
if (!estaEmbarcado() || hostQueEmbarca(paineis) === false) {
// mostra o aviso em vez de tentar chamar
}ancestorOrigins (que é o que hostQueEmbarca lê) é preenchido pelo navegador e a
página não consegue falsificar. No Firefox ele não existe: ali a resposta é null e quem
decide é o handshake, que sem o painel não completa.
Para o app aparecer na barra lateral, informe o endereço publicado em Apps > o app >
Publicação. atualia create monta esse app inteiro para você — ver abaixo.
Lib
import { criarCliente } from '@codeharbor-br/atualia-sdk'
const atualia = criarCliente({
baseUrl: process.env.ATUALIA_URL!,
appId: process.env.ATUALIA_APP_ID!,
clientId: process.env.ATUALIA_CLIENT_ID!,
secretKey: process.env.ATUALIA_SECRET_KEY!,
})
await atualia.ping() // confere credencial, assinatura e relógioAPIs
O destino real (URL, headers, credencial) fica no servidor. Você manda a chave e os valores dos campos declarados.
A chamada aceita os parâmetros separados por seção, o que deixa o contrato visível no próprio código:
const { corpo } = await atualia.api.call('core-pedido-criar', {
method: 'POST', // conferido contra o cadastro
headers: { instancia: 'erp_cliente01' }, // vira header na saída
params: { pagina: 1 }, // vira query string
path: { codigo: 42 }, // entra no caminho da URL
body: { estmov, estimo }, // vira corpo JSON
})As seções são headers, params (query), path e body. O method é
declarativo: o método real vem do cadastro (é o que impede trocar um GET por um
DELETE), e declará-lo faz o servidor conferir — se divergir, a chamada é recusada
com mensagem clara em vez de executar a operação que você não quis.
Também aceita a forma plana, quando você não se importa com a divisão:
await atualia.api.call('core-pedido-criar', { instancia: 'erp_cliente01', estmov, estimo })Quem posiciona de fato é o servidor, a partir do cadastro — a divisão é contrato
legível, não roteamento. Um campo declarado em body que o cadastro diz ser de
query ainda vai para a query.
Campos que vêm do ambiente do app
Um campo com variavelAmbiente preenchido é obrigatório mas não precisa ser
enviado: o valor está configurado no app (painel → o app → Ambiente) e o servidor o
aplica na saída.
É o caso da instancia do Core, obrigatória em todos os endpoints dele.
const pedido = await atualia.api.get('core-pedido-criar')
pedido?.parametros
.filter((p) => p.variavelAmbiente)
.forEach((p) => console.log(p.nome, '← ambiente:', p.variavelAmbiente))
// instancia ← ambiente: instancia
// Com a instância configurada, a chamada não a repete:
await atualia.api.call('core-sql-one', { sql: 'SELECT 1 FROM dual' })Enviar o valor continua valendo e tem preferência sobre a variável — é assim que se
chama em nome de outra instância sem mexer na configuração.
atualia.api.tools() já deixa esses campos fora do required, e a CLI os mostra como
obrig.: ambiente em vez de pedir o valor.
Descobrindo o que existe:
const endpoints = await atualia.api.list() // as do app + as compartilhadas
const consulta = await atualia.api.get('core-sql-all')
console.log(consulta?.documentacao) // documentação completa, em Markdown
console.log(consulta?.exemploResposta) // forma da resposta, sem precisar chamarPara dar os endpoints a um modelo como ferramentas:
const tools = await atualia.api.tools() // name, description e input_schema
atualia.proxyé o mesmo objeto, mantido como apelido: o painel de quem publica chama isso de proxy, porque descreve a mecânica. Para quem consome, é API.
Collections
Cada collection é uma tabela real no seu schema no Postgres. Você a endereça só pelo nome: o app vem da credencial, então não existe forma de pedir a collection de outro app.
const pacientes = atualia.collection('pacientes')
const { total, itens } = await pacientes
.where('ativo', '=', true)
.where('idade', '>=', 18)
.orderBy('nome')
.limit(50)
.get()
const um = await pacientes.where('cpf', '=', '11111111111').first()
const quantos = await pacientes.count()
await pacientes.insert({ nome: 'Maria Souza', cpf: '11111111111', idade: 40 })
await pacientes.update(id, { idade: 41 })
await pacientes.delete(id)Operadores: =, !=, >, >=, <, <=, contem, comeca_com,
termina_com, em, nulo, nao_nulo.
Campo não declarado, tipo errado ou obrigatório ausente são recusados pelo servidor com mensagem dizendo qual campo — a validação não é só do cliente.
Relatórios
O template é desenhado no Jaspersoft Studio e configurado no painel do app: lá se diz de
onde vem cada campo do .jrxml e quais filtros o relatório aceita. Daqui você só escolhe
formato e filtros.
const relatorios = await atualia.relatorio.list()
const contrato = await atualia.relatorio.get('faturamento') // filtros, tipos, formatos
const { url, linhas, expiraEm } = await atualia.relatorio.gerar('faturamento', {
formato: 'XLSX', // PDF (padrão) ou XLSX
filtros: { data_de: '2026-01-01', data_ate: '2026-01-31' },
})A resposta é uma URL assinada e temporária, nunca o arquivo: o download não passa pelo
backend, e a URL pode ser entregue a quem pediu. linhas responde "o filtro cortou tudo?"
sem baixar nada.
Filtro não declarado é recusado, e obrigatório sem valor também — a mensagem diz qual. Fonte, colunas e ordenação vêm do cadastro; quem chama não escolhe, do mesmo jeito que não escolhe o destino de uma API.
CLI
O pacote instala o binário atualia.
atualia # estado atual: APIs e collections
atualia create meu-app # gera um app React+Vite documentado deste app
atualia login # pergunta url, app, client_id e secret_key
atualia config # mostra o que está valendo e de onde veio
atualia ping # credencial, assinatura e relógio
atualia api list # APIs disponíveis
atualia api search pedido # procura por nome, resumo ou campo
atualia api doc core-sql-all # documentação, campos por seção e retorno
atualia api doc core-sql-all --full # documentação inteira, sem truncar
atualia api call core-sql-all --sql 'SELECT cliente, nome FROM cadcli LIMIT 20'
atualia db list # collections deste app
atualia db doc pacientes # campos, tipos, documentação e operadores
atualia db query pacientes --where ativo=true --order nome:asc --limit 20
atualia db insert pacientes --json '{"nome": "Maria"}'
atualia db update pacientes <id> --json '{"idade": 41}'
atualia db delete pacientes <id>
atualia relatorio list # relatórios publicados neste app
atualia relatorio doc faturamento # filtros aceitos, tipos e documentação
atualia relatorio publicar relatorios/faturamento.json # sobe o .jrxml e o de-para
atualia relatorio gerar faturamento --data_de 2026-01-01 --data_ate 2026-01-31
atualia relatorio gerar faturamento --formato xlsx
atualia tools # definições de ferramenta (JSON) para um modeloatualia relatorio publicar é o comando de quem escreveu o template: o .jrxml
mais um manifesto JSON com a fonte, o de-para dos campos e os filtros, numa chamada.
O servidor compila o template e valida o mapeamento inteiro antes de gravar, e
republicar o mesmo identificador substitui — ajustar o layout e rodar de novo não
duplica o cadastro. No painel o ciclo continua sendo em dois passos (subir o arquivo,
depois mapear), que é o certo para quem desenhou no Jaspersoft Studio e ainda não sabe
quais campos o template declara.
atualia api doc <chave> e atualia db doc <collection> são os comandos que
respondem o que chamar e o que esperar de cada campo: resumo, documentação,
campos agrupados por seção (headers, params, path, body) com tipo,
obrigatoriedade, exemplo e valores aceitos, exemplo de retorno e o trecho de
código pronto.
Filtros da consulta: --where campo=valor (igualdade) ou
--where campo=operador:valor, repetível — os filtros somam com AND. Para em,
separe os valores com |. Os operadores nulo e nao_nulo não levam valor.
--order campo:desc, --limit e --offset completam a consulta.
atualia create
Gera um projeto React + Vite pronto para o Cloudflare Pages, que nasce sabendo o app e com a mesma stack do painel — shadcn/ui (Tailwind v4), TanStack Query, React Router e axios:
.env.local o endereço do painel — e nada mais: o app não tem credencial
components.json config do shadcn: `npx shadcn add <componente>` já funciona
src/components/layout/ a casca: sidebar, barra superior e a lista de telas
src/components/ui/ os do painel (button, card, table, input, select, field...)
+ combobox.tsx: escolher UM registro buscando, que o registry não tem
src/lib/api.ts cliente embarcado (sem credencial) + axios + mensagemDoErro
src/lib/contexto.ts useContexto(): app, cliente e o usuário logado
src/lib/query-client.ts padrões do TanStack Query
src/lib/utils.ts cn(), que todo componente do shadcn importa
src/paginas/ uma tela por arquivo: inicio, exemplo-cadastro, fora-do-painel
src/App.tsx o mapa de rotas
src/main.tsx portão de entrada, providers e router
src/iframe.ts links para fora e storage dentro do iframe
src/index.css tema (tokens do painel), tema claro só, altura do frame
public/_redirects fallback de SPA — sem ele, sub-rota dá 404 depois do deploy
.claude/skills/erp/ como atacar tarefa que toca o legado do cliente
AGENTS.md instruções para um agente de IA trabalhar no projeto
wrangler.toml npm run deploy publica no PagesAs duas telas de src/paginas são exemplo, e se declaram exemplo: inicio lista dado
real (o ERP do cliente, ou a primeira collection) e exemplo-cadastro é o padrão de tela
deste projeto — formulário com react-hook-form + zod, Combobox para escolher um
registro e Select para lista fechada. Não há pasta docs/: contrato de API e de
collection é dado do servidor, e quem responde o que existe hoje é npx atualia.
A stack não é preferência: o app é exibido dentro do painel, então componente copiado de lá cola aqui e a tela não parece outro produto colado na página. A tela inicial já lista dados reais da primeira collection.
Não há lista de endpoints em arquivo, de propósito: as APIs e as collections mudam no
painel sem este código mudar, e uma cópia congelada passaria a mentir. O que o projeto
ensina é como perguntar — pela CLI e pelo painel /docs, que lê do servidor a cada
abertura.
Não roda npm install: os comandos ficam impressos no fim.
Formatos de saída
| destino | formato | como forçar |
| --- | --- | --- |
| terminal | tabelas alinhadas, com cor | --humano |
| pipe, arquivo, script | JSON | --json |
| agente de IA | TOON — ~40% menos tokens | --toon |
A escolha é pelo destino, não por preferência: atualia api list num terminal sai
legível, e atualia api list \| jq sai JSON sem precisar de flag. Quem paga token
por caractere pede --toon e recebe a mesma informação em menos espaço.
Convenções da CLI
- sem argumento mostra estado, não manual;
- erros vão para stdout no mesmo formato dos dados, com o comando que resolve;
- stderr é só diagnóstico — nada de dado por lá;
- exit codes:
0sucesso,1erro,2erro de uso; - prompt só existe com terminal: em pipeline, valor faltando é erro imediato — travar esperando um input que não vem é pior que falhar com mensagem.
MCP
O pacote instala também o binário atualia-mcp: o mesmo catálogo da CLI, servido pelo
protocolo MCP para um agente que não tem shell (Claude Code, Claude Desktop, qualquer
cliente MCP).
Instalar no Claude Code
1. Instale o pacote. O binário precisa existir no PATH antes do registro — o Claude Code executa o comando que você registrar, não resolve pacote por conta.
npm i -g @codeharbor-br/atualia-sdk
which atualia-mcp # tem que imprimir um caminhoNão rode atualia-mcp na mão para "testar": ele é um servidor de stdio e fica esperando
mensagem no stdin, parado.
Quem conversa com ele é o cliente MCP.
2. Configure a credencial.
O servidor MCP lê a mesma configuração da CLI, então login uma vez serve para os dois:
atualia login # pergunta url, app, client_id e secret_key; grava em ~/.atualia/config.json3. Registre o servidor. Rode de dentro do projeto onde ele deve valer:
claude mcp add atualia -- atualia-mcpTudo depois de -- é o comando que o Claude Code executa; sem esse separador, o resto da
linha vira argumento do add.
O escopo decide quem enxerga o servidor, e a escolha importa porque a credencial aponta para um app só:
| escopo | onde vale | quando usar |
| --- | --- | --- |
| --scope local (padrão) | só você, só neste projeto | o normal: um app por projeto |
| --scope project | todo mundo, via .mcp.json versionado | time inteiro no mesmo app |
| --scope user | você, em todos os projetos | uma credencial que serve para tudo que você faz |
4. Confirme que subiu.
claude mcp list # atualia deve aparecer como ✔ Connected
claude mcp get atualia # escopo, comando e variáveis em usoDentro de uma sessão, /mcp mostra o mesmo e lista as ferramentas.
A partir daí o agente chama atualia_status e segue do catálogo.
Credencial na configuração do servidor
Em CI, container ou máquina compartilhada, onde não há atualia login a rodar, as
variáveis vão no próprio registro — elas têm precedência sobre o arquivo:
claude mcp add atualia \
-e ATUALIA_URL=https://atualia-api.codeharbor.com.br \
-e ATUALIA_APP_ID=agenda \
-e ATUALIA_CLIENT_ID=ak_... \
-e ATUALIA_SECRET_KEY=sk_... \
-- atualia-mcpCom --scope project isso entra num .mcp.json que vai para o git, e aí a secret_key
literal não pode estar lá.
Duas saídas: versionar só ATUALIA_URL e ATUALIA_APP_ID e deixar a credencial de cada
pessoa no atualia login, ou usar a expansão que o Claude Code faz no .mcp.json
(${VAR} e ${VAR:-padrão}, em command, args, env, url e headers):
{
"mcpServers": {
"atualia": {
"command": "atualia-mcp",
"env": {
"ATUALIA_URL": "${ATUALIA_URL:-https://atualia-api.codeharbor.com.br}",
"ATUALIA_APP_ID": "agenda",
"ATUALIA_CLIENT_ID": "${ATUALIA_CLIENT_ID}",
"ATUALIA_SECRET_KEY": "${ATUALIA_SECRET_KEY}"
}
}
}
}Variável sem valor e sem padrão não impede o arquivo de carregar: o claude mcp list
avisa e o texto ${VAR} vai cru para o servidor, que responde erro de credencial.
Outros clientes MCP
Cliente que se configura por arquivo (claude_desktop_config.json, o .mcp.json de um
projeto, a configuração de MCP do seu editor) recebe o mesmo servidor:
{
"mcpServers": {
"atualia": {
"command": "atualia-mcp",
"env": { "ATUALIA_APP_ID": "agenda", "ATUALIA_CLIENT_ID": "ak_..." }
}
}
}Trocar de app, atualizar, remover
atualia login # aponta a credencial para outro app
claude mcp remove atualia # sem -s, remove do escopo em que estiver
claude mcp add atualia -- atualia-mcpTrocar a credencial não exige reinstalar nada: o servidor lê a configuração ao subir.
Depois de atualia login, saia e volte à sessão para o servidor reiniciar.
Quando não conecta
| sintoma | causa provável |
| --- | --- |
| Status: ✘ Failed to connect | atualia-mcp não está no PATH do processo que abriu o Claude Code — teste com which atualia-mcp no mesmo terminal |
| conecta, mas toda ferramenta responde "Configuração ausente" | não há ~/.atualia/config.json nem variáveis no registro; rode atualia login ou passe -e |
| ferramentas somem depois de um npm i -g | o link do binário mudou; claude mcp remove e add de novo |
| erro 401 em toda chamada | credencial de outro app ou revogada — confira com atualia config e atualia ping |
Para ver o que o servidor fala, claude --debug mostra a conversa e o stderr do processo.
Nada além de JSON-RPC vai para o stdout, então não espere log por lá.
As ferramentas
| ferramenta | o que faz |
| --- | --- |
| atualia_status | catálogo do app: credencial em uso, APIs e collections com resumo |
| atualia_api_doc | contrato de uma API: documentação, campos, exemplo de retorno |
| atualia_api_call | executa a API pela chave, com os campos declarados |
| atualia_db_doc | esquema de uma collection |
| atualia_db_query | consulta com filtros, ordenação e paginação |
| atualia_db_insert / update / delete | escrita nas collections |
| atualia_relatorio_doc | contrato de um relatório: filtros, tipos e formatos |
| atualia_relatorio_gerar | gera o relatório e devolve a URL temporária |
atualia_status é a porta de entrada: dele saem as chaves de API, os nomes de collection e
os nomes de relatório que as outras ferramentas recebem.
Não existe ferramenta de listagem separada porque o status já traz as três listas.
Vale para o MCP tudo o que vale para a CLI: a credencial define o app, o app define o
alcance, e a documentação de cada API e collection vem do servidor.
A resposta de cada ferramenta é TOON, pela mesma razão da CLI.
Erro de execução volta como conteúdo com isError, e não como erro de protocolo, para o
modelo poder ler a mensagem e corrigir a chamada.
Documentação vem do servidor
Toda API e toda collection carregam a documentação escrita por quem as cadastrou:
documentacao (Markdown), exemploResposta e, por campo, descricao e exemplo.
É o que permite descobrir o contrato sem sair da ferramenta — e, na API
compartilhada, é a única documentação disponível, já que a URL de destino fica
oculta.
Erros
import { AtualiaError } from '@codeharbor-br/atualia-sdk'
try {
await atualia.api.call('core-pedido-criar', { body: { estimo } }) // falta a instância
} catch (erro) {
if (erro instanceof AtualiaError) {
console.error(erro.status, erro.message) // 400 Parâmetro obrigatório ausente: 'Instância'
}
}Status do sistema de destino não é exceção: um 404 ou 500 do ERP chega em
resposta.status, para você distinguir "a chamada falhou" de "o destino respondeu
erro".
Dois status que valem tratar explicitamente:
- 429 — teto por minuto da credencial. O header
Retry-Afterdiz quantos segundos esperar; - 401 — credencial, assinatura ou relógio. A mensagem distingue o que é erro de configuração (relógio fora da janela, app divergente, chamada sem assinatura) do que é credencial inválida.
