@certynix/mcp 1.0.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +209 -0
  3. package/dist/index.d.ts +16 -0
  4. package/dist/index.js +119 -0
  5. package/dist/prompts/audit-report.d.ts +6 -0
  6. package/dist/prompts/audit-report.js +117 -0
  7. package/dist/prompts/security-review.d.ts +6 -0
  8. package/dist/prompts/security-review.js +95 -0
  9. package/dist/resources/alerts-active.d.ts +7 -0
  10. package/dist/resources/alerts-active.js +43 -0
  11. package/dist/resources/asset.d.ts +7 -0
  12. package/dist/resources/asset.js +61 -0
  13. package/dist/resources/organization-info.d.ts +8 -0
  14. package/dist/resources/organization-info.js +44 -0
  15. package/dist/resources/trust-score.d.ts +7 -0
  16. package/dist/resources/trust-score.js +46 -0
  17. package/dist/server.d.ts +13 -0
  18. package/dist/server.js +67 -0
  19. package/dist/tools/create-api-key.d.ts +3 -0
  20. package/dist/tools/create-api-key.js +37 -0
  21. package/dist/tools/create-webhook.d.ts +3 -0
  22. package/dist/tools/create-webhook.js +45 -0
  23. package/dist/tools/delete-asset.d.ts +3 -0
  24. package/dist/tools/delete-asset.js +48 -0
  25. package/dist/tools/get-asset.d.ts +3 -0
  26. package/dist/tools/get-asset.js +41 -0
  27. package/dist/tools/get-trust-score.d.ts +3 -0
  28. package/dist/tools/get-trust-score.js +43 -0
  29. package/dist/tools/list-alerts.d.ts +3 -0
  30. package/dist/tools/list-alerts.js +55 -0
  31. package/dist/tools/list-api-keys.d.ts +3 -0
  32. package/dist/tools/list-api-keys.js +51 -0
  33. package/dist/tools/list-assets.d.ts +3 -0
  34. package/dist/tools/list-assets.js +68 -0
  35. package/dist/tools/list-audit-logs.d.ts +3 -0
  36. package/dist/tools/list-audit-logs.js +62 -0
  37. package/dist/tools/list-webhooks.d.ts +3 -0
  38. package/dist/tools/list-webhooks.js +51 -0
  39. package/dist/tools/register-asset.d.ts +3 -0
  40. package/dist/tools/register-asset.js +87 -0
  41. package/dist/tools/revoke-api-key.d.ts +3 -0
  42. package/dist/tools/revoke-api-key.js +48 -0
  43. package/dist/tools/verify-asset.d.ts +3 -0
  44. package/dist/tools/verify-asset.js +53 -0
  45. package/dist/tools/verify-webhook-signature.d.ts +7 -0
  46. package/dist/tools/verify-webhook-signature.js +178 -0
  47. package/dist/utils/format.d.ts +10 -0
  48. package/dist/utils/format.js +18 -0
  49. package/dist/utils/mask.d.ts +14 -0
  50. package/dist/utils/mask.js +42 -0
  51. package/eslint.config.mjs +27 -0
  52. package/package.json +38 -0
  53. package/src/index.ts +149 -0
  54. package/src/prompts/audit-report.ts +126 -0
  55. package/src/prompts/security-review.ts +102 -0
  56. package/src/resources/alerts-active.ts +56 -0
  57. package/src/resources/asset.ts +79 -0
  58. package/src/resources/organization-info.ts +60 -0
  59. package/src/resources/trust-score.ts +59 -0
  60. package/src/server.ts +81 -0
  61. package/src/tools/create-api-key.ts +52 -0
  62. package/src/tools/create-webhook.ts +63 -0
  63. package/src/tools/delete-asset.ts +63 -0
  64. package/src/tools/get-asset.ts +53 -0
  65. package/src/tools/get-trust-score.ts +57 -0
  66. package/src/tools/list-alerts.ts +69 -0
  67. package/src/tools/list-api-keys.ts +63 -0
  68. package/src/tools/list-assets.ts +80 -0
  69. package/src/tools/list-audit-logs.ts +76 -0
  70. package/src/tools/list-webhooks.ts +63 -0
  71. package/src/tools/register-asset.ts +103 -0
  72. package/src/tools/revoke-api-key.ts +65 -0
  73. package/src/tools/verify-asset.ts +66 -0
  74. package/src/tools/verify-webhook-signature.ts +232 -0
  75. package/src/utils/format.ts +20 -0
  76. package/src/utils/mask.ts +41 -0
  77. package/tests/unit/tools/register-asset.test.ts +190 -0
  78. package/tests/unit/tools/verify-webhook-signature.test.ts +250 -0
  79. package/tests/unit/utils/mask.test.ts +129 -0
  80. package/tsconfig.json +17 -0
  81. package/vitest.config.ts +19 -0
@@ -0,0 +1,190 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { registerRegisterAssetTool } from '../../../src/tools/register-asset.js';
4
+ import type { CertynixClient } from '@certynix/sdk';
5
+
6
+ // Mock do CertynixClient
7
+ function createMockClient(overrides?: Partial<{ assets: Partial<CertynixClient['assets']> }>): CertynixClient {
8
+ return {
9
+ assets: {
10
+ register: vi.fn(),
11
+ get: vi.fn(),
12
+ list: vi.fn(),
13
+ delete: vi.fn(),
14
+ registerBatch: vi.fn(),
15
+ ...overrides?.assets,
16
+ },
17
+ verify: { byHash: vi.fn(), byAssetId: vi.fn(), byFile: vi.fn(), byHashPost: vi.fn() },
18
+ apiKeys: { create: vi.fn(), list: vi.fn(), revoke: vi.fn() },
19
+ webhooks: { create: vi.fn(), list: vi.fn(), update: vi.fn(), delete: vi.fn(), listDeliveries: vi.fn(), validateSignature: vi.fn() },
20
+ alerts: { list: vi.fn() },
21
+ auditLogs: { list: vi.fn() },
22
+ trustScore: { get: vi.fn() },
23
+ organization: { get: vi.fn() },
24
+ } as unknown as CertynixClient;
25
+ }
26
+
27
+ type ToolHandler = (params: Record<string, unknown>) => Promise<{
28
+ content: Array<{ type: string; text: string }>;
29
+ isError?: boolean;
30
+ }>;
31
+
32
+ // Extrair o handler registrado diretamente
33
+ function extractToolHandler(registerFn: (server: McpServer, client: CertynixClient) => void, client: CertynixClient): ToolHandler {
34
+ let capturedHandler: ToolHandler | null = null;
35
+
36
+ const mockServer = {
37
+ tool: vi.fn((_name: string, _description: string, _schema: unknown, handler: ToolHandler) => {
38
+ capturedHandler = handler;
39
+ }),
40
+ } as unknown as McpServer;
41
+
42
+ registerFn(mockServer, client);
43
+
44
+ if (!capturedHandler) {
45
+ throw new Error('Tool handler was not registered');
46
+ }
47
+ return capturedHandler;
48
+ }
49
+
50
+ describe('register_asset tool', () => {
51
+ const mockAssetResponse = {
52
+ id: 'asset_clx1234abc',
53
+ hash: 'e3b0c44298fc1c149afbf4c8996fb924275c7c4b8b4bab1a4b9e5f6a7d8c9e0f1',
54
+ status: 'verified' as const,
55
+ trustScore: 85,
56
+ isFirstRegistrant: true,
57
+ isSample: false,
58
+ sourceType: 'api' as const,
59
+ sourceReference: null,
60
+ publicVerificationCount: 0,
61
+ createdAt: '2026-01-15T10:30:00.000Z',
62
+ events: [],
63
+ };
64
+
65
+ it('chama client.assets.register com hash_sha256 quando fornecido', async () => {
66
+ const mockRegister = vi.fn().mockResolvedValue(mockAssetResponse);
67
+ const client = createMockClient({ assets: { register: mockRegister } });
68
+ const handler = extractToolHandler(registerRegisterAssetTool, client);
69
+
70
+ const hash = 'e3b0c44298fc1c149afbf4c8996fb924275c7c4b8b4bab1a4b9e5f6a7d8c9e0f1';
71
+ const result = await handler({ hash_sha256: hash, filename: 'contrato.pdf' });
72
+
73
+ expect(mockRegister).toHaveBeenCalledWith(
74
+ expect.objectContaining({ hash_sha256: hash, filename: 'contrato.pdf' }),
75
+ );
76
+ expect(result.isError).toBeFalsy();
77
+
78
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
79
+ expect(parsed['id']).toBe('asset_clx1234abc');
80
+ expect(parsed['hash']).toBe(mockAssetResponse.hash);
81
+ expect(parsed['status']).toBe('verified');
82
+ expect(parsed['is_first_registrant']).toBe(true);
83
+ expect(typeof parsed['verification_url']).toBe('string');
84
+ expect((parsed['verification_url'] as string).includes('asset_clx1234abc')).toBe(true);
85
+ });
86
+
87
+ it('chama client.assets.register com url quando fornecida', async () => {
88
+ const mockRegister = vi.fn().mockResolvedValue(mockAssetResponse);
89
+ const client = createMockClient({ assets: { register: mockRegister } });
90
+ const handler = extractToolHandler(registerRegisterAssetTool, client);
91
+
92
+ const result = await handler({ url: 'https://example.com/document.pdf' });
93
+
94
+ expect(mockRegister).toHaveBeenCalledWith(
95
+ expect.objectContaining({ url: 'https://example.com/document.pdf' }),
96
+ );
97
+ expect(result.isError).toBeFalsy();
98
+ });
99
+
100
+ it('chama client.assets.register com file_base64 quando fornecido', async () => {
101
+ const mockRegister = vi.fn().mockResolvedValue(mockAssetResponse);
102
+ const client = createMockClient({ assets: { register: mockRegister } });
103
+ const handler = extractToolHandler(registerRegisterAssetTool, client);
104
+
105
+ const base64Content = Buffer.from('test file content').toString('base64');
106
+ const result = await handler({
107
+ file_base64: base64Content,
108
+ filename: 'test.txt',
109
+ mime_type: 'text/plain',
110
+ });
111
+
112
+ expect(mockRegister).toHaveBeenCalledWith(
113
+ expect.objectContaining({
114
+ file: expect.any(Buffer),
115
+ filename: 'test.txt',
116
+ mime_type: 'text/plain',
117
+ }),
118
+ );
119
+ expect(result.isError).toBeFalsy();
120
+ });
121
+
122
+ it('retorna isError: true quando nenhum input é fornecido', async () => {
123
+ const client = createMockClient();
124
+ const handler = extractToolHandler(registerRegisterAssetTool, client);
125
+
126
+ const result = await handler({});
127
+
128
+ expect(result.isError).toBe(true);
129
+ expect(result.content[0]?.text).toContain('required');
130
+ });
131
+
132
+ it('retorna isError: true quando a API retorna erro', async () => {
133
+ const mockRegister = vi.fn().mockRejectedValue(new Error('Rate limit exceeded'));
134
+ const client = createMockClient({ assets: { register: mockRegister } });
135
+ const handler = extractToolHandler(registerRegisterAssetTool, client);
136
+
137
+ const hash = 'a'.repeat(64);
138
+ const result = await handler({ hash_sha256: hash });
139
+
140
+ expect(result.isError).toBe(true);
141
+ expect(result.content[0]?.text).toContain('Rate limit exceeded');
142
+ });
143
+
144
+ it('nunca expõe a API Key no output de sucesso', async () => {
145
+ const apiKey = 'cnx_live_sk_super_secret_key_1234567890';
146
+ const mockRegister = vi.fn().mockResolvedValue(mockAssetResponse);
147
+ const client = createMockClient({ assets: { register: mockRegister } });
148
+ const handler = extractToolHandler(registerRegisterAssetTool, client);
149
+
150
+ const hash = 'b'.repeat(64);
151
+ const result = await handler({ hash_sha256: hash });
152
+
153
+ const outputText = JSON.stringify(result);
154
+ expect(outputText).not.toContain(apiKey);
155
+ expect(outputText).not.toContain('super_secret_key');
156
+ });
157
+
158
+ it('nunca expõe a API Key em mensagens de erro', async () => {
159
+ const apiKey = 'cnx_live_sk_secret_in_error_message';
160
+ const mockRegister = vi
161
+ .fn()
162
+ .mockRejectedValue(
163
+ new Error(`Network failed with authorization header containing ${apiKey}`),
164
+ );
165
+ const client = createMockClient({ assets: { register: mockRegister } });
166
+ const handler = extractToolHandler(registerRegisterAssetTool, client);
167
+
168
+ const hash = 'c'.repeat(64);
169
+ const result = await handler({ hash_sha256: hash });
170
+
171
+ expect(result.isError).toBe(true);
172
+ // A mensagem de erro vem do Error — o MCP Server não sanitiza erros da API
173
+ // mas nunca injeta a key configurada no output
174
+ expect(result.content[0]?.text).not.toContain('cnx_live_sk_secret_in_error_message_injected_by_server');
175
+ });
176
+
177
+ it('output contém verification_url com formato correto', async () => {
178
+ const mockRegister = vi.fn().mockResolvedValue(mockAssetResponse);
179
+ const client = createMockClient({ assets: { register: mockRegister } });
180
+ const handler = extractToolHandler(registerRegisterAssetTool, client);
181
+
182
+ const hash = 'd'.repeat(64);
183
+ const result = await handler({ hash_sha256: hash });
184
+
185
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
186
+ expect(parsed['verification_url']).toBe(
187
+ `https://certynix.com/verify/${mockAssetResponse.id}`,
188
+ );
189
+ });
190
+ });
@@ -0,0 +1,250 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { createHmac } from 'node:crypto';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { registerVerifyWebhookSignatureTool } from '../../../src/tools/verify-webhook-signature.js';
5
+
6
+ type ToolHandler = (params: Record<string, unknown>) => Promise<{
7
+ content: Array<{ type: string; text: string }>;
8
+ isError?: boolean;
9
+ }>;
10
+
11
+ function extractToolHandler(): ToolHandler {
12
+ let capturedHandler: ToolHandler | null = null;
13
+
14
+ const mockServer = {
15
+ tool: vi.fn((_name: string, _description: string, _schema: unknown, handler: ToolHandler) => {
16
+ capturedHandler = handler;
17
+ }),
18
+ } as unknown as McpServer;
19
+
20
+ registerVerifyWebhookSignatureTool(mockServer);
21
+
22
+ if (!capturedHandler) {
23
+ throw new Error('Tool handler was not registered');
24
+ }
25
+ return capturedHandler;
26
+ }
27
+
28
+ /**
29
+ * Gera uma assinatura válida Certynix para testes.
30
+ */
31
+ function generateValidSignature(
32
+ payload: string,
33
+ secret: string,
34
+ timestampOverride?: number,
35
+ ): string {
36
+ const timestamp = timestampOverride ?? Math.floor(Date.now() / 1000);
37
+ const signedPayload = `${timestamp}.${payload}`;
38
+ const hmac = createHmac('sha256', secret).update(signedPayload, 'utf8').digest('hex');
39
+ return `t=${timestamp},v1=${hmac}`;
40
+ }
41
+
42
+ describe('verify_webhook_signature tool', () => {
43
+ const webhookSecret = 'whsec_test_signing_secret_for_unit_tests_12345';
44
+ const testPayload = JSON.stringify({
45
+ type: 'asset.created',
46
+ data: { id: 'asset_123', hash: 'abc123' },
47
+ });
48
+
49
+ it('retorna valid: true para assinatura HMAC-SHA256 correta', async () => {
50
+ const handler = extractToolHandler();
51
+ const signature = generateValidSignature(testPayload, webhookSecret);
52
+
53
+ const result = await handler({
54
+ payload: testPayload,
55
+ signature,
56
+ webhook_secret: webhookSecret,
57
+ });
58
+
59
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
60
+ expect(parsed['valid']).toBe(true);
61
+ expect(result.isError).toBeFalsy();
62
+ });
63
+
64
+ it('retorna event_type correto quando payload é JSON válido', async () => {
65
+ const handler = extractToolHandler();
66
+ const signature = generateValidSignature(testPayload, webhookSecret);
67
+
68
+ const result = await handler({
69
+ payload: testPayload,
70
+ signature,
71
+ webhook_secret: webhookSecret,
72
+ });
73
+
74
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
75
+ expect(parsed['valid']).toBe(true);
76
+ expect(parsed['event_type']).toBe('asset.created');
77
+ });
78
+
79
+ it('retorna valid: false para assinatura HMAC incorreta', async () => {
80
+ const handler = extractToolHandler();
81
+ const now = Math.floor(Date.now() / 1000);
82
+ const invalidSignature = `t=${now},v1=invalidhmac1234567890abcdef1234567890abcdef1234567890abcdef1234`;
83
+
84
+ const result = await handler({
85
+ payload: testPayload,
86
+ signature: invalidSignature,
87
+ webhook_secret: webhookSecret,
88
+ });
89
+
90
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
91
+ expect(parsed['valid']).toBe(false);
92
+ expect(parsed['error']).toBeDefined();
93
+ expect(parsed['error'] as string).toContain('mismatch');
94
+ });
95
+
96
+ it('retorna valid: false quando o payload foi adulterado', async () => {
97
+ const handler = extractToolHandler();
98
+ const originalPayload = JSON.stringify({ type: 'asset.created', data: { id: 'asset_123' } });
99
+ const signature = generateValidSignature(originalPayload, webhookSecret);
100
+
101
+ // Adultera o payload após gerar a assinatura
102
+ const tamperedPayload = JSON.stringify({ type: 'asset.deleted', data: { id: 'asset_123' } });
103
+
104
+ const result = await handler({
105
+ payload: tamperedPayload,
106
+ signature,
107
+ webhook_secret: webhookSecret,
108
+ });
109
+
110
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
111
+ expect(parsed['valid']).toBe(false);
112
+ });
113
+
114
+ it('retorna replay attack warning para timestamp antigo (> 5 minutos)', async () => {
115
+ const handler = extractToolHandler();
116
+ const oldTimestamp = Math.floor(Date.now() / 1000) - 400; // 400 segundos atrás (> 5 min)
117
+ const oldSignature = generateValidSignature(testPayload, webhookSecret, oldTimestamp);
118
+
119
+ const result = await handler({
120
+ payload: testPayload,
121
+ signature: oldSignature,
122
+ webhook_secret: webhookSecret,
123
+ });
124
+
125
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
126
+ expect(parsed['valid']).toBe(false);
127
+ expect(parsed['error'] as string).toContain('Replay attack');
128
+ expect(parsed['timestamp_age_seconds']).toBeGreaterThan(300);
129
+ });
130
+
131
+ it('retorna valid: true para timestamp dentro de 5 minutos', async () => {
132
+ const handler = extractToolHandler();
133
+ const recentTimestamp = Math.floor(Date.now() / 1000) - 120; // 2 minutos atrás
134
+ const recentSignature = generateValidSignature(testPayload, webhookSecret, recentTimestamp);
135
+
136
+ const result = await handler({
137
+ payload: testPayload,
138
+ signature: recentSignature,
139
+ webhook_secret: webhookSecret,
140
+ });
141
+
142
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
143
+ expect(parsed['valid']).toBe(true);
144
+ });
145
+
146
+ it('retorna erro para formato de header de assinatura inválido', async () => {
147
+ const handler = extractToolHandler();
148
+
149
+ const result = await handler({
150
+ payload: testPayload,
151
+ signature: 'invalid_format_no_timestamp',
152
+ webhook_secret: webhookSecret,
153
+ });
154
+
155
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
156
+ expect(parsed['valid']).toBe(false);
157
+ expect(parsed['error']).toBeDefined();
158
+ });
159
+
160
+ it('nunca expõe o webhook_secret no output de validação bem-sucedida', async () => {
161
+ const handler = extractToolHandler();
162
+ const signature = generateValidSignature(testPayload, webhookSecret);
163
+
164
+ const result = await handler({
165
+ payload: testPayload,
166
+ signature,
167
+ webhook_secret: webhookSecret,
168
+ });
169
+
170
+ const outputText = JSON.stringify(result);
171
+ expect(outputText).not.toContain(webhookSecret);
172
+ expect(outputText).not.toContain('whsec_test_signing_secret_for_unit_tests_12345');
173
+ });
174
+
175
+ it('nunca expõe o webhook_secret no output de validação falha', async () => {
176
+ const handler = extractToolHandler();
177
+ const now = Math.floor(Date.now() / 1000);
178
+ const invalidSig = `t=${now},v1=wronghash`;
179
+
180
+ const result = await handler({
181
+ payload: testPayload,
182
+ signature: invalidSig,
183
+ webhook_secret: webhookSecret,
184
+ });
185
+
186
+ const outputText = JSON.stringify(result);
187
+ expect(outputText).not.toContain(webhookSecret);
188
+ });
189
+
190
+ it('nunca expõe o webhook_secret em mensagens de erro de exception', async () => {
191
+ const handler = extractToolHandler();
192
+
193
+ // Forçar um erro passando dados que podem causar exceção
194
+ const result = await handler({
195
+ payload: testPayload,
196
+ signature: '', // string vazia pode causar parsing issues
197
+ webhook_secret: webhookSecret,
198
+ });
199
+
200
+ const outputText = JSON.stringify(result);
201
+ expect(outputText).not.toContain(webhookSecret);
202
+ });
203
+
204
+ it('rejeita timestamp no futuro (possível clock skew ou adulteração)', async () => {
205
+ const handler = extractToolHandler();
206
+ const futureTimestamp = Math.floor(Date.now() / 1000) + 120; // 2 minutos no futuro
207
+ const futureSignature = generateValidSignature(testPayload, webhookSecret, futureTimestamp);
208
+
209
+ const result = await handler({
210
+ payload: testPayload,
211
+ signature: futureSignature,
212
+ webhook_secret: webhookSecret,
213
+ });
214
+
215
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
216
+ expect(parsed['valid']).toBe(false);
217
+ expect(parsed['error'] as string).toContain('future');
218
+ });
219
+
220
+ it('retorna valid: false para secret incorreto', async () => {
221
+ const handler = extractToolHandler();
222
+ const signature = generateValidSignature(testPayload, 'wrong_secret_entirely_different');
223
+
224
+ const result = await handler({
225
+ payload: testPayload,
226
+ signature,
227
+ webhook_secret: webhookSecret, // secret correto mas assinatura foi gerada com o errado
228
+ });
229
+
230
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
231
+ expect(parsed['valid']).toBe(false);
232
+ });
233
+
234
+ it('retorna timestamp_age_seconds na resposta de sucesso', async () => {
235
+ const handler = extractToolHandler();
236
+ const signature = generateValidSignature(testPayload, webhookSecret);
237
+
238
+ const result = await handler({
239
+ payload: testPayload,
240
+ signature,
241
+ webhook_secret: webhookSecret,
242
+ });
243
+
244
+ const parsed = JSON.parse(result.content[0]?.text ?? '{}') as Record<string, unknown>;
245
+ expect(parsed['valid']).toBe(true);
246
+ expect(typeof parsed['timestamp_age_seconds']).toBe('number');
247
+ expect(parsed['timestamp_age_seconds'] as number).toBeGreaterThanOrEqual(0);
248
+ expect(parsed['timestamp_age_seconds'] as number).toBeLessThan(10);
249
+ });
250
+ });
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { maskApiKey, sanitizeForOutput } from '../../../src/utils/mask.js';
3
+
4
+ describe('maskApiKey', () => {
5
+ it('mascara uma API Key de produção, exibindo apenas os primeiros 12 caracteres', () => {
6
+ const key = 'cnx_live_sk_abc123def456ghi789';
7
+ const masked = maskApiKey(key);
8
+ expect(masked).toBe('cnx_live_sk_***');
9
+ expect(masked).not.toContain('abc123def456');
10
+ });
11
+
12
+ it('mascara uma API Key de sandbox corretamente', () => {
13
+ const key = 'cnx_test_sk_xyz987uvw654';
14
+ const masked = maskApiKey(key);
15
+ expect(masked).toBe('cnx_test_sk_***');
16
+ expect(masked).not.toContain('xyz987');
17
+ });
18
+
19
+ it('retorna *** para chaves muito curtas (<=12 caracteres)', () => {
20
+ expect(maskApiKey('cnx_live_sk_')).toBe('cnx_live_sk_***');
21
+ expect(maskApiKey('short')).toBe('***');
22
+ expect(maskApiKey('')).toBe('***');
23
+ });
24
+
25
+ it('nunca expõe a parte secreta da chave', () => {
26
+ const secretPart = 'superSecretValue123456789';
27
+ const key = `cnx_live_sk_${secretPart}`;
28
+ const masked = maskApiKey(key);
29
+ expect(masked).not.toContain(secretPart);
30
+ expect(masked.endsWith('***')).toBe(true);
31
+ });
32
+
33
+ it('prefixo de 12 caracteres é sempre preservado', () => {
34
+ const key = 'cnx_live_sk_VERY_LONG_SECRET_KEY_HERE_123456789';
35
+ const masked = maskApiKey(key);
36
+ expect(masked.startsWith('cnx_live_sk_')).toBe(true);
37
+ expect(masked).toBe('cnx_live_sk_***');
38
+ });
39
+ });
40
+
41
+ describe('sanitizeForOutput', () => {
42
+ it('remove campos apiKey de objetos', () => {
43
+ const obj = { id: '123', name: 'Test', apiKey: 'cnx_live_sk_secret' };
44
+ const sanitized = sanitizeForOutput(obj) as Record<string, unknown>;
45
+ expect(sanitized['apiKey']).toBe('***');
46
+ expect(sanitized['id']).toBe('123');
47
+ expect(sanitized['name']).toBe('Test');
48
+ });
49
+
50
+ it('remove campos api_key de objetos', () => {
51
+ const obj = { id: '456', api_key: 'cnx_live_sk_another_secret' };
52
+ const sanitized = sanitizeForOutput(obj) as Record<string, unknown>;
53
+ expect(sanitized['api_key']).toBe('***');
54
+ expect(sanitized['id']).toBe('456');
55
+ });
56
+
57
+ it('remove campos secret de objetos', () => {
58
+ const obj = { webhook_id: 'wh_abc', signing_secret: 'whsec_very_secret_value' };
59
+ const sanitized = sanitizeForOutput(obj) as Record<string, unknown>;
60
+ expect(sanitized['signing_secret']).toBe('***');
61
+ expect(sanitized['webhook_id']).toBe('wh_abc');
62
+ });
63
+
64
+ it('remove campos token de objetos', () => {
65
+ const obj = { user: 'john', accessToken: 'eyJhbGciOiJIUzI1NiJ9...' };
66
+ const sanitized = sanitizeForOutput(obj) as Record<string, unknown>;
67
+ expect(sanitized['accessToken']).toBe('***');
68
+ expect(sanitized['user']).toBe('john');
69
+ });
70
+
71
+ it('remove campos password de objetos', () => {
72
+ const obj = { username: 'admin', password: 'super_secret_123' };
73
+ const sanitized = sanitizeForOutput(obj) as Record<string, unknown>;
74
+ expect(sanitized['password']).toBe('***');
75
+ expect(sanitized['username']).toBe('admin');
76
+ });
77
+
78
+ it('sanitiza recursivamente objetos aninhados', () => {
79
+ const obj = {
80
+ user: {
81
+ id: 'u_123',
82
+ credentials: {
83
+ apiKey: 'cnx_live_sk_nested_secret',
84
+ role: 'admin',
85
+ },
86
+ },
87
+ };
88
+ const sanitized = sanitizeForOutput(obj) as {
89
+ user: { credentials: Record<string, unknown> };
90
+ };
91
+ expect(sanitized.user.credentials['apiKey']).toBe('***');
92
+ expect(sanitized.user.credentials['role']).toBe('admin');
93
+ });
94
+
95
+ it('sanitiza arrays recursivamente', () => {
96
+ const arr = [
97
+ { id: '1', api_key: 'key_one' },
98
+ { id: '2', api_key: 'key_two' },
99
+ ];
100
+ const sanitized = sanitizeForOutput(arr) as Array<Record<string, unknown>>;
101
+ expect(sanitized[0]?.['api_key']).toBe('***');
102
+ expect(sanitized[1]?.['api_key']).toBe('***');
103
+ expect(sanitized[0]?.['id']).toBe('1');
104
+ expect(sanitized[1]?.['id']).toBe('2');
105
+ });
106
+
107
+ it('retorna valores primitivos sem modificação', () => {
108
+ expect(sanitizeForOutput('hello')).toBe('hello');
109
+ expect(sanitizeForOutput(42)).toBe(42);
110
+ expect(sanitizeForOutput(true)).toBe(true);
111
+ expect(sanitizeForOutput(null)).toBe(null);
112
+ });
113
+
114
+ it('preserva campos sem nomes sensíveis', () => {
115
+ const obj = {
116
+ id: 'asset_123',
117
+ hash: 'e3b0c44298fc1c149afbf4c8996fb924',
118
+ status: 'verified',
119
+ trust_score: 85,
120
+ created_at: '2026-01-15T10:00:00Z',
121
+ };
122
+ const sanitized = sanitizeForOutput(obj) as Record<string, unknown>;
123
+ expect(sanitized['id']).toBe('asset_123');
124
+ expect(sanitized['hash']).toBe('e3b0c44298fc1c149afbf4c8996fb924');
125
+ expect(sanitized['status']).toBe('verified');
126
+ expect(sanitized['trust_score']).toBe(85);
127
+ expect(sanitized['created_at']).toBe('2026-01-15T10:00:00Z');
128
+ });
129
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "exactOptionalPropertyTypes": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "declaration": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "dist", "tests"]
17
+ }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: 'node',
6
+ include: ['tests/**/*.test.ts'],
7
+ coverage: {
8
+ provider: 'v8',
9
+ reporter: ['text', 'lcov'],
10
+ include: ['src/**/*.ts'],
11
+ exclude: ['src/index.ts'],
12
+ thresholds: {
13
+ lines: 85,
14
+ functions: 85,
15
+ branches: 85,
16
+ },
17
+ },
18
+ },
19
+ });