HMAC-SHA256 em Node.js (CommonJS + ESM + axios)
Receita reutilizável em Node 18+ usando o módulo nativo
crypto e fetch embutido. Versões
anteriores podem usar axios sem mudar a
assinatura.
Pré-requisitos
- Node.js 18+ (para
fetchnativo). Em 16/14, usenode-fetchouaxios. - Sem dependências para HMAC —
node:cryptoé stdlib. - Relógio sincronizado por NTP (janela ±300s).
1. Versão CommonJS — Node 18+ com fetch nativo
const crypto = require('node:crypto');
const API_KEY = 'svp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const API_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
function signRequest(method, urlOrPath, body = '') {
const ts = String(Math.floor(Date.now() / 1000));
// new URL(path, base) aceita absoluta OU relativa -- extraimos
// sempre pathname + search, identico ao REQUEST_URI no servidor.
const u = new URL(urlOrPath, 'http://placeholder.local');
const pathQ = u.pathname + u.search;
const payload = `${ts}.${method.toUpperCase()} ${pathQ}\n${body}`;
const signature = crypto
.createHmac('sha256', API_SECRET)
.update(payload, 'utf8')
.digest('hex'); // hex lowercase por padrao no Node
return {
'X-API-Key': API_KEY,
'X-Timestamp': ts,
'X-Signature': signature,
};
}
// Uso com fetch nativo (Node 18+):
(async () => {
const url = 'https://api.sivoe.med.br/v1/ping';
const headers = signRequest('GET', url);
const resp = await fetch(url, { headers });
const data = await resp.json();
console.log(resp.status, data);
})();
Saída esperada
200 {
data: { ok: true, service: 'Sivoe API Gateway', version: 'v1', ... },
meta: { request_id: '...', duracao_ms: 12 },
error: null
}
2. Versão ESM (import)
import { createHmac } from 'node:crypto';
const API_KEY = 'svp_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
const API_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
export function signRequest(method, urlOrPath, body = '') {
const ts = String(Math.floor(Date.now() / 1000));
const u = new URL(urlOrPath, 'http://placeholder.local');
const pathQ = u.pathname + u.search;
const payload = `${ts}.${method.toUpperCase()} ${pathQ}\n${body}`;
const signature = createHmac('sha256', API_SECRET)
.update(payload, 'utf8')
.digest('hex');
return {
'X-API-Key': API_KEY,
'X-Timestamp': ts,
'X-Signature': signature,
};
}
// package.json: { "type": "module" } -- ou arquivo .mjs
const url = 'https://api.sivoe.med.br/v1/ping';
const headers = signRequest('GET', url);
const resp = await fetch(url, { headers });
console.log(resp.status, await resp.json());
3. POST com body JSON
const url = 'https://api.sivoe.med.br/v1/ping';
// Serialize ANTES de assinar -- garante igualdade byte a byte
// entre o que voce assinou e o que vai pela rede.
const body = JSON.stringify({ hello: 'world', numero: 42 });
const headers = {
...signRequest('POST', url, body),
'Content-Type': 'application/json',
};
const resp = await fetch(url, {
method: 'POST',
headers,
body, // exatamente a string que foi assinada
});
console.log(resp.status, await resp.json());
4. Alternativa com axios (Node 14/16/18+)
const axios = require('axios');
const crypto = require('node:crypto');
// signRequest() identico a secao 1
(async () => {
const url = 'https://api.sivoe.med.br/v1/ping';
const headers = signRequest('GET', url);
try {
const resp = await axios.get(url, { headers, timeout: 10000 });
console.log(resp.status, resp.data);
} catch (err) {
// axios eleva 4xx/5xx -- inspecione err.response
console.error(err.response?.status, err.response?.data);
}
})();
axios e
transformRequest:
axios por padrão transforma data: com
JSON.stringify(). Se você assinar uma string e
passar como objeto, a transformação muda o body. Para
POSTs assinados, sempre passe a string já
serializada em data.
Pegadinhas comuns
-
Date.now()é em milissegundos. SempreMath.floor(Date.now() / 1000)antes de virar string. -
new URL(path, 'http://placeholder')é o truque para aceitar tanto URL absoluta quanto path relativo sem erro. O hostname placeholder é descartado. -
.digest('hex')já é lowercase no Node. Não precisatoLowerCase(). -
Em
fetchnativo,bodydeve serstringouBodyInit. Se passar objeto JS direto, vai virar[object Object]— a assinatura quebra. -
TypeScript:
createHmac('sha256', secret)aceitastring | Buffer | KeyObject; passando string usa UTF-8 internamente — mesmo comportamento da receita Python.
Troubleshooting
| Erro recebido | Causa provável |
|---|---|
AUTH_INVALID_SIGNATURE |
Body passado como objeto (não-string); query string fora do pathQ; Date.now() sem dividir por 1000 |
AUTH_TIMESTAMP_EXPIRED |
Relógio fora ±300s — sincronize via NTP |
fetch is not defined |
Node < 18 — atualize ou use node-fetch/axios |
Catálogo completo: Códigos de erro.