@tzam-org/audit
v0.2.0
Published
Trilha de auditoria agnóstica: evento validado, redação de segredos, gravação durável (falha bloqueia) ou best-effort, sink por interface implementado em cada app.
Readme
@tzam-org/audit
Trilha de auditoria: valida o evento, redige segredos e decide o que uma falha de gravação significa (bloqueia a operação ou segue em best-effort).
Instalação
pnpm add @tzam-org/auditSem dependências de runtime (usa node:crypto).
Garantias
- Gravação durável.
recordlançaAuditWriteErrorquando o sink falha, para que o chamador não siga como se o evento estivesse registrado.recordBestEffortdevolvenulle avisa poronBestEffortError, para eventos que não devem bloquear. - Evento validado.
actionprecisa ser um verbo com namespace em minúsculas (role.update,authorization.check),actor.idé obrigatório eoutcomeésuccess,failureoudenied. - Redação de segredos em
before,afteremetadata: chaves que parecem segredo (password,secret,token,authorization,cookie,api_key,private_key,code_verifier,credential) e valores que parecem segredo (JWT,Bearer/Basic, chave privada PEM) viram[REDACTED]. Strings acima de 2000 caracteres são cortadas, ciclos viram[CYCLE]e profundidade acima de 8 vira[TRUNCATED].
Uso
O app implementa AuditSink (uma tabela própria). O exemplo usa memoryAuditSink, que vem no
pacote, e mostra um sink escrito à mão:
import { createAuditRecorder, delegateAuditSink, memoryAuditSink, type AuditEvent, type AuditRow, type AuditSink } from '@tzam-org/audit';
const sink = memoryAuditSink();
const audit = createAuditRecorder({
sink,
onBestEffortError: (error, event) => console.error('audit falhou', event.action, error),
});
// Durável: se o sink falhar, lança AuditWriteError e a operação não continua.
await audit.record({
action: 'role.update',
actor: { type: 'user', id: 'u1', label: '[email protected]' },
outcome: 'success',
target: { type: 'role', id: 'r1' },
before: { name: 'Leitor' },
after: { name: 'Editor', apiKey: process.env.SECRET_KEY }, // apiKey é gravado como [REDACTED]
});
// Best-effort: em falha, devolve null e chama onBestEffortError.
await audit.recordBestEffort({ action: 'session.view', actor: { type: 'system', id: 'job' }, outcome: 'success' });
// Um sink próprio é só um write().
const rows: AuditEvent[] = [];
const mySink: AuditSink = { write: async (event) => void rows.push(event) };
const auditToMine = audit.withSink(mySink);
// Para um ORM com `create({ data })` (ex.: prisma.auditEvent, ou tx.auditEvent numa transação):
const table = { create: async (args: { data: AuditRow }) => args.data };
const auditToTable = audit.withSink(delegateAuditSink(table));withSink devolve o mesmo gravador com outro sink, por exemplo um preso à transação da mudança.
User-Agent de um evento
parseUserAgent lê o User-Agent guardado num evento e diz o que a lista de auditoria mostra. É uma função pura,
sem dependência, para chamar no servidor uma vez por linha:
import { parseUserAgent } from '@tzam-org/audit';
parseUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/154.0.0.0 Safari/537.36');
// { kind: 'browser', browser: 'chrome', browserVersion: '154', os: 'macos' }
parseUserAgent('curl/8.7.1'); // { kind: 'server', browser: null, browserVersion: null, os: null }
parseUserAgent(null); // { kind: 'unknown', ... }kind:browser,server(o texto começa com uma ferramenta ou biblioteca HTTP:node,undici,node-fetch,curl,axios,python-requests,Go-http-client,okhttp,PostmanRuntime…),bot(compatible; Algumbot/…, nomes conhecidos eHeadlessChrome) ouunknown.browser:chrome,edge,firefox,safari,opera,braveounull(inclusive outros Chromium, como Samsung Internet e Vivaldi, que não são chamados de Chrome).browserVersioné só a versão maior.- Brave quase nunca é reconhecido. Desde 2020 o Brave manda o mesmo User-Agent do Chrome (de propósito, contra
fingerprinting); só algumas versões e contextos incluem o token
Brave. Na prática ele aparece como Chrome; a regra debravesó casa quando o token vem. os:macos,windows,linux,ios,android,chromeosounull.- Lê só os primeiros
USER_AGENT_PARSE_MAX(512) caracteres; aceita qualquer valor (coluna JSON) e nunca lança. - Não traz texto: o rótulo e os ícones ficam com a tela (
@tzam-org/ui/user-agent).
API
| Nome | Tipo | O que faz |
|---|---|---|
| createAuditRecorder({ sink, clock?, newId?, onBestEffortError? }) | função | Cria o gravador: record, recordBestEffort, withSink. |
| AuditRecorder | tipo | Retorno de createAuditRecorder. |
| AuditRecorderOptions | interface | Opções de createAuditRecorder. |
| AuditSink | interface | write(event): o que o app implementa. |
| AuditInput | interface | Entrada: action, actor, outcome, target?, reason?, before?, after?, policyVersion?, requestId?, metadata?. |
| AuditEvent | interface | AuditInput + id + occurredAt, já redigido. |
| AuditActor | interface | { type: 'user' \| 'service' \| 'system', id, label? }. |
| AuditOutcome | tipo | 'success' \| 'failure' \| 'denied'. |
| redact(value) | função | Cópia de value com segredos trocados por [REDACTED] e datas em ISO. |
| memoryAuditSink() | função | Sink em memória (events), para testes. |
| toAuditRow(event) | função | Evento → linha plana (AuditRow), sem campos undefined. |
| AuditRow | interface | Linha plana de uma tabela de auditoria (actorType, actorId, targetType…). |
| delegateAuditSink(delegate) | função | Sink sobre qualquer { create({ data }) }. |
| parseUserAgent(ua) | função | User-Agent → { kind, browser, browserVersion, os } (UserAgentInfo). |
| UserAgentInfo, UserAgentKind, BrowserId, OsId | tipos | Resultado de parseUserAgent. |
| USER_AGENT_PARSE_MAX | constante | Quantos caracteres parseUserAgent lê (512). |
| AuditValidationError | classe | Evento inválido. |
| AuditWriteError | classe | O sink falhou em record; a causa fica em cause. |
Erros
AuditValidationError:actionfora do formato,actor.idausente ououtcomeinválido. É erro de programação; lançado porrecorde também porrecordBestEffort.AuditWriteError: lançado só porrecordquandosink.writefalha. O erro original está emerror.cause. Trate como falha da operação que estava sendo auditada.
Faz parte dos módulos @tzam-org/*
(github.com/Tzam-St/packages-modules): sem
framework e sem texto de produto.
