@hed-hog/core 0.0.386 → 0.0.387

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 (61) hide show
  1. package/dist/auth/auth.controller.d.ts +2 -1
  2. package/dist/auth/auth.controller.d.ts.map +1 -1
  3. package/dist/auth/auth.controller.js.map +1 -1
  4. package/dist/auth/auth.service.d.ts +2 -1
  5. package/dist/auth/auth.service.d.ts.map +1 -1
  6. package/dist/auth/auth.service.js.map +1 -1
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  12. package/dist/integration-profile/integration-profile.secrets.js +3 -1
  13. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  14. package/dist/integration-profile/integration-profile.service.d.ts +7 -0
  15. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  16. package/dist/integration-profile/integration-profile.service.js +73 -2
  17. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  18. package/dist/integration-profile/integration-profile.utils.d.ts +18 -0
  19. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  20. package/dist/integration-profile/integration-profile.utils.js +16 -0
  21. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  22. package/dist/profile/profile.controller.d.ts +3 -2
  23. package/dist/profile/profile.controller.d.ts.map +1 -1
  24. package/dist/profile/profile.controller.js.map +1 -1
  25. package/dist/profile/profile.service.d.ts +4 -3
  26. package/dist/profile/profile.service.d.ts.map +1 -1
  27. package/dist/profile/profile.service.js.map +1 -1
  28. package/dist/webhook-integration/webhook-integration.controller.d.ts +5 -8
  29. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  30. package/dist/webhook-integration/webhook-integration.controller.js +31 -8
  31. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  32. package/dist/webhook-integration/webhook-integration.module.d.ts.map +1 -1
  33. package/dist/webhook-integration/webhook-integration.module.js +5 -0
  34. package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
  35. package/dist/webhook-integration/webhook-integration.service.d.ts +33 -1
  36. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  37. package/dist/webhook-integration/webhook-integration.service.js +109 -4
  38. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  39. package/dist/webhook-integration/webhook-protocol.registry.d.ts +63 -0
  40. package/dist/webhook-integration/webhook-protocol.registry.d.ts.map +1 -0
  41. package/dist/webhook-integration/webhook-protocol.registry.js +51 -0
  42. package/dist/webhook-integration/webhook-protocol.registry.js.map +1 -0
  43. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +16 -8
  44. package/hedhog/frontend/messages/en.json +22 -0
  45. package/hedhog/frontend/messages/pt.json +22 -0
  46. package/hedhog/table/webhook_integration.yaml +64 -43
  47. package/package.json +5 -5
  48. package/src/auth/auth.controller.ts +2 -1
  49. package/src/auth/auth.service.ts +2 -1
  50. package/src/index.ts +1 -0
  51. package/src/integration-profile/integration-profile.secrets.ts +3 -1
  52. package/src/integration-profile/integration-profile.service.ts +110 -3
  53. package/src/integration-profile/integration-profile.utils.spec.ts +75 -0
  54. package/src/integration-profile/integration-profile.utils.ts +39 -0
  55. package/src/profile/profile.controller.ts +6 -2
  56. package/src/profile/profile.service.ts +5 -3
  57. package/src/webhook-integration/webhook-integration.controller.ts +40 -2
  58. package/src/webhook-integration/webhook-integration.module.ts +5 -0
  59. package/src/webhook-integration/webhook-integration.service.ts +133 -0
  60. package/src/webhook-integration/webhook-protocol.registry.spec.ts +70 -0
  61. package/src/webhook-integration/webhook-protocol.registry.ts +93 -0
@@ -0,0 +1,63 @@
1
+ import { Request } from 'express';
2
+ /**
3
+ * Resposta que um adaptador devolve ao controller público.
4
+ *
5
+ * `contentType` existe porque nem todo provedor aceita JSON: o handshake da Meta,
6
+ * por exemplo, compara o corpo com o challenge literalmente — um envelope JSON
7
+ * reprova a verificação.
8
+ */
9
+ export type WebhookProtocolResponse = {
10
+ status: number;
11
+ contentType?: string;
12
+ body: string | object;
13
+ };
14
+ export type WebhookProtocolRequest = {
15
+ /** O registro de webhook_integration que casou com o uuid da URL. */
16
+ integration: {
17
+ id: number;
18
+ slug: string;
19
+ integration_profile_id: number | null;
20
+ };
21
+ /** Body já parseado (POST) ou query params (GET). */
22
+ body: unknown;
23
+ query: Record<string, string>;
24
+ headers: Record<string, string | string[] | undefined>;
25
+ /**
26
+ * Bytes originais da requisição. Obrigatório para validar assinaturas HMAC:
27
+ * reserializar o JSON reordena chaves e invalida a assinatura.
28
+ */
29
+ rawBody?: Buffer;
30
+ remoteIp: string;
31
+ req: Request;
32
+ };
33
+ /**
34
+ * Adaptador de um protocolo de webhook de entrada.
35
+ *
36
+ * O framework de webhooks do core é um disparador de ações genérico, autenticado
37
+ * por token estático. Provedores como a Meta falam um protocolo próprio (handshake
38
+ * de verificação + assinatura HMAC) que não cabe nesse modelo.
39
+ *
40
+ * Em vez de espalhar `if (provider === 'meta')` pelo core, um módulo registra aqui
41
+ * um adaptador e passa a responder pela mesma URL pública (`/webhook/:uuid`). O core
42
+ * continua sem conhecer nenhum provedor específico.
43
+ */
44
+ export interface IWebhookProtocolAdapter {
45
+ /** GET na URL pública (tipicamente o handshake de verificação). */
46
+ verify(request: WebhookProtocolRequest): Promise<WebhookProtocolResponse>;
47
+ /** POST na URL pública (o evento em si). */
48
+ handle(request: WebhookProtocolRequest): Promise<WebhookProtocolResponse>;
49
+ }
50
+ /**
51
+ * Registro dos adaptadores de protocolo, no mesmo padrão do QueueHandlerRegistry
52
+ * e do FiscalProviderRegistry: o módulo dono do protocolo se registra no boot.
53
+ */
54
+ export declare class WebhookProtocolRegistry {
55
+ private readonly logger;
56
+ private readonly adapters;
57
+ register(protocol: string, adapter: IWebhookProtocolAdapter): void;
58
+ get(protocol: string | null | undefined): IWebhookProtocolAdapter | null;
59
+ has(protocol: string | null | undefined): boolean;
60
+ /** Protocolos registrados (para a UI listar o que existe). */
61
+ list(): string[];
62
+ }
63
+ //# sourceMappingURL=webhook-protocol.registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-protocol.registry.d.ts","sourceRoot":"","sources":["../../src/webhook-integration/webhook-protocol.registry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC;;;;;;GAMG;AACH,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,qEAAqE;IACrE,WAAW,EAAE;QACX,EAAE,EAAE,MAAM,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,sBAAsB,EAAE,MAAM,GAAG,IAAI,CAAC;KACvC,CAAC;IACF,qDAAqD;IACrD,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,OAAO,CAAC;CACd,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,WAAW,uBAAuB;IACtC,mEAAmE;IACnE,MAAM,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;IAC1E,4CAA4C;IAC5C,MAAM,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC3E;AAED;;;GAGG;AACH,qBACa,uBAAuB;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA4C;IACnE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA8C;IAEvE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,uBAAuB,GAAG,IAAI;IAYlE,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,uBAAuB,GAAG,IAAI;IAKxE,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO;IAIjD,8DAA8D;IAC9D,IAAI,IAAI,MAAM,EAAE;CAGjB"}
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var WebhookProtocolRegistry_1;
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.WebhookProtocolRegistry = void 0;
11
+ const common_1 = require("@nestjs/common");
12
+ /**
13
+ * Registro dos adaptadores de protocolo, no mesmo padrão do QueueHandlerRegistry
14
+ * e do FiscalProviderRegistry: o módulo dono do protocolo se registra no boot.
15
+ */
16
+ let WebhookProtocolRegistry = WebhookProtocolRegistry_1 = class WebhookProtocolRegistry {
17
+ constructor() {
18
+ this.logger = new common_1.Logger(WebhookProtocolRegistry_1.name);
19
+ this.adapters = new Map();
20
+ }
21
+ register(protocol, adapter) {
22
+ const key = normalizeProtocol(protocol);
23
+ if (!key)
24
+ throw new Error('Webhook protocol key is required.');
25
+ if (this.adapters.has(key)) {
26
+ this.logger.warn(`Webhook protocol "${key}" was overwritten.`);
27
+ }
28
+ this.adapters.set(key, adapter);
29
+ this.logger.log(`Registered webhook protocol "${key}"`);
30
+ }
31
+ get(protocol) {
32
+ var _a;
33
+ const key = normalizeProtocol(protocol);
34
+ return key ? ((_a = this.adapters.get(key)) !== null && _a !== void 0 ? _a : null) : null;
35
+ }
36
+ has(protocol) {
37
+ return this.get(protocol) !== null;
38
+ }
39
+ /** Protocolos registrados (para a UI listar o que existe). */
40
+ list() {
41
+ return [...this.adapters.keys()].sort();
42
+ }
43
+ };
44
+ exports.WebhookProtocolRegistry = WebhookProtocolRegistry;
45
+ exports.WebhookProtocolRegistry = WebhookProtocolRegistry = WebhookProtocolRegistry_1 = __decorate([
46
+ (0, common_1.Injectable)()
47
+ ], WebhookProtocolRegistry);
48
+ function normalizeProtocol(protocol) {
49
+ return String(protocol !== null && protocol !== void 0 ? protocol : '').trim().toLowerCase();
50
+ }
51
+ //# sourceMappingURL=webhook-protocol.registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"webhook-protocol.registry.js","sourceRoot":"","sources":["../../src/webhook-integration/webhook-protocol.registry.ts"],"names":[],"mappings":";;;;;;;;;;AAAA,2CAAoD;AAsDpD;;;GAGG;AAEI,IAAM,uBAAuB,+BAA7B,MAAM,uBAAuB;IAA7B;QACY,WAAM,GAAG,IAAI,eAAM,CAAC,yBAAuB,CAAC,IAAI,CAAC,CAAC;QAClD,aAAQ,GAAG,IAAI,GAAG,EAAmC,CAAC;IA2BzE,CAAC;IAzBC,QAAQ,CAAC,QAAgB,EAAE,OAAgC;QACzD,MAAM,GAAG,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,CAAC,GAAG;YAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;QAE/D,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,qBAAqB,GAAG,oBAAoB,CAAC,CAAC;QACjE,CAAC;QAED,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAChC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,gCAAgC,GAAG,GAAG,CAAC,CAAC;IAC1D,CAAC;IAED,GAAG,CAAC,QAAmC;;QACrC,MAAM,GAAG,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;QACxC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,MAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,mCAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACvD,CAAC;IAED,GAAG,CAAC,QAAmC;QACrC,OAAO,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC;IACrC,CAAC;IAED,8DAA8D;IAC9D,IAAI;QACF,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1C,CAAC;CACF,CAAA;AA7BY,0DAAuB;kCAAvB,uBAAuB;IADnC,IAAA,mBAAU,GAAE;GACA,uBAAuB,CA6BnC;AAED,SAAS,iBAAiB,CAAC,QAAmC;IAC5D,OAAO,MAAM,CAAC,QAAQ,aAAR,QAAQ,cAAR,QAAQ,GAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACrD,CAAC"}
@@ -48,7 +48,8 @@ import {
48
48
  TableRow,
49
49
  } from '@/components/ui/table';
50
50
  import { TableBodySkeleton } from '@/components/ui/skeletons';
51
- import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
51
+ import { Tabs, TabsContent } from '@/components/ui/tabs';
52
+ import { TabsOverflowList } from '@/components/ui/tabs-overflow-list';
52
53
  import { usePagination } from '@/hooks/use-pagination';
53
54
  import { formatDate } from '@/lib/format-date';
54
55
  import { useApp } from '@hed-hog/next-app-provider';
@@ -696,13 +697,20 @@ export default function IntegrationProfilesPage() {
696
697
  />
697
698
 
698
699
  <Tabs value={activeTab} onValueChange={setActiveTab}>
699
- <TabsList>
700
- {types.map((ty) => (
701
- <TabsTrigger key={ty.slug} value={ty.slug}>
702
- {getLocaleName(ty.integration_type_locale, currentLocaleCode)}
703
- </TabsTrigger>
704
- ))}
705
- </TabsList>
700
+ {/* Colapsa as abas que não cabem num dropdown "Mais": há muitos tipos de
701
+ integração e em telas estreitas os TabsTrigger crus encolhiam e se
702
+ sobrepunham. triggerClassName replica o padding do TabsTrigger nos
703
+ spans de medição invisíveis do TabsOverflowList. */}
704
+ <TabsOverflowList
705
+ items={types.map((ty) => ({
706
+ value: ty.slug,
707
+ label: getLocaleName(ty.integration_type_locale, currentLocaleCode),
708
+ }))}
709
+ activeValue={activeTab}
710
+ onValueChange={setActiveTab}
711
+ moreLabel={t('tabsMore')}
712
+ triggerClassName="min-h-8 px-3 text-sm font-medium"
713
+ />
706
714
 
707
715
  {types.map((ty) => (
708
716
  <TabsContent key={ty.slug} value={ty.slug} className="mt-4">
@@ -2339,6 +2339,7 @@
2339
2339
  "title": "Integration Profiles",
2340
2340
  "description": "Manage credentials and connection settings for email, messaging, storage, AI, OAuth, and payment providers.",
2341
2341
  "searchPlaceholder": "Search profiles by name or slug...",
2342
+ "tabsMore": "More",
2342
2343
  "selectProfile": "Select profile",
2343
2344
  "selectAllVisible": "Select all visible profiles",
2344
2345
  "newProfile": "New Profile",
@@ -2432,6 +2433,27 @@
2432
2433
  "fieldPhoneNumberId": "Phone Number ID",
2433
2434
  "fieldAccessToken": "Access Token",
2434
2435
  "fieldBusinessId": "Business ID",
2436
+ "fieldAppId": "Meta App ID",
2437
+ "fieldAppSecret": "Meta App Secret",
2438
+ "fieldWabaId": "WhatsApp Business Account ID (WABA)",
2439
+ "fieldVerifyToken": "Webhook Verify Token",
2440
+ "fieldGraphApiVersion": "Graph API Version",
2441
+ "fieldEnvironmentTest": "Test",
2442
+ "fieldEnvironmentProduction": "Production",
2443
+ "copiedToClipboard": "Copied to clipboard",
2444
+ "whatsappWebhookSectionTitle": "Meta Webhook",
2445
+ "whatsappWebhookDescription": "Register this webhook in your Meta app to receive WhatsApp messages.",
2446
+ "whatsappWebhookCallbackUrlLabel": "Callback URL",
2447
+ "whatsappWebhookVerifyTokenLabel": "Verify Token",
2448
+ "whatsappWebhookVerifyTokenHint": "Use the same value you set in the Verify Token field above.",
2449
+ "whatsappWebhookLoading": "Generating URL…",
2450
+ "whatsappWebhookUnavailable": "URL unavailable — check the API base URL setting.",
2451
+ "whatsappWebhookSaveFirst": "Save the profile to generate the webhook URL.",
2452
+ "whatsappWebhookStepsTitle": "In the Meta dashboard (WhatsApp → Configuration):",
2453
+ "whatsappWebhookStep1": "Paste the Callback URL and the Verify Token above.",
2454
+ "whatsappWebhookStep2": "Click \"Verify and save\".",
2455
+ "whatsappWebhookStep3": "Subscribe to the \"messages\" field.",
2456
+ "whatsappWebhookStep4": "Send a test message and confirm it arrives.",
2435
2457
  "fieldBasePath": "Base Path",
2436
2458
  "fieldBucket": "Bucket",
2437
2459
  "fieldProjectId": "Project ID",
@@ -2394,6 +2394,7 @@
2394
2394
  "title": "Perfis de Integração",
2395
2395
  "description": "Gerencie credenciais para provedores de e-mail, mensagens, armazenamento, IA, OAuth e pagamentos.",
2396
2396
  "searchPlaceholder": "Buscar perfis por nome ou slug...",
2397
+ "tabsMore": "Mais",
2397
2398
  "selectProfile": "Selecionar perfil",
2398
2399
  "selectAllVisible": "Selecionar todos os perfis visíveis",
2399
2400
  "newProfile": "Novo Perfil",
@@ -2487,6 +2488,27 @@
2487
2488
  "fieldPhoneNumberId": "ID do Número de Telefone",
2488
2489
  "fieldAccessToken": "Access Token",
2489
2490
  "fieldBusinessId": "Business ID",
2491
+ "fieldAppId": "Meta App ID",
2492
+ "fieldAppSecret": "Meta App Secret",
2493
+ "fieldWabaId": "ID da Conta do WhatsApp Business (WABA)",
2494
+ "fieldVerifyToken": "Verify Token do Webhook",
2495
+ "fieldGraphApiVersion": "Versão da Graph API",
2496
+ "fieldEnvironmentTest": "Teste",
2497
+ "fieldEnvironmentProduction": "Produção",
2498
+ "copiedToClipboard": "Copiado para a área de transferência",
2499
+ "whatsappWebhookSectionTitle": "Webhook da Meta",
2500
+ "whatsappWebhookDescription": "Registre este webhook no seu app da Meta para receber mensagens do WhatsApp.",
2501
+ "whatsappWebhookCallbackUrlLabel": "URL de Callback",
2502
+ "whatsappWebhookVerifyTokenLabel": "Verify Token",
2503
+ "whatsappWebhookVerifyTokenHint": "Use o mesmo valor que você definiu no campo Verify Token acima.",
2504
+ "whatsappWebhookLoading": "Gerando URL…",
2505
+ "whatsappWebhookUnavailable": "URL indisponível — verifique a configuração de URL base da API.",
2506
+ "whatsappWebhookSaveFirst": "Salve o perfil para gerar a URL do webhook.",
2507
+ "whatsappWebhookStepsTitle": "No painel da Meta (WhatsApp → Configuração):",
2508
+ "whatsappWebhookStep1": "Cole a URL de Callback e o Verify Token acima.",
2509
+ "whatsappWebhookStep2": "Clique em \"Verificar e salvar\".",
2510
+ "whatsappWebhookStep3": "Assine o campo \"messages\".",
2511
+ "whatsappWebhookStep4": "Envie uma mensagem de teste e confirme que ela chega.",
2490
2512
  "fieldBasePath": "Caminho Base",
2491
2513
  "fieldBucket": "Bucket",
2492
2514
  "fieldProjectId": "Project ID",
@@ -1,43 +1,64 @@
1
- columns:
2
- - type: pk
3
- - type: slug
4
- - name: name
5
- type: varchar
6
- length: 127
7
- - name: description
8
- type: text
9
- isNullable: true
10
- - name: public_uuid
11
- type: varchar
12
- length: 36
13
- - name: status
14
- type: enum
15
- enum:
16
- - active
17
- - inactive
18
- default: active
19
- - name: require_token
20
- type: boolean
21
- default: false
22
- - name: token_hash
23
- type: varchar
24
- length: 128
25
- isNullable: true
26
- - name: allowed_ips
27
- type: json
28
- isNullable: true
29
- - name: body_schema
30
- type: json
31
- isNullable: true
32
- - type: created_at
33
- - type: updated_at
34
- indices:
35
- - columns:
36
- - slug
37
- isUnique: true
38
- - columns:
39
- - public_uuid
40
- isUnique: true
41
- - columns:
42
- - status
43
-
1
+ columns:
2
+ - type: pk
3
+ - type: slug
4
+ - name: name
5
+ type: varchar
6
+ length: 127
7
+ - name: description
8
+ type: text
9
+ isNullable: true
10
+ - name: public_uuid
11
+ type: varchar
12
+ length: 36
13
+ - name: status
14
+ type: enum
15
+ enum:
16
+ - active
17
+ - inactive
18
+ default: active
19
+ - name: require_token
20
+ type: boolean
21
+ default: false
22
+ - name: token_hash
23
+ type: varchar
24
+ length: 128
25
+ isNullable: true
26
+ - name: allowed_ips
27
+ type: json
28
+ isNullable: true
29
+ - name: body_schema
30
+ type: json
31
+ isNullable: true
32
+ # Protocolo de entrada. Nulo = webhook clássico (token + actions). Preenchido,
33
+ # a chamada é delegada ao adaptador registrado por outro módulo no
34
+ # WebhookProtocolRegistry (ex.: 'meta-whatsapp'), que faz a própria autenticação.
35
+ - name: protocol
36
+ type: varchar
37
+ length: 63
38
+ isNullable: true
39
+ # Perfil que guarda as credenciais do protocolo (ex.: o app_secret que assina o
40
+ # HMAC). Nulo nos webhooks clássicos, que não têm provedor por trás.
41
+ - name: integration_profile_id
42
+ type: fk
43
+ isNullable: true
44
+ references:
45
+ table: integration_profile
46
+ column: id
47
+ onDelete: CASCADE
48
+ onUpdate: CASCADE
49
+ - type: created_at
50
+ - type: updated_at
51
+ indices:
52
+ - columns:
53
+ - slug
54
+ isUnique: true
55
+ - columns:
56
+ - public_uuid
57
+ isUnique: true
58
+ - columns:
59
+ - status
60
+ - columns:
61
+ - protocol
62
+ - columns:
63
+ - integration_profile_id
64
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/core",
3
- "version": "0.0.386",
3
+ "version": "0.0.387",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -41,12 +41,12 @@
41
41
  "speakeasy": "^2.0.0",
42
42
  "ua-parser-js": "^2.0.10",
43
43
  "uuid": "^11.1.0",
44
- "@hed-hog/api-mail": "0.0.9",
45
- "@hed-hog/api-types": "0.0.1",
46
44
  "@hed-hog/api-prisma": "0.0.6",
47
- "@hed-hog/api": "0.0.8",
45
+ "@hed-hog/api-types": "0.0.1",
46
+ "@hed-hog/api-mail": "0.0.9",
47
+ "@hed-hog/api-pagination": "0.0.7",
48
48
  "@hed-hog/api-locale": "0.0.14",
49
- "@hed-hog/api-pagination": "0.0.7"
49
+ "@hed-hog/api": "0.0.8"
50
50
  },
51
51
  "exports": {
52
52
  ".": {
@@ -16,6 +16,7 @@ import {
16
16
  UseGuards
17
17
  } from '@nestjs/common';
18
18
  import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
19
+ import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
19
20
  import { TokenService } from '../token/token.service';
20
21
  import { CreateWithEmailAndPasswordDTO } from '../user/dto/create-with-email-and-password.dto';
21
22
  import { UserService } from '../user/user.service';
@@ -206,7 +207,7 @@ export class AuthController {
206
207
  @Locale() locale: string,
207
208
  @Body() { mfaToken }: { mfaToken: string },
208
209
  @Headers('origin') origin: string
209
- ) {
210
+ ): Promise<PublicKeyCredentialRequestOptionsJSON> {
210
211
  return this.service.generateWebAuthnAuthenticationOptions(locale, mfaToken, origin);
211
212
  }
212
213
 
@@ -1,6 +1,7 @@
1
1
  import { getLocaleText } from '@hed-hog/api-locale';
2
2
  import { PrismaService } from '@hed-hog/api-prisma';
3
3
  import { User } from '@hed-hog/api-types';
4
+ import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
4
5
  import {
5
6
  BadRequestException,
6
7
  forwardRef,
@@ -890,7 +891,7 @@ export class AuthService {
890
891
  };
891
892
  }
892
893
 
893
- async generateWebAuthnAuthenticationOptions(locale: string, mfaToken: string, requestOrigin?: string) {
894
+ async generateWebAuthnAuthenticationOptions(locale: string, mfaToken: string, requestOrigin?: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
894
895
  const { generateAuthenticationOptions } = await import('@simplewebauthn/server');
895
896
  const payload = await this.token.verifyMfaChallengeToken(mfaToken);
896
897
 
package/src/index.ts CHANGED
@@ -45,6 +45,7 @@ export * from './webhook-integration/event-webhook.service';
45
45
  export * from './webhook-integration/integration-event-catalog.service';
46
46
  export * from './webhook-integration/webhook-integration.module';
47
47
  export * from './webhook-integration/webhook-integration.service';
48
+ export * from './webhook-integration/webhook-protocol.registry';
48
49
 
49
50
  // Integration Module
50
51
  export * from './integration/integration.module';
@@ -22,7 +22,9 @@ export const INTEGRATION_SECRET_KEYS: Record<string, string[]> = {
22
22
  resend: ['api_token', 'webhook_secret'],
23
23
  // Mensageria
24
24
  'evolution-api': ['token'],
25
- 'whatsapp-official': ['access_token'],
25
+ // app_secret assina o HMAC do webhook (X-Hub-Signature-256) e verify_token é
26
+ // comparado no handshake da Meta: quem tem qualquer um dos dois forja eventos.
27
+ 'whatsapp-official': ['access_token', 'app_secret', 'verify_token'],
26
28
  // Storage
27
29
  s3: ['secret_access_key'],
28
30
  gcs: ['key_file_json'],
@@ -9,7 +9,10 @@ import {
9
9
  forwardRef,
10
10
  } from '@nestjs/common';
11
11
  import * as pako from 'pako';
12
- import { buildMailConfigFromIntegration } from './integration-profile.utils';
12
+ import {
13
+ buildMailConfigFromIntegration,
14
+ buildWhatsappOfficialConfigFromIntegration,
15
+ } from './integration-profile.utils';
13
16
  import { CreateDTO } from './dto/create.dto';
14
17
  import {
15
18
  ConflictResolutionDTO,
@@ -451,9 +454,10 @@ export class IntegrationProfileService {
451
454
  );
452
455
  }
453
456
 
454
- if (provider.integration_type.slug !== 'email') {
457
+ const typeSlug = provider.integration_type.slug;
458
+ if (typeSlug !== 'email' && typeSlug !== 'whatsapp') {
455
459
  throw new BadRequestException(
456
- 'Integration profile test is only available for email type.',
460
+ 'Integration profile test is only available for email and whatsapp types.',
457
461
  );
458
462
  }
459
463
 
@@ -472,6 +476,14 @@ export class IntegrationProfileService {
472
476
  ) as Record<string, unknown>;
473
477
  }
474
478
 
479
+ if (typeSlug === 'whatsapp') {
480
+ return this.testWhatsappProfile(
481
+ provider.slug,
482
+ data.slug.trim(),
483
+ effectiveConfig,
484
+ );
485
+ }
486
+
475
487
  // Resend não usa SMTP/from_email: valida credenciais/domínio próprios.
476
488
  if (provider.slug === 'resend') {
477
489
  const resendCfg = (effectiveConfig as Record<string, unknown> | null) ?? {};
@@ -508,6 +520,101 @@ export class IntegrationProfileService {
508
520
  };
509
521
  }
510
522
 
523
+ /**
524
+ * Valida um perfil whatsapp-official contra a Graph API (lê o WABA com o token)
525
+ * e registra o resultado no config. Nunca devolve o token/secret na resposta.
526
+ */
527
+ private async testWhatsappProfile(
528
+ providerSlug: string,
529
+ slug: string,
530
+ effectiveConfig: Record<string, unknown> | null,
531
+ ) {
532
+ if (providerSlug !== 'whatsapp-official') {
533
+ throw new BadRequestException(
534
+ `Integration profile test is not available for provider "${providerSlug}".`,
535
+ );
536
+ }
537
+
538
+ const cfg = buildWhatsappOfficialConfigFromIntegration(effectiveConfig);
539
+
540
+ if (!cfg.accessToken) {
541
+ throw new BadRequestException('WhatsApp access token is required.');
542
+ }
543
+ if (!cfg.wabaId) {
544
+ throw new BadRequestException('WhatsApp Business Account ID is required.');
545
+ }
546
+ if (!cfg.appSecret) {
547
+ throw new BadRequestException(
548
+ 'Meta App Secret is required to validate webhook signatures.',
549
+ );
550
+ }
551
+ if (!cfg.verifyToken) {
552
+ throw new BadRequestException('Webhook verify token is required.');
553
+ }
554
+
555
+ const url = `https://graph.facebook.com/${cfg.graphApiVersion}/${encodeURIComponent(
556
+ cfg.wabaId,
557
+ )}?fields=id,name`;
558
+
559
+ let name = '';
560
+ let failure: string | null = null;
561
+ try {
562
+ const response = await fetch(url, {
563
+ headers: { Authorization: `Bearer ${cfg.accessToken}` },
564
+ });
565
+ const body = (await response.json().catch(() => ({}))) as {
566
+ name?: string;
567
+ error?: { message?: string };
568
+ };
569
+ if (!response.ok) {
570
+ // A mensagem da Meta não contém o token; ainda assim só propagamos o texto dela.
571
+ failure =
572
+ body?.error?.message ?? `Graph API returned HTTP ${response.status}.`;
573
+ } else {
574
+ name = String(body?.name ?? '');
575
+ }
576
+ } catch (error) {
577
+ failure = error instanceof Error ? error.message : 'Graph API is unreachable.';
578
+ }
579
+
580
+ await this.recordWhatsappValidation(providerSlug, slug, failure);
581
+
582
+ if (failure) {
583
+ throw new BadRequestException(`WhatsApp validation failed: ${failure}`);
584
+ }
585
+
586
+ return { success: true, destination: name || cfg.wabaId };
587
+ }
588
+
589
+ /** Grava metadados (não-secretos) da última validação no config do perfil. */
590
+ private async recordWhatsappValidation(
591
+ providerSlug: string,
592
+ slug: string,
593
+ failure: string | null,
594
+ ) {
595
+ const stored = await this.prismaService.integration_profile.findUnique({
596
+ where: { slug },
597
+ select: { id: true, config: true },
598
+ });
599
+ // Perfil ainda não salvo (teste no fluxo de criação): nada a registrar.
600
+ if (!stored) return;
601
+
602
+ // `stored.config` vem decifrado pelo middleware; recifra-mos ao gravar.
603
+ const plain = {
604
+ ...((stored.config as Record<string, unknown>) ?? {}),
605
+ last_validated_at: new Date().toISOString(),
606
+ last_validation_status: failure ? 'error' : 'ok',
607
+ last_error: failure ?? null,
608
+ };
609
+
610
+ await this.prismaService.integration_profile.update({
611
+ where: { id: stored.id },
612
+ data: {
613
+ config: this.credentialCrypto.encryptConfig(providerSlug, plain) as object,
614
+ },
615
+ });
616
+ }
617
+
511
618
  async exportProfiles({
512
619
  ids,
513
620
  includeSecrets = false,
@@ -12,6 +12,8 @@ import {
12
12
  buildDigitalOceanConfigFromIntegration,
13
13
  buildKubernetesConfigFromIntegration,
14
14
  buildAiConfigFromIntegration,
15
+ buildWhatsappOfficialConfigFromIntegration,
16
+ DEFAULT_GRAPH_API_VERSION,
15
17
  } from './integration-profile.utils';
16
18
 
17
19
  describe('integration-profile.utils', () => {
@@ -329,4 +331,77 @@ describe('integration-profile.utils', () => {
329
331
  );
330
332
  });
331
333
  });
334
+
335
+ describe('buildWhatsappOfficialConfigFromIntegration', () => {
336
+ const fullConfig = {
337
+ app_id: '123456',
338
+ app_secret: 'secret',
339
+ business_id: 'BIZ',
340
+ waba_id: 'WABA',
341
+ access_token: 'token',
342
+ graph_api_version: 'v22.0',
343
+ verify_token: 'verify',
344
+ environment: 'test',
345
+ };
346
+
347
+ it('mapeia todos os campos do perfil', () => {
348
+ expect(buildWhatsappOfficialConfigFromIntegration(fullConfig)).toEqual({
349
+ appId: '123456',
350
+ appSecret: 'secret',
351
+ businessId: 'BIZ',
352
+ wabaId: 'WABA',
353
+ accessToken: 'token',
354
+ graphApiVersion: 'v22.0',
355
+ verifyToken: 'verify',
356
+ environment: 'test',
357
+ });
358
+ });
359
+
360
+ it('assume a versão default da Graph API quando ausente ou vazia', () => {
361
+ expect(
362
+ buildWhatsappOfficialConfigFromIntegration({ ...fullConfig, graph_api_version: '' })
363
+ .graphApiVersion,
364
+ ).toBe(DEFAULT_GRAPH_API_VERSION);
365
+ expect(
366
+ buildWhatsappOfficialConfigFromIntegration({}).graphApiVersion,
367
+ ).toBe(DEFAULT_GRAPH_API_VERSION);
368
+ });
369
+
370
+ it('assume production quando o ambiente não é "test"', () => {
371
+ // Default seguro: um ambiente inválido não deve virar sandbox silenciosamente.
372
+ expect(
373
+ buildWhatsappOfficialConfigFromIntegration({ environment: 'qualquer' })
374
+ .environment,
375
+ ).toBe('production');
376
+ expect(
377
+ buildWhatsappOfficialConfigFromIntegration({}).environment,
378
+ ).toBe('production');
379
+ });
380
+
381
+ it('omite business_id quando não informado (é opcional)', () => {
382
+ const result = buildWhatsappOfficialConfigFromIntegration({});
383
+ expect(result.businessId).toBeUndefined();
384
+ });
385
+
386
+ it('não quebra com config nulo e devolve campos vazios', () => {
387
+ expect(buildWhatsappOfficialConfigFromIntegration(null)).toMatchObject({
388
+ appId: '',
389
+ appSecret: '',
390
+ wabaId: '',
391
+ accessToken: '',
392
+ verifyToken: '',
393
+ });
394
+ });
395
+
396
+ it('remove espaços em volta dos segredos (copiar/colar do painel da Meta)', () => {
397
+ const result = buildWhatsappOfficialConfigFromIntegration({
398
+ access_token: ' token ',
399
+ app_secret: ' secret ',
400
+ verify_token: ' verify ',
401
+ });
402
+ expect(result.accessToken).toBe('token');
403
+ expect(result.appSecret).toBe('secret');
404
+ expect(result.verifyToken).toBe('verify');
405
+ });
406
+ });
332
407
  });