neovault 0.1.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.
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # neovault
2
+
3
+ SDK, CLI e servidor MCP oficiais do **NeoVault** — cofre de identidade, consentimento e comportamento pseudonimizado (LGPD by design). Os sistemas consomem dados pessoais por API com finalidade auditada, em vez de guardar o próprio cadastro.
4
+
5
+ ```sh
6
+ npm install neovault
7
+ ```
8
+
9
+ ## SDK
10
+
11
+ ```ts
12
+ import { NeoVault } from 'neovault';
13
+
14
+ const nv = new NeoVault({ token: process.env.NEOVAULT_TOKEN!, purpose: 'atendimento' });
15
+
16
+ const { id_nome } = await nv.identities.register({
17
+ nome: 'Maria Silva', cpf: '52998224725', dataNascimento: '1990-01-01',
18
+ });
19
+ await nv.contacts.upsert('minha-loja', id_nome, {
20
+ celular: '+5585999998888', canalPref: 'whatsapp', optinMkt: true,
21
+ });
22
+ const perfil = await nv.prefs.profile('minha-loja', id_nome); // pseudonimizado, sem PII
23
+
24
+ // Opt-in em grupo: aprovação ÚNICA para uma rede nomeada de empresas
25
+ await nv.optin.aprovar('rede-neovault', { idNome: id_nome, evidencia: 'termo-v1:hash:...' });
26
+
27
+ // Eliminação LGPD (crypto-shredding + cascata automática)
28
+ await nv.identities.shred(id_nome);
29
+ ```
30
+
31
+ ## CLI
32
+
33
+ ```sh
34
+ export NEOVAULT_TOKEN=<jwt do realm>
35
+ npx neovault health
36
+ npx neovault identity resolve --id <id_nome> --purpose atendimento
37
+ npx neovault optin aprovar --grupo rede-neovault --id <id_nome> --evidencia termo-v1
38
+ npx neovault help
39
+ ```
40
+
41
+ Operadores emitem tokens com `EXT_JWT_SECRET=<segredo> npx neovault mint --sub eduuw --scopes contact:read,prefs:read --tenants eduue`.
42
+
43
+ ## MCP (agentes)
44
+
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "neovault": {
49
+ "command": "npx",
50
+ "args": ["-y", "--package=neovault", "neovault-mcp"],
51
+ "env": { "NEOVAULT_TOKEN": "<jwt do realm>" }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ Ferramentas: `vault_register/resolve/shred`, `contact_upsert/get`, `optin_aprovar/revogar/status`, `prefs_profile`, `qsa_pessoa/empresa`.
58
+
59
+ ## Tokens e escopos
60
+
61
+ JWT HS256 do realm NeoVault com escopos mínimos: `vault:register|resolve|shred`, `contact:read|write|delete`, `optin:read|write|admin`, `prefs:read`, `qsa:read|write` — e tenants autorizados. Peça o seu ao operador em https://neovault.com.br.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI do NeoVault. Env:
4
+ * NEOVAULT_TOKEN JWT do realm (obrigatório, exceto `mint`)
5
+ * NEOVAULT_API base da API (default https://api.neovault.com.br)
6
+ * EXT_JWT_SECRET segredo do realm (só para `mint` — operador)
7
+ */
8
+ import { parseArgs } from 'node:util';
9
+ import { NeoVault, NeoVaultError } from './client.js';
10
+ import { decode, mint } from './mint.js';
11
+ const USAGE = `neovault <comando> [opções]
12
+
13
+ mint --sub <sistema> --scopes a,b [--tenants x,y] [--purpose p] [--ttl 3600]
14
+ whoami
15
+ health
16
+ identity register --nome N --cpf C --nascimento YYYY-MM-DD [--source s]
17
+ resolve --id <id_nome> [--purpose p]
18
+ shred --id <id_nome> [--purpose p]
19
+ contact get|delete --tenant T --id <id_nome>
20
+ upsert --tenant T --id <id_nome> [--celular +55...] [--email e] --canal whatsapp|email|sms [--optin]
21
+ optin grupo --grupo G [--descricao D]
22
+ membro --grupo G --tenant T [--encerrar]
23
+ aprovar --grupo G --id <id_nome> --evidencia E
24
+ revogar --grupo G --id <id_nome>
25
+ status --grupo G --id <id_nome>
26
+ prefs profile --tenant T --id <id_nome>
27
+ qsa pessoa --id <id_nome>
28
+ empresa --cnpj C [--todos]
29
+
30
+ Env: NEOVAULT_TOKEN, NEOVAULT_API, EXT_JWT_SECRET (mint).`;
31
+ function die(msg) {
32
+ console.error(msg);
33
+ process.exit(1);
34
+ }
35
+ function out(v) {
36
+ console.log(typeof v === 'string' ? v : JSON.stringify(v ?? { ok: true }, null, 2));
37
+ }
38
+ async function main() {
39
+ const argv = process.argv.slice(2);
40
+ const cmd = argv[0];
41
+ const sub = argv[1] && !argv[1].startsWith('--') ? argv[1] : undefined;
42
+ const flags = parseArgs({
43
+ args: argv.slice(sub ? 2 : 1),
44
+ options: {
45
+ sub: { type: 'string' }, scopes: { type: 'string' }, tenants: { type: 'string' },
46
+ purpose: { type: 'string' }, ttl: { type: 'string' },
47
+ nome: { type: 'string' }, cpf: { type: 'string' }, nascimento: { type: 'string' },
48
+ source: { type: 'string' }, id: { type: 'string' }, tenant: { type: 'string' },
49
+ celular: { type: 'string' }, email: { type: 'string' }, canal: { type: 'string' },
50
+ optin: { type: 'boolean' }, grupo: { type: 'string' }, descricao: { type: 'string' },
51
+ evidencia: { type: 'string' }, encerrar: { type: 'boolean' },
52
+ cnpj: { type: 'string' }, todos: { type: 'boolean' },
53
+ },
54
+ strict: true,
55
+ }).values;
56
+ if (!cmd || cmd === 'help' || cmd === '--help')
57
+ return void console.log(USAGE);
58
+ if (cmd === 'mint') {
59
+ const secret = process.env.EXT_JWT_SECRET ?? die('EXT_JWT_SECRET não definido (segredo vem do cofre, nunca de flag)');
60
+ if (!flags.sub || !flags.scopes)
61
+ die('mint exige --sub e --scopes');
62
+ return void out(mint(secret, {
63
+ sub: flags.sub, scopes: flags.scopes.split(','),
64
+ tenants: flags.tenants?.split(','), purpose: flags.purpose,
65
+ ttlSeconds: flags.ttl ? Number(flags.ttl) : undefined,
66
+ }));
67
+ }
68
+ const token = process.env.NEOVAULT_TOKEN ?? die('NEOVAULT_TOKEN não definido');
69
+ if (cmd === 'whoami')
70
+ return void out(decode(token) ?? die('token ilegível'));
71
+ const nv = new NeoVault({ token, baseUrl: process.env.NEOVAULT_API, purpose: flags.purpose, source: flags.source });
72
+ const need = (v, n) => v ?? die(`--${n} é obrigatório`);
73
+ switch (`${cmd} ${sub ?? ''}`.trim()) {
74
+ case 'health': return void out((await nv.health()) ? 'online' : 'fora do ar');
75
+ case 'identity register':
76
+ return void out(await nv.identities.register({
77
+ nome: need(flags.nome, 'nome'), cpf: need(flags.cpf, 'cpf'),
78
+ dataNascimento: need(flags.nascimento, 'nascimento'), source: flags.source,
79
+ }));
80
+ case 'identity resolve': return void out(await nv.identities.resolve(need(flags.id, 'id'), flags.purpose));
81
+ case 'identity shred': return void out(await nv.identities.shred(need(flags.id, 'id'), flags.purpose));
82
+ case 'contact get': return void out(await nv.contacts.get(need(flags.tenant, 'tenant'), need(flags.id, 'id')));
83
+ case 'contact delete': return void out(await nv.contacts.delete(need(flags.tenant, 'tenant'), need(flags.id, 'id')));
84
+ case 'contact upsert':
85
+ return void out(await nv.contacts.upsert(need(flags.tenant, 'tenant'), need(flags.id, 'id'), {
86
+ celular: flags.celular, email: flags.email, canalPref: need(flags.canal, 'canal'), optinMkt: !!flags.optin,
87
+ }));
88
+ case 'optin grupo': return void out(await nv.optin.upsertGrupo(need(flags.grupo, 'grupo'), flags.descricao));
89
+ case 'optin membro':
90
+ return void out(await nv.optin.setMembro(need(flags.grupo, 'grupo'), need(flags.tenant, 'tenant'), !flags.encerrar));
91
+ case 'optin aprovar':
92
+ return void out(await nv.optin.aprovar(need(flags.grupo, 'grupo'), {
93
+ idNome: need(flags.id, 'id'), evidencia: need(flags.evidencia, 'evidencia'),
94
+ }));
95
+ case 'optin revogar': return void out(await nv.optin.revogar(need(flags.grupo, 'grupo'), need(flags.id, 'id')));
96
+ case 'optin status': return void out(await nv.optin.status(need(flags.grupo, 'grupo'), need(flags.id, 'id')));
97
+ case 'prefs profile': return void out(await nv.prefs.profile(need(flags.tenant, 'tenant'), need(flags.id, 'id')));
98
+ case 'qsa pessoa': return void out(await nv.qsa.vinculosDaPessoa(need(flags.id, 'id')));
99
+ case 'qsa empresa': return void out(await nv.qsa.qsaDaEmpresa(need(flags.cnpj, 'cnpj'), !flags.todos));
100
+ default: die(USAGE);
101
+ }
102
+ }
103
+ main().catch((err) => {
104
+ if (err instanceof NeoVaultError)
105
+ die(`${err.message}\n${JSON.stringify(err.body)}`);
106
+ die(String(err?.message ?? err));
107
+ });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * NeoVault SDK — cliente da borda F6-lite (https://api.neovault.com.br).
3
+ *
4
+ * Browser-safe (fetch nativo, zero dependências). O token é um JWT HS256 do
5
+ * realm NeoVault com escopos mínimos — emita com a CLI (`neovault mint`) ou
6
+ * peça ao operador.
7
+ *
8
+ * const nv = new NeoVault({ token });
9
+ * const { id_nome } = await nv.identities.register({ nome, cpf, dataNascimento: '1990-01-01' });
10
+ * await nv.contacts.upsert('minha-loja', id_nome, { celular: '+5585...', canalPref: 'whatsapp', optinMkt: true });
11
+ */
12
+ export interface NeoVaultOptions {
13
+ /** JWT do realm NeoVault (escopos mínimos do seu caso de uso). */
14
+ token: string;
15
+ /** Base da API. Default: produção. */
16
+ baseUrl?: string;
17
+ /** Finalidade LGPD padrão das leituras (audita no cofre). */
18
+ purpose?: string;
19
+ /** Proveniência dos dados gravados (ex.: "declarado", "import:legacy"). */
20
+ source?: string;
21
+ fetch?: typeof fetch;
22
+ }
23
+ export declare class NeoVaultError extends Error {
24
+ status: number;
25
+ body: unknown;
26
+ constructor(status: number, body: unknown, message?: string);
27
+ }
28
+ export interface Identity {
29
+ id_nome: string;
30
+ nome: string;
31
+ cpf: string;
32
+ data_nascimento: string;
33
+ }
34
+ export interface Contact {
35
+ tenant: string;
36
+ id_nome: string;
37
+ celular?: string;
38
+ email?: string;
39
+ canal_pref: string;
40
+ optin_mkt: boolean;
41
+ source: string;
42
+ }
43
+ export interface Aprovacao {
44
+ aprovacao_id: number;
45
+ grupo: string;
46
+ tenants: string[];
47
+ evidencia: string;
48
+ aprovado_at: string;
49
+ vigente: boolean;
50
+ }
51
+ export interface Profile {
52
+ tenant: string;
53
+ faixa_etaria?: string;
54
+ regiao?: string;
55
+ segmento?: string;
56
+ eventos: {
57
+ tipo: string;
58
+ payload: Record<string, unknown>;
59
+ source: string;
60
+ at: string;
61
+ }[];
62
+ }
63
+ export interface Vinculo {
64
+ id_nome: string;
65
+ cnpj: string;
66
+ vinculo: 'socio' | 'administrador';
67
+ qualificacao?: string;
68
+ source: string;
69
+ valido_de: string;
70
+ valido_ate?: string;
71
+ vigente: boolean;
72
+ }
73
+ export declare class NeoVault {
74
+ readonly baseUrl: string;
75
+ private readonly opts;
76
+ private readonly f;
77
+ constructor(opts: NeoVaultOptions);
78
+ private req;
79
+ /** Identidade civil (cofre de PII). Escopos: vault:register|resolve|shred. */
80
+ readonly identities: {
81
+ register: (p: {
82
+ nome: string;
83
+ cpf: string;
84
+ dataNascimento: string;
85
+ source?: string;
86
+ }) => Promise<{
87
+ id_nome: string;
88
+ }>;
89
+ /** Leitura de PII — a finalidade é obrigatória e auditada. */
90
+ resolve: (idNome: string, purpose?: string) => Promise<Identity>;
91
+ /** Eliminação LGPD (crypto-shredding + cascata em todos os módulos). */
92
+ shred: (idNome: string, purpose?: string) => Promise<void>;
93
+ };
94
+ /** Contato por tenant (consentimento por controlador). Escopos: contact:*. */
95
+ readonly contacts: {
96
+ upsert: (tenant: string, idNome: string, p: {
97
+ celular?: string;
98
+ email?: string;
99
+ canalPref: string;
100
+ optinMkt: boolean;
101
+ }) => Promise<Contact>;
102
+ get: (tenant: string, idNome: string) => Promise<Contact>;
103
+ delete: (tenant: string, idNome: string) => Promise<void>;
104
+ };
105
+ /** Opt-in em grupo — aprovação única para uma rede nomeada. Escopos: optin:*. */
106
+ readonly optin: {
107
+ upsertGrupo: (grupo: string, descricao?: string) => Promise<{
108
+ grupo: string;
109
+ }>;
110
+ setMembro: (grupo: string, tenant: string, ativo?: boolean) => Promise<void>;
111
+ /** Aprovação única: fan-out automático de opt-in nos tenants membros. */
112
+ aprovar: (grupo: string, p: {
113
+ idNome: string;
114
+ evidencia: string;
115
+ }) => Promise<{
116
+ aprovacao_id: number;
117
+ aplicados: string[];
118
+ sem_contato: string[] | null;
119
+ }>;
120
+ /** Revogação única: saída automática exatamente do conjunto aprovado. */
121
+ revogar: (grupo: string, idNome: string) => Promise<{
122
+ aprovacao_id: number;
123
+ revogados: string[] | null;
124
+ }>;
125
+ status: (grupo: string, idNome: string) => Promise<Aprovacao>;
126
+ };
127
+ /** Perfil comportamental pseudonimizado do tenant. Escopo: prefs:read. */
128
+ readonly prefs: {
129
+ profile: (tenant: string, idNome: string) => Promise<Profile>;
130
+ };
131
+ /** Vínculos societários (fato global). Escopos: qsa:read|write. */
132
+ readonly qsa: {
133
+ upsertVinculo: (p: {
134
+ idNome: string;
135
+ cnpj: string;
136
+ vinculo: "socio" | "administrador";
137
+ qualificacao?: string;
138
+ validoDe: string;
139
+ validoAte?: string;
140
+ }) => Promise<Vinculo>;
141
+ encerrar: (p: {
142
+ idNome: string;
143
+ cnpj: string;
144
+ vinculo: "socio" | "administrador";
145
+ validoAte?: string;
146
+ }) => Promise<Vinculo>;
147
+ vinculosDaPessoa: (idNome: string) => Promise<Vinculo[]>;
148
+ qsaDaEmpresa: (cnpj: string, vigentes?: boolean) => Promise<Vinculo[]>;
149
+ };
150
+ /** Health da borda (sem auth). */
151
+ health(): Promise<boolean>;
152
+ }
package/dist/client.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * NeoVault SDK — cliente da borda F6-lite (https://api.neovault.com.br).
3
+ *
4
+ * Browser-safe (fetch nativo, zero dependências). O token é um JWT HS256 do
5
+ * realm NeoVault com escopos mínimos — emita com a CLI (`neovault mint`) ou
6
+ * peça ao operador.
7
+ *
8
+ * const nv = new NeoVault({ token });
9
+ * const { id_nome } = await nv.identities.register({ nome, cpf, dataNascimento: '1990-01-01' });
10
+ * await nv.contacts.upsert('minha-loja', id_nome, { celular: '+5585...', canalPref: 'whatsapp', optinMkt: true });
11
+ */
12
+ export class NeoVaultError extends Error {
13
+ status;
14
+ body;
15
+ constructor(status, body, message) {
16
+ super(message ?? `NeoVault API: HTTP ${status}`);
17
+ this.status = status;
18
+ this.body = body;
19
+ this.name = 'NeoVaultError';
20
+ }
21
+ }
22
+ export class NeoVault {
23
+ baseUrl;
24
+ opts;
25
+ f;
26
+ constructor(opts) {
27
+ if (!opts.token)
28
+ throw new Error('NeoVault: token é obrigatório');
29
+ this.opts = opts;
30
+ this.baseUrl = (opts.baseUrl ?? 'https://api.neovault.com.br').replace(/\/$/, '');
31
+ this.f = opts.fetch ?? fetch;
32
+ }
33
+ async req(method, path, body, headers) {
34
+ const h = {
35
+ Authorization: `Bearer ${this.opts.token}`,
36
+ ...headers,
37
+ };
38
+ if (this.opts.purpose && !h['X-LGPD-Purpose'])
39
+ h['X-LGPD-Purpose'] = this.opts.purpose;
40
+ if (this.opts.source && !h['X-Data-Source'])
41
+ h['X-Data-Source'] = this.opts.source;
42
+ if (body !== undefined)
43
+ h['Content-Type'] = 'application/json';
44
+ const res = await this.f(`${this.baseUrl}${path}`, {
45
+ method,
46
+ headers: h,
47
+ body: body === undefined ? undefined : JSON.stringify(body),
48
+ });
49
+ let parsed = null;
50
+ try {
51
+ parsed = await res.json();
52
+ }
53
+ catch {
54
+ /* 204 / sem corpo */
55
+ }
56
+ if (!res.ok) {
57
+ const msg = parsed?.error;
58
+ throw new NeoVaultError(res.status, parsed, msg ? `NeoVault API: ${msg} (HTTP ${res.status})` : undefined);
59
+ }
60
+ return parsed;
61
+ }
62
+ /** Identidade civil (cofre de PII). Escopos: vault:register|resolve|shred. */
63
+ identities = {
64
+ register: (p) => this.req('POST', '/vault/v1/identities', {
65
+ nome: p.nome,
66
+ cpf: p.cpf,
67
+ data_nascimento: p.dataNascimento,
68
+ }, p.source ? { 'X-Data-Source': p.source } : undefined),
69
+ /** Leitura de PII — a finalidade é obrigatória e auditada. */
70
+ resolve: (idNome, purpose) => this.req('GET', `/vault/v1/identities/${idNome}`, undefined, purpose ? { 'X-LGPD-Purpose': purpose } : undefined),
71
+ /** Eliminação LGPD (crypto-shredding + cascata em todos os módulos). */
72
+ shred: (idNome, purpose) => this.req('DELETE', `/vault/v1/identities/${idNome}`, undefined, purpose ? { 'X-LGPD-Purpose': purpose } : undefined),
73
+ };
74
+ /** Contato por tenant (consentimento por controlador). Escopos: contact:*. */
75
+ contacts = {
76
+ upsert: (tenant, idNome, p) => this.req('PUT', `/contact/v1/tenants/${tenant}/contacts/${idNome}`, {
77
+ celular: p.celular ?? '',
78
+ email: p.email ?? '',
79
+ canal_pref: p.canalPref,
80
+ optin_mkt: p.optinMkt,
81
+ }),
82
+ get: (tenant, idNome) => this.req('GET', `/contact/v1/tenants/${tenant}/contacts/${idNome}`),
83
+ delete: (tenant, idNome) => this.req('DELETE', `/contact/v1/tenants/${tenant}/contacts/${idNome}`),
84
+ };
85
+ /** Opt-in em grupo — aprovação única para uma rede nomeada. Escopos: optin:*. */
86
+ optin = {
87
+ upsertGrupo: (grupo, descricao = '') => this.req('PUT', `/contact/v1/grupos/${grupo}`, { descricao }),
88
+ setMembro: (grupo, tenant, ativo = true) => this.req(ativo ? 'PUT' : 'DELETE', `/contact/v1/grupos/${grupo}/membros/${tenant}`, ativo ? {} : undefined),
89
+ /** Aprovação única: fan-out automático de opt-in nos tenants membros. */
90
+ aprovar: (grupo, p) => this.req('POST', `/contact/v1/grupos/${grupo}/aprovacoes`, { id_nome: p.idNome, evidencia: p.evidencia }),
91
+ /** Revogação única: saída automática exatamente do conjunto aprovado. */
92
+ revogar: (grupo, idNome) => this.req('DELETE', `/contact/v1/grupos/${grupo}/aprovacoes/${idNome}`),
93
+ status: (grupo, idNome) => this.req('GET', `/contact/v1/grupos/${grupo}/aprovacoes/${idNome}`),
94
+ };
95
+ /** Perfil comportamental pseudonimizado do tenant. Escopo: prefs:read. */
96
+ prefs = {
97
+ profile: (tenant, idNome) => this.req('GET', `/prefs/v1/tenants/${tenant}/profiles/${idNome}`),
98
+ };
99
+ /** Vínculos societários (fato global). Escopos: qsa:read|write. */
100
+ qsa = {
101
+ upsertVinculo: (p) => this.req('PUT', '/qsa/v1/vinculos', {
102
+ id_nome: p.idNome,
103
+ cnpj: p.cnpj,
104
+ vinculo: p.vinculo,
105
+ qualificacao: p.qualificacao ?? '',
106
+ valido_de: p.validoDe,
107
+ valido_ate: p.validoAte ?? '',
108
+ }),
109
+ encerrar: (p) => this.req('POST', '/qsa/v1/vinculos/encerrar', {
110
+ id_nome: p.idNome,
111
+ cnpj: p.cnpj,
112
+ vinculo: p.vinculo,
113
+ valido_ate: p.validoAte ?? '',
114
+ }),
115
+ vinculosDaPessoa: (idNome) => this.req('GET', `/qsa/v1/pessoas/${idNome}/vinculos`),
116
+ qsaDaEmpresa: (cnpj, vigentes = true) => this.req('GET', `/qsa/v1/empresas/${cnpj}/qsa${vigentes ? '' : '?vigentes=false'}`),
117
+ };
118
+ /** Health da borda (sem auth). */
119
+ async health() {
120
+ const res = await this.f(`${this.baseUrl}/healthz`);
121
+ return res.ok;
122
+ }
123
+ }
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/mcp.js ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Servidor MCP do NeoVault (stdio) — expõe o cofre como ferramentas para
4
+ * agentes (Claude Code, etc.). Env: NEOVAULT_TOKEN (obrigatório), NEOVAULT_API.
5
+ *
6
+ * Config no cliente MCP:
7
+ * { "command": "npx", "args": ["-y", "neovault-mcp"],
8
+ * "env": { "NEOVAULT_TOKEN": "<jwt do realm>" } }
9
+ */
10
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+ import { z } from 'zod';
13
+ import { NeoVault, NeoVaultError } from './client.js';
14
+ const token = process.env.NEOVAULT_TOKEN;
15
+ if (!token) {
16
+ console.error('NEOVAULT_TOKEN não definido');
17
+ process.exit(1);
18
+ }
19
+ const nv = new NeoVault({ token, baseUrl: process.env.NEOVAULT_API, purpose: process.env.NEOVAULT_PURPOSE ?? 'mcp' });
20
+ const server = new McpServer({ name: 'neovault', version: '0.1.0' });
21
+ function wrap(fn) {
22
+ return async (args) => {
23
+ try {
24
+ const data = await fn(args);
25
+ return { content: [{ type: 'text', text: JSON.stringify(data ?? { ok: true }, null, 2) }] };
26
+ }
27
+ catch (err) {
28
+ const msg = err instanceof NeoVaultError ? `${err.message} ${JSON.stringify(err.body)}` : String(err);
29
+ return { content: [{ type: 'text', text: `erro: ${msg}` }], isError: true };
30
+ }
31
+ };
32
+ }
33
+ const idNome = z.string().uuid().describe('id_nome (token pseudônimo do titular)');
34
+ const tenant = z.string().describe('tenant/controlador (slug, ex.: eduue)');
35
+ server.registerTool('vault_register', {
36
+ description: 'Cadastra uma identidade civil no cofre; retorna o id_nome pseudônimo. PII nunca deve ser repetida na conversa.',
37
+ inputSchema: { nome: z.string(), cpf: z.string().describe('CPF, só dígitos'), data_nascimento: z.string().describe('YYYY-MM-DD') },
38
+ }, wrap((a) => nv.identities.register({ nome: a.nome, cpf: a.cpf, dataNascimento: a.data_nascimento })));
39
+ server.registerTool('vault_resolve', {
40
+ description: 'Resolve um id_nome para os dados civis (leitura auditada com finalidade LGPD).',
41
+ inputSchema: { id_nome: idNome, purpose: z.string().describe('finalidade LGPD da leitura') },
42
+ }, wrap((a) => nv.identities.resolve(a.id_nome, a.purpose)));
43
+ server.registerTool('vault_shred', {
44
+ description: 'Elimina a identidade (LGPD art. 18 VI): crypto-shredding + cascata automática em contatos/QSA/comportamento. IRREVERSÍVEL.',
45
+ inputSchema: { id_nome: idNome, purpose: z.string().optional() },
46
+ }, wrap((a) => nv.identities.shred(a.id_nome, a.purpose)));
47
+ server.registerTool('contact_upsert', {
48
+ description: 'Cria/atualiza o contato do titular em um tenant (consentimento por controlador; transição de opt-in é auditada).',
49
+ inputSchema: {
50
+ tenant, id_nome: idNome,
51
+ celular: z.string().optional().describe('E.164, ex.: +5585999998888'),
52
+ email: z.string().optional(),
53
+ canal_pref: z.enum(['whatsapp', 'email', 'sms']),
54
+ optin_mkt: z.boolean(),
55
+ },
56
+ }, wrap((a) => nv.contacts.upsert(a.tenant, a.id_nome, { celular: a.celular, email: a.email, canalPref: a.canal_pref, optinMkt: a.optin_mkt })));
57
+ server.registerTool('contact_get', {
58
+ description: 'Lê o contato do titular em um tenant.',
59
+ inputSchema: { tenant, id_nome: idNome },
60
+ }, wrap((a) => nv.contacts.get(a.tenant, a.id_nome)));
61
+ server.registerTool('optin_aprovar', {
62
+ description: 'Aprovação ÚNICA de opt-in para uma rede nomeada de empresas: entrada automática na lista de envio de todos os tenants membros, com prova auditada.',
63
+ inputSchema: { grupo: z.string().describe('slug da rede'), id_nome: idNome, evidencia: z.string().describe('referência SEM PII da prova (id de termo, hash)') },
64
+ }, wrap((a) => nv.optin.aprovar(a.grupo, { idNome: a.id_nome, evidencia: a.evidencia })));
65
+ server.registerTool('optin_revogar', {
66
+ description: 'Revogação única: saída automática da lista de envio exatamente nos tenants do snapshot aprovado.',
67
+ inputSchema: { grupo: z.string(), id_nome: idNome },
68
+ }, wrap((a) => nv.optin.revogar(a.grupo, a.id_nome)));
69
+ server.registerTool('optin_status', {
70
+ description: 'Consulta a aprovação vigente do titular na rede (snapshot, evidência, vigência).',
71
+ inputSchema: { grupo: z.string(), id_nome: idNome },
72
+ }, wrap((a) => nv.optin.status(a.grupo, a.id_nome)));
73
+ server.registerTool('prefs_profile', {
74
+ description: 'Perfil comportamental PSEUDONIMIZADO do titular no tenant (faixa etária, região, segmento, eventos) — sem PII, para personalização.',
75
+ inputSchema: { tenant, id_nome: idNome },
76
+ }, wrap((a) => nv.prefs.profile(a.tenant, a.id_nome)));
77
+ server.registerTool('qsa_pessoa', {
78
+ description: 'Lista os vínculos societários (empresas) de um titular.',
79
+ inputSchema: { id_nome: idNome },
80
+ }, wrap((a) => nv.qsa.vinculosDaPessoa(a.id_nome)));
81
+ server.registerTool('qsa_empresa', {
82
+ description: 'Lista o QSA (sócios/administradores) de uma empresa pelo CNPJ.',
83
+ inputSchema: { cnpj: z.string(), vigentes: z.boolean().optional().describe('default true') },
84
+ }, wrap((a) => nv.qsa.qsaDaEmpresa(a.cnpj, a.vigentes ?? true)));
85
+ const transport = new StdioServerTransport();
86
+ await server.connect(transport);
package/dist/mint.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ export interface MintClaims {
2
+ /** id do sistema consumidor (ex.: "eduuw"). */
3
+ sub: string;
4
+ /** escopos mínimos (ex.: ["contact:read","prefs:read"]). */
5
+ scopes: string[];
6
+ /** tenants autorizados; ["*"] = todos (só operador). */
7
+ tenants?: string[];
8
+ /** finalidade LGPD padrão das leituras. */
9
+ purpose?: string;
10
+ /** vida útil em segundos (default 3600). */
11
+ ttlSeconds?: number;
12
+ }
13
+ /** Assina um JWT HS256 aceito pela borda F6-lite. */
14
+ export declare function mint(secret: string, c: MintClaims): string;
15
+ /** Decodifica o payload de um token (sem verificar assinatura — uso informativo). */
16
+ export declare function decode(token: string): {
17
+ sub: string;
18
+ scopes: string[];
19
+ tenants?: string[];
20
+ exp: number;
21
+ } | null;
package/dist/mint.js ADDED
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Emissão de tokens de sistema do realm NeoVault (HS256) — NODE-ONLY.
3
+ * O segredo NUNCA deve ir para código ou flags: leia de env/secret manager.
4
+ * Produção: a emissão pertence ao operador do realm.
5
+ */
6
+ import { createHmac } from 'node:crypto';
7
+ const b64url = (buf) => buf.toString('base64url');
8
+ /** Assina um JWT HS256 aceito pela borda F6-lite. */
9
+ export function mint(secret, c) {
10
+ if (!secret)
11
+ throw new Error('mint: secret é obrigatório (use env EXT_JWT_SECRET)');
12
+ if (!c.sub || !c.scopes?.length)
13
+ throw new Error('mint: sub e scopes são obrigatórios');
14
+ const now = Math.floor(Date.now() / 1000);
15
+ const head = b64url(Buffer.from(JSON.stringify({ alg: 'HS256', typ: 'JWT' })));
16
+ const payload = b64url(Buffer.from(JSON.stringify({
17
+ sub: c.sub,
18
+ scopes: c.scopes,
19
+ ...(c.tenants?.length ? { tenants: c.tenants } : {}),
20
+ ...(c.purpose ? { purpose: c.purpose } : {}),
21
+ iat: now,
22
+ exp: now + (c.ttlSeconds ?? 3600),
23
+ })));
24
+ const signed = `${head}.${payload}`;
25
+ const sig = b64url(createHmac('sha256', secret).update(signed).digest());
26
+ return `${signed}.${sig}`;
27
+ }
28
+ /** Decodifica o payload de um token (sem verificar assinatura — uso informativo). */
29
+ export function decode(token) {
30
+ const parts = token.trim().split('.');
31
+ if (parts.length !== 3)
32
+ return null;
33
+ try {
34
+ return JSON.parse(Buffer.from(parts[1], 'base64url').toString());
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "neovault",
3
+ "version": "0.1.0",
4
+ "description": "SDK, CLI e servidor MCP oficiais do NeoVault — cofre de identidade, consentimento e comportamento pseudonimizado (LGPD by design).",
5
+ "keywords": ["neovault", "lgpd", "identidade", "consentimento", "pii", "vault", "mcp"],
6
+ "homepage": "https://neovault.com.br",
7
+ "repository": { "type": "git", "url": "git+https://github.com/vertikon/neovault.com.br.git", "directory": "sdk/js" },
8
+ "license": "MIT",
9
+ "author": "Vertikon",
10
+ "type": "module",
11
+ "exports": {
12
+ ".": { "types": "./dist/client.d.ts", "default": "./dist/client.js" },
13
+ "./mint": { "types": "./dist/mint.d.ts", "default": "./dist/mint.js" }
14
+ },
15
+ "bin": { "neovault": "dist/cli.js", "neovault-mcp": "dist/mcp.js" },
16
+ "files": ["dist", "README.md"],
17
+ "engines": { "node": ">=18" },
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "test": "npm run build && node --test test/client.test.mjs"
21
+ },
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^1.12.0",
24
+ "zod": "^3.24.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^22",
28
+ "typescript": "^5"
29
+ }
30
+ }