@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,76 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { CertynixClient } from '@certynix/sdk';
4
+ import { formatError } from '../utils/format.js';
5
+
6
+ export function registerListAuditLogsTool(server: McpServer, client: CertynixClient): void {
7
+ server.tool(
8
+ 'list_audit_logs',
9
+ 'Lista o histórico completo de ações auditadas na organização: registros de assets, criação de API Keys, mudanças de plano, alertas, e eventos de segurança.',
10
+ {
11
+ limit: z
12
+ .number()
13
+ .int()
14
+ .min(1)
15
+ .max(100)
16
+ .optional()
17
+ .describe('Número de registros por página (máximo 100, padrão 20).'),
18
+ cursor: z
19
+ .string()
20
+ .optional()
21
+ .describe('Cursor de paginação retornado pela chamada anterior.'),
22
+ action: z
23
+ .string()
24
+ .optional()
25
+ .describe(
26
+ 'Filtrar por tipo de ação (ex: asset.created, api_key.created, asset.deleted).',
27
+ ),
28
+ created_after: z
29
+ .string()
30
+ .optional()
31
+ .describe('Retornar apenas logs criados após esta data (ISO 8601).'),
32
+ created_before: z
33
+ .string()
34
+ .optional()
35
+ .describe('Retornar apenas logs criados antes desta data (ISO 8601).'),
36
+ },
37
+ async (params) => {
38
+ try {
39
+ const page = await client.auditLogs.listPage({
40
+ ...(params.limit !== undefined ? { limit: params.limit } : {}),
41
+ ...(params.cursor !== undefined ? { cursor: params.cursor } : {}),
42
+ ...(params.action !== undefined ? { action: params.action } : {}),
43
+ ...(params.created_after !== undefined ? { createdAfter: params.created_after } : {}),
44
+ ...(params.created_before !== undefined
45
+ ? { createdBefore: params.created_before }
46
+ : {}),
47
+ });
48
+
49
+ return {
50
+ content: [
51
+ {
52
+ type: 'text' as const,
53
+ text: JSON.stringify(
54
+ {
55
+ data: page.data,
56
+ pagination: {
57
+ has_more: page.pagination.has_more,
58
+ next_cursor: page.pagination.next_cursor,
59
+ },
60
+ total_returned: page.data.length,
61
+ },
62
+ null,
63
+ 2,
64
+ ),
65
+ },
66
+ ],
67
+ };
68
+ } catch (err) {
69
+ return {
70
+ content: [{ type: 'text' as const, text: formatError(err) }],
71
+ isError: true,
72
+ };
73
+ }
74
+ },
75
+ );
76
+ }
@@ -0,0 +1,63 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { CertynixClient } from '@certynix/sdk';
4
+ import { formatError } from '../utils/format.js';
5
+
6
+ export function registerListWebhooksTool(server: McpServer, client: CertynixClient): void {
7
+ server.tool(
8
+ 'list_webhooks',
9
+ 'Lista os endpoints de webhook configurados na organização. O signing_secret nunca é retornado em listagens — apenas o ID e URL são exibidos.',
10
+ {
11
+ limit: z
12
+ .number()
13
+ .int()
14
+ .min(1)
15
+ .max(100)
16
+ .optional()
17
+ .describe('Número de webhooks por página (máximo 100, padrão 20).'),
18
+ cursor: z
19
+ .string()
20
+ .optional()
21
+ .describe('Cursor de paginação retornado pela chamada anterior.'),
22
+ },
23
+ async (params) => {
24
+ try {
25
+ const page = await client.webhooks.listPage({
26
+ ...(params.limit !== undefined ? { limit: params.limit } : {}),
27
+ ...(params.cursor !== undefined ? { cursor: params.cursor } : {}),
28
+ });
29
+
30
+ return {
31
+ content: [
32
+ {
33
+ type: 'text' as const,
34
+ text: JSON.stringify(
35
+ {
36
+ data: page.data.map((webhook: (typeof page.data)[number]) => ({
37
+ id: webhook.id,
38
+ url: webhook.url,
39
+ events: webhook.events,
40
+ // signing_secret NUNCA retornado em listagens
41
+ created_at: webhook.createdAt,
42
+ })),
43
+ pagination: {
44
+ has_more: page.pagination.has_more,
45
+ next_cursor: page.pagination.next_cursor,
46
+ },
47
+ total_returned: page.data.length,
48
+ },
49
+ null,
50
+ 2,
51
+ ),
52
+ },
53
+ ],
54
+ };
55
+ } catch (err) {
56
+ return {
57
+ content: [{ type: 'text' as const, text: formatError(err) }],
58
+ isError: true,
59
+ };
60
+ }
61
+ },
62
+ );
63
+ }
@@ -0,0 +1,103 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { CertynixClient } from '@certynix/sdk';
4
+ import { formatError } from '../utils/format.js';
5
+
6
+ export function registerRegisterAssetTool(server: McpServer, client: CertynixClient): void {
7
+ server.tool(
8
+ 'register_asset',
9
+ 'Registra um ativo digital na Certynix. Aceita hash SHA-256, URL pública ou conteúdo de arquivo em base64. Retorna o ID do asset, o hash criptográfico e o URL de verificação pública.',
10
+ {
11
+ hash_sha256: z
12
+ .string()
13
+ .regex(/^[a-fA-F0-9]{64}$/)
14
+ .optional()
15
+ .describe(
16
+ 'Hash SHA-256 do arquivo em formato hexadecimal (64 caracteres). Use quando já calculou o hash localmente.',
17
+ ),
18
+ url: z
19
+ .string()
20
+ .url()
21
+ .optional()
22
+ .describe('URL pública do ativo para calcular e registrar o hash.'),
23
+ file_base64: z
24
+ .string()
25
+ .optional()
26
+ .describe('Conteúdo do arquivo em base64 (máximo 10MB). Use quando tiver acesso direto ao arquivo.'),
27
+ filename: z
28
+ .string()
29
+ .optional()
30
+ .describe('Nome original do arquivo (ex: contrato-2024.pdf). Usado para referência — não afeta o hash.'),
31
+ mime_type: z
32
+ .string()
33
+ .optional()
34
+ .describe('Tipo MIME do arquivo (ex: application/pdf, image/png).'),
35
+ },
36
+ async (params) => {
37
+ try {
38
+ if (!params.hash_sha256 && !params.url && !params.file_base64) {
39
+ return {
40
+ content: [
41
+ {
42
+ type: 'text' as const,
43
+ text: 'Error: one of hash_sha256, url, or file_base64 is required',
44
+ },
45
+ ],
46
+ isError: true,
47
+ };
48
+ }
49
+
50
+ type RegisterInput = Parameters<typeof client.assets.register>[0];
51
+ let input: RegisterInput;
52
+
53
+ if (params.hash_sha256) {
54
+ input = {
55
+ hash_sha256: params.hash_sha256,
56
+ ...(params.filename !== undefined ? { filename: params.filename } : {}),
57
+ ...(params.mime_type !== undefined ? { mime_type: params.mime_type } : {}),
58
+ } as RegisterInput;
59
+ } else if (params.url) {
60
+ input = {
61
+ url: params.url,
62
+ ...(params.filename !== undefined ? { filename: params.filename } : {}),
63
+ } as RegisterInput;
64
+ } else {
65
+ const buffer = Buffer.from(params.file_base64 as string, 'base64');
66
+ input = {
67
+ file: buffer,
68
+ ...(params.filename !== undefined ? { filename: params.filename } : {}),
69
+ ...(params.mime_type !== undefined ? { mime_type: params.mime_type } : {}),
70
+ } as RegisterInput;
71
+ }
72
+
73
+ const asset = await client.assets.register(input);
74
+
75
+ return {
76
+ content: [
77
+ {
78
+ type: 'text' as const,
79
+ text: JSON.stringify(
80
+ {
81
+ id: asset.id,
82
+ hash: asset.hash,
83
+ status: asset.status,
84
+ trust_score: asset.trustScore,
85
+ verification_url: `https://certynix.com/verify/${asset.id}`,
86
+ is_first_registrant: asset.isFirstRegistrant,
87
+ created_at: asset.createdAt,
88
+ },
89
+ null,
90
+ 2,
91
+ ),
92
+ },
93
+ ],
94
+ };
95
+ } catch (err) {
96
+ return {
97
+ content: [{ type: 'text' as const, text: formatError(err) }],
98
+ isError: true,
99
+ };
100
+ }
101
+ },
102
+ );
103
+ }
@@ -0,0 +1,65 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { CertynixClient } from '@certynix/sdk';
4
+ import { formatError } from '../utils/format.js';
5
+
6
+ export function registerRevokeApiKeyTool(server: McpServer, client: CertynixClient): void {
7
+ server.tool(
8
+ 'revoke_api_key',
9
+ 'Revoga uma API Key imediatamente. Todas as integrações que usam esta chave param de funcionar instantaneamente. Esta ação é irreversível — requer confirmação explícita com confirm: true.',
10
+ {
11
+ api_key_id: z
12
+ .string()
13
+ .min(1)
14
+ .describe('ID da API Key a ser revogada (retornado pelo list_api_keys).'),
15
+ confirm: z
16
+ .literal(true)
17
+ .describe(
18
+ 'Deve ser true para confirmar a revogação. Esta ação é irreversível e interrompe imediatamente todas as integrações que usam esta chave.',
19
+ ),
20
+ },
21
+ async (params) => {
22
+ try {
23
+ // Verificação de segurança adicional além do schema
24
+ if (params.confirm !== true) {
25
+ return {
26
+ content: [
27
+ {
28
+ type: 'text' as const,
29
+ text: 'Error: confirm must be true to revoke an API Key. This action is irreversible and will immediately break all integrations using this key.',
30
+ },
31
+ ],
32
+ isError: true,
33
+ };
34
+ }
35
+
36
+ await client.apiKeys.revoke(params.api_key_id);
37
+
38
+ return {
39
+ content: [
40
+ {
41
+ type: 'text' as const,
42
+ text: JSON.stringify(
43
+ {
44
+ success: true,
45
+ api_key_id: params.api_key_id,
46
+ message:
47
+ 'API Key revogada com sucesso. Todas as integrações que usavam esta chave pararam de funcionar imediatamente.',
48
+ warning:
49
+ 'Esta ação é irreversível. Para reativar o acesso, crie uma nova API Key e atualize todas as integrações.',
50
+ },
51
+ null,
52
+ 2,
53
+ ),
54
+ },
55
+ ],
56
+ };
57
+ } catch (err) {
58
+ return {
59
+ content: [{ type: 'text' as const, text: formatError(err) }],
60
+ isError: true,
61
+ };
62
+ }
63
+ },
64
+ );
65
+ }
@@ -0,0 +1,66 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import type { CertynixClient } from '@certynix/sdk';
4
+ import { formatError } from '../utils/format.js';
5
+
6
+ export function registerVerifyAssetTool(server: McpServer, client: CertynixClient): void {
7
+ server.tool(
8
+ 'verify_asset',
9
+ 'Verifica publicamente se um ativo digital é genuíno. Retorna quem certificou o ativo e quando. Esta operação não requer autenticação e não consome quota do plano.',
10
+ {
11
+ hash_sha256: z
12
+ .string()
13
+ .regex(/^[a-fA-F0-9]{64}$/)
14
+ .optional()
15
+ .describe('Hash SHA-256 para verificar (formato hexadecimal, 64 caracteres).'),
16
+ asset_id: z
17
+ .string()
18
+ .optional()
19
+ .describe('ID do asset retornado pelo register_asset.'),
20
+ },
21
+ async (params) => {
22
+ try {
23
+ if (!params.hash_sha256 && !params.asset_id) {
24
+ return {
25
+ content: [
26
+ {
27
+ type: 'text' as const,
28
+ text: 'Error: one of hash_sha256 or asset_id is required',
29
+ },
30
+ ],
31
+ isError: true,
32
+ };
33
+ }
34
+
35
+ const result = params.hash_sha256
36
+ ? await client.verify.byHash(params.hash_sha256)
37
+ : await client.verify.byAssetId(params.asset_id as string);
38
+
39
+ return {
40
+ content: [
41
+ {
42
+ type: 'text' as const,
43
+ text: JSON.stringify(
44
+ {
45
+ match: result.match,
46
+ hash: result.hash,
47
+ certifiers: result.certifiers,
48
+ summary: result.match
49
+ ? `Asset verificado. Certificado por ${result.certifiers.length} organização(ões).`
50
+ : 'Asset não encontrado na Certynix.',
51
+ },
52
+ null,
53
+ 2,
54
+ ),
55
+ },
56
+ ],
57
+ };
58
+ } catch (err) {
59
+ return {
60
+ content: [{ type: 'text' as const, text: formatError(err) }],
61
+ isError: true,
62
+ };
63
+ }
64
+ },
65
+ );
66
+ }
@@ -0,0 +1,232 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { createHmac, timingSafeEqual } from 'node:crypto';
4
+
5
+ /**
6
+ * Tolerância máxima de timestamp para prevenir replay attacks.
7
+ * Conforme especificação: 5 minutos (300 segundos).
8
+ */
9
+ const MAX_TIMESTAMP_AGE_SECONDS = 300;
10
+
11
+ /**
12
+ * Verifica assinatura de webhook localmente via HMAC-SHA256.
13
+ * NÃO realiza chamada à API — operação 100% local.
14
+ * NUNCA loga o webhook_secret.
15
+ */
16
+ export function registerVerifyWebhookSignatureTool(server: McpServer): void {
17
+ server.tool(
18
+ 'verify_webhook_signature',
19
+ 'Valida se um delivery de webhook é genuíno verificando a assinatura HMAC-SHA256. Operação local — não realiza chamada à API. Use para garantir que o evento veio da Certynix e não foi adulterado. Proteção anti-replay: timestamps com mais de 5 minutos são rejeitados.',
20
+ {
21
+ payload: z
22
+ .string()
23
+ .min(1)
24
+ .describe('Body raw do webhook recebido (string exata, sem parsing JSON prévio).'),
25
+ signature: z
26
+ .string()
27
+ .min(1)
28
+ .describe(
29
+ 'Valor do header X-Certynix-Signature recebido (formato esperado: t=timestamp,v1=hash).',
30
+ ),
31
+ webhook_secret: z
32
+ .string()
33
+ .min(1)
34
+ .describe(
35
+ 'Signing secret do webhook (obtido ao criar o webhook via create_webhook). Este valor nunca aparecerá no output.',
36
+ ),
37
+ },
38
+ (params) => {
39
+ // NUNCA logar webhook_secret — nem em debug, nem em erro
40
+ try {
41
+ const signatureHeader = params.signature;
42
+
43
+ // Extrair timestamp e assinatura do header
44
+ // Formato: t=1706745600,v1=abc123def456...
45
+ const parts = signatureHeader.split(',');
46
+ let timestamp: string | undefined;
47
+ let receivedSignature: string | undefined;
48
+
49
+ for (const part of parts) {
50
+ if (part.startsWith('t=')) {
51
+ timestamp = part.slice(2);
52
+ } else if (part.startsWith('v1=')) {
53
+ receivedSignature = part.slice(3);
54
+ }
55
+ }
56
+
57
+ if (!timestamp || !receivedSignature) {
58
+ return {
59
+ content: [
60
+ {
61
+ type: 'text' as const,
62
+ text: JSON.stringify(
63
+ {
64
+ valid: false,
65
+ error:
66
+ "Invalid signature header format. Expected: t=<timestamp>,v1=<hash>. Got: " +
67
+ signatureHeader.substring(0, 50),
68
+ },
69
+ null,
70
+ 2,
71
+ ),
72
+ },
73
+ ],
74
+ };
75
+ }
76
+
77
+ // Validar que o timestamp é numérico
78
+ const timestampNumber = parseInt(timestamp, 10);
79
+ if (isNaN(timestampNumber)) {
80
+ return {
81
+ content: [
82
+ {
83
+ type: 'text' as const,
84
+ text: JSON.stringify(
85
+ {
86
+ valid: false,
87
+ error: 'Invalid timestamp in signature header: not a number.',
88
+ },
89
+ null,
90
+ 2,
91
+ ),
92
+ },
93
+ ],
94
+ };
95
+ }
96
+
97
+ // Verificar anti-replay: timestamp não pode ser mais antigo que 5 minutos
98
+ const nowSeconds = Math.floor(Date.now() / 1000);
99
+ const ageSeconds = nowSeconds - timestampNumber;
100
+
101
+ if (ageSeconds > MAX_TIMESTAMP_AGE_SECONDS) {
102
+ return {
103
+ content: [
104
+ {
105
+ type: 'text' as const,
106
+ text: JSON.stringify(
107
+ {
108
+ valid: false,
109
+ error: `Replay attack detected: timestamp is ${ageSeconds} seconds old (maximum allowed: ${MAX_TIMESTAMP_AGE_SECONDS} seconds).`,
110
+ timestamp_age_seconds: ageSeconds,
111
+ },
112
+ null,
113
+ 2,
114
+ ),
115
+ },
116
+ ],
117
+ };
118
+ }
119
+
120
+ if (ageSeconds < -60) {
121
+ return {
122
+ content: [
123
+ {
124
+ type: 'text' as const,
125
+ text: JSON.stringify(
126
+ {
127
+ valid: false,
128
+ error:
129
+ 'Invalid timestamp: timestamp is in the future. Possible clock skew or tampered request.',
130
+ },
131
+ null,
132
+ 2,
133
+ ),
134
+ },
135
+ ],
136
+ };
137
+ }
138
+
139
+ // Calcular HMAC-SHA256
140
+ // Algoritmo: HMAC(signing_secret, timestamp + "." + raw_body)
141
+ const signedPayload = `${timestamp}.${params.payload}`;
142
+ const expectedHmac = createHmac('sha256', params.webhook_secret)
143
+ .update(signedPayload, 'utf8')
144
+ .digest('hex');
145
+
146
+ // Comparação constant-time para prevenir timing attacks
147
+ let signaturesMatch = false;
148
+ try {
149
+ const expectedBuffer = Buffer.from(expectedHmac, 'hex');
150
+ const receivedBuffer = Buffer.from(receivedSignature, 'hex');
151
+
152
+ if (expectedBuffer.length === receivedBuffer.length) {
153
+ signaturesMatch = timingSafeEqual(expectedBuffer, receivedBuffer);
154
+ }
155
+ } catch {
156
+ // Buffer com hex inválido — assinatura malformada
157
+ signaturesMatch = false;
158
+ }
159
+
160
+ if (!signaturesMatch) {
161
+ return {
162
+ content: [
163
+ {
164
+ type: 'text' as const,
165
+ text: JSON.stringify(
166
+ {
167
+ valid: false,
168
+ error:
169
+ 'Invalid signature: HMAC-SHA256 mismatch. The webhook payload may have been tampered with, or the wrong signing secret was used.',
170
+ },
171
+ null,
172
+ 2,
173
+ ),
174
+ },
175
+ ],
176
+ };
177
+ }
178
+
179
+ // Extrair tipo de evento do payload (sem logar o conteúdo completo)
180
+ let eventType: string | undefined;
181
+ try {
182
+ const parsed = JSON.parse(params.payload) as Record<string, unknown>;
183
+ if (typeof parsed['type'] === 'string') {
184
+ eventType = parsed['type'];
185
+ }
186
+ } catch {
187
+ // Payload não é JSON válido — ainda pode ser válido dependendo do uso
188
+ }
189
+
190
+ return {
191
+ content: [
192
+ {
193
+ type: 'text' as const,
194
+ text: JSON.stringify(
195
+ {
196
+ valid: true,
197
+ timestamp: timestampNumber,
198
+ timestamp_age_seconds: ageSeconds,
199
+ ...(eventType !== undefined ? { event_type: eventType } : {}),
200
+ message:
201
+ 'Assinatura válida. O webhook é genuíno e foi enviado pela Certynix.',
202
+ },
203
+ null,
204
+ 2,
205
+ ),
206
+ },
207
+ ],
208
+ };
209
+ } catch (err) {
210
+ // NUNCA incluir webhook_secret em mensagens de erro
211
+ return {
212
+ content: [
213
+ {
214
+ type: 'text' as const,
215
+ text: JSON.stringify(
216
+ {
217
+ valid: false,
218
+ error:
219
+ err instanceof Error
220
+ ? `Verification failed: ${err.message}`
221
+ : 'Verification failed: unknown error',
222
+ },
223
+ null,
224
+ 2,
225
+ ),
226
+ },
227
+ ],
228
+ };
229
+ }
230
+ },
231
+ );
232
+ }
@@ -0,0 +1,20 @@
1
+ import { sanitizeForOutput } from './mask.js';
2
+
3
+ /**
4
+ * Formata um asset para exibição no output de uma tool call.
5
+ * Remove campos sensíveis antes de serializar.
6
+ */
7
+ export function formatAssetOutput(asset: unknown): string {
8
+ return JSON.stringify(sanitizeForOutput(asset), null, 2);
9
+ }
10
+
11
+ /**
12
+ * Formata um erro para mensagem legível no output de uma tool call.
13
+ * Nunca inclui stack trace ou detalhes internos.
14
+ */
15
+ export function formatError(err: unknown): string {
16
+ if (err instanceof Error) {
17
+ return `Error: ${err.message}`;
18
+ }
19
+ return `Unknown error: ${String(err)}`;
20
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Máscara de campos sensíveis para logs e outputs do MCP Server.
3
+ * NUNCA expor API Keys, secrets ou tokens em qualquer output.
4
+ */
5
+
6
+ /**
7
+ * Mascara uma API Key para exibição em logs.
8
+ * Formato de saída: cnx_live_sk_*** (primeiros 12 caracteres + ***)
9
+ */
10
+ export function maskApiKey(key: string): string {
11
+ if (!key.startsWith('cnx_')) return '***';
12
+ return key.substring(0, 12) + '***';
13
+ }
14
+
15
+ /**
16
+ * Remove campos sensíveis recursivamente de um objeto antes de qualquer output.
17
+ * Campos sensíveis nunca devem aparecer em respostas de tool calls.
18
+ */
19
+ export function sanitizeForOutput(obj: unknown): unknown {
20
+ if (typeof obj !== 'object' || obj === null) return obj;
21
+ if (Array.isArray(obj)) return obj.map(sanitizeForOutput);
22
+
23
+ const result: Record<string, unknown> = {};
24
+ for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
25
+ const sensitiveKeys = [
26
+ 'apiKey',
27
+ 'api_key',
28
+ 'secret',
29
+ 'token',
30
+ 'password',
31
+ 'signing_secret',
32
+ 'webhook_secret',
33
+ ];
34
+ if (sensitiveKeys.some((k) => key.toLowerCase().includes(k.toLowerCase()))) {
35
+ result[key] = '***';
36
+ } else {
37
+ result[key] = sanitizeForOutput(value);
38
+ }
39
+ }
40
+ return result;
41
+ }