@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.
- package/dist/auth/auth.controller.d.ts +2 -1
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +2 -1
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +3 -1
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +7 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +73 -2
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.d.ts +18 -0
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +16 -0
- package/dist/integration-profile/integration-profile.utils.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +3 -2
- package/dist/profile/profile.controller.d.ts.map +1 -1
- package/dist/profile/profile.controller.js.map +1 -1
- package/dist/profile/profile.service.d.ts +4 -3
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.d.ts +5 -8
- package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.js +31 -8
- package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.module.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.module.js +5 -0
- package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts +33 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +109 -4
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/dist/webhook-integration/webhook-protocol.registry.d.ts +63 -0
- package/dist/webhook-integration/webhook-protocol.registry.d.ts.map +1 -0
- package/dist/webhook-integration/webhook-protocol.registry.js +51 -0
- package/dist/webhook-integration/webhook-protocol.registry.js.map +1 -0
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +16 -8
- package/hedhog/frontend/messages/en.json +22 -0
- package/hedhog/frontend/messages/pt.json +22 -0
- package/hedhog/table/webhook_integration.yaml +64 -43
- package/package.json +5 -5
- package/src/auth/auth.controller.ts +2 -1
- package/src/auth/auth.service.ts +2 -1
- package/src/index.ts +1 -0
- package/src/integration-profile/integration-profile.secrets.ts +3 -1
- package/src/integration-profile/integration-profile.service.ts +110 -3
- package/src/integration-profile/integration-profile.utils.spec.ts +75 -0
- package/src/integration-profile/integration-profile.utils.ts +39 -0
- package/src/profile/profile.controller.ts +6 -2
- package/src/profile/profile.service.ts +5 -3
- package/src/webhook-integration/webhook-integration.controller.ts +40 -2
- package/src/webhook-integration/webhook-integration.module.ts +5 -0
- package/src/webhook-integration/webhook-integration.service.ts +133 -0
- package/src/webhook-integration/webhook-protocol.registry.spec.ts +70 -0
- 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
|
|
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
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
)
|
|
705
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
package/src/auth/auth.service.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
});
|