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