@hed-hog/core 0.0.401 → 0.0.404

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 (114) hide show
  1. package/dist/access-log/access-log.interceptor.d.ts +10 -0
  2. package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
  3. package/dist/access-log/access-log.interceptor.js +15 -1
  4. package/dist/access-log/access-log.interceptor.js.map +1 -1
  5. package/dist/authorize/authorize.controller.d.ts +1 -0
  6. package/dist/authorize/authorize.controller.d.ts.map +1 -1
  7. package/dist/authorize/authorize.service.d.ts +1 -0
  8. package/dist/authorize/authorize.service.d.ts.map +1 -1
  9. package/dist/authorize/authorize.service.js +3 -0
  10. package/dist/authorize/authorize.service.js.map +1 -1
  11. package/dist/file/file-upload-ticket.service.d.ts +1 -1
  12. package/dist/file/file.controller.d.ts +3 -3
  13. package/dist/file/file.service.d.ts +121 -12
  14. package/dist/file/file.service.d.ts.map +1 -1
  15. package/dist/file/file.service.js +245 -3
  16. package/dist/file/file.service.js.map +1 -1
  17. package/dist/file/provider/abstract.provider.d.ts +33 -0
  18. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  19. package/dist/file/provider/abstract.provider.js +9 -0
  20. package/dist/file/provider/abstract.provider.js.map +1 -1
  21. package/dist/file/provider/s3-multipart.util.d.ts +17 -0
  22. package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
  23. package/dist/file/provider/s3-multipart.util.js +88 -0
  24. package/dist/file/provider/s3-multipart.util.js.map +1 -0
  25. package/dist/file/provider/s3.provider.d.ts +9 -1
  26. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  27. package/dist/file/provider/s3.provider.js +16 -0
  28. package/dist/file/provider/s3.provider.js.map +1 -1
  29. package/dist/file/provider/spaces.provider.d.ts +10 -1
  30. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  31. package/dist/file/provider/spaces.provider.js +17 -0
  32. package/dist/file/provider/spaces.provider.js.map +1 -1
  33. package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
  34. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  35. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  36. package/dist/integration-profile/integration-profile.secrets.js +7 -2
  37. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  38. package/dist/integration-profile/integration-profile.service.d.ts +1 -0
  39. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  40. package/dist/integration-profile/integration-profile.service.js +3 -3
  41. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  42. package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
  43. package/dist/integration-profile/testers/http-probe.js +5 -3
  44. package/dist/integration-profile/testers/http-probe.js.map +1 -1
  45. package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
  46. package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
  47. package/dist/mail/mail.service.d.ts.map +1 -1
  48. package/dist/mail/mail.service.js +16 -1
  49. package/dist/mail/mail.service.js.map +1 -1
  50. package/dist/mcp/mcp-server.service.d.ts.map +1 -1
  51. package/dist/mcp/mcp-server.service.js +10 -1
  52. package/dist/mcp/mcp-server.service.js.map +1 -1
  53. package/dist/pdf/document/pdf-document.css.d.ts.map +1 -1
  54. package/dist/pdf/document/pdf-document.css.js +88 -0
  55. package/dist/pdf/document/pdf-document.css.js.map +1 -1
  56. package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -1
  57. package/dist/pdf/document/pdf-document.renderer.js +73 -0
  58. package/dist/pdf/document/pdf-document.renderer.js.map +1 -1
  59. package/dist/pdf/document/pdf-document.types.d.ts +56 -0
  60. package/dist/pdf/document/pdf-document.types.d.ts.map +1 -1
  61. package/dist/pdf/pdf.service.d.ts +6 -0
  62. package/dist/pdf/pdf.service.d.ts.map +1 -1
  63. package/dist/pdf/pdf.service.js +3 -0
  64. package/dist/pdf/pdf.service.js.map +1 -1
  65. package/dist/setting/setting.service.d.ts.map +1 -1
  66. package/dist/setting/setting.service.js +3 -0
  67. package/dist/setting/setting.service.js.map +1 -1
  68. package/dist/task/task.service.d.ts +6 -0
  69. package/dist/task/task.service.d.ts.map +1 -1
  70. package/dist/task/task.service.js +44 -13
  71. package/dist/task/task.service.js.map +1 -1
  72. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
  73. package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
  74. package/dist/whatsapp-sender/whatsapp-sender.service.js +83 -21
  75. package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
  76. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
  77. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
  78. package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
  79. package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
  80. package/hedhog/frontend/messages/en.json +46 -20
  81. package/hedhog/frontend/messages/pt.json +46 -20
  82. package/hedhog/table/file.yaml +3 -1
  83. package/package.json +3 -3
  84. package/src/access-log/access-log.interceptor.spec.ts +50 -0
  85. package/src/access-log/access-log.interceptor.ts +18 -0
  86. package/src/authorize/authorize.service.spec.ts +1 -0
  87. package/src/authorize/authorize.service.ts +2 -0
  88. package/src/file/file-direct-upload.spec.ts +344 -0
  89. package/src/file/file.service.ts +363 -2
  90. package/src/file/provider/abstract.provider.ts +57 -0
  91. package/src/file/provider/s3-multipart.spec.ts +123 -0
  92. package/src/file/provider/s3-multipart.util.ts +126 -0
  93. package/src/file/provider/s3.provider.ts +65 -1
  94. package/src/file/provider/spaces.provider.ts +67 -1
  95. package/src/integration-profile/integration-profile.secrets.ts +7 -2
  96. package/src/integration-profile/integration-profile.service.ts +1 -0
  97. package/src/integration-profile/testers/http-probe.ts +5 -3
  98. package/src/integration-profile/testers/integration-tester.types.ts +5 -0
  99. package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
  100. package/src/integration-profile/testers/tester-coverage.spec.ts +2 -0
  101. package/src/mail/mail.service.spec.ts +25 -0
  102. package/src/mail/mail.service.ts +18 -1
  103. package/src/mcp/mcp-server.service.spec.ts +16 -0
  104. package/src/mcp/mcp-server.service.ts +10 -1
  105. package/src/pdf/document/pdf-document.css.ts +88 -0
  106. package/src/pdf/document/pdf-document.renderer.spec.ts +43 -0
  107. package/src/pdf/document/pdf-document.renderer.ts +102 -0
  108. package/src/pdf/document/pdf-document.types.ts +52 -0
  109. package/src/pdf/pdf.service.ts +11 -0
  110. package/src/setting/setting.service.ts +3 -0
  111. package/src/task/task.service.spec.ts +77 -1
  112. package/src/task/task.service.ts +58 -14
  113. package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +191 -1
  114. package/src/whatsapp-sender/whatsapp-sender.service.ts +122 -14
@@ -32,8 +32,17 @@ export type WhatsappSendInput = {
32
32
  phoneNumberId?: string;
33
33
  /** Template aprovado pela Meta — sem ele o envio só chega na janela de 24h. */
34
34
  template?: { name: string; language?: string; params?: string[] };
35
+ /**
36
+ * Providers aceitos para este envio. Vazio aceita qualquer um. Quem manda em
37
+ * lote para alunos restringe ao oficial: disparo em massa pela Evolution
38
+ * (WhatsApp Web) derruba e bane o número.
39
+ */
40
+ providers?: readonly string[];
35
41
  };
36
42
 
43
+ /** Só a Cloud API da Meta — para envio em volume a quem não é da equipe. */
44
+ export const WHATSAPP_OFFICIAL_ONLY: readonly string[] = ['whatsapp-official'];
45
+
37
46
  type ResolvedProfile = {
38
47
  id: number;
39
48
  slug: string;
@@ -46,13 +55,27 @@ type ResolvedProfile = {
46
55
  * resultado diferente. Um HTTP 4xx da Graph API (id inexistente, sem
47
56
  * permissão, token ruim) não muda em cinco minutos; um 5xx ou falha de rede
48
57
  * pode ser uma instabilidade passageira do provedor.
58
+ *
59
+ * `isTransient` repete o `retryable` no formato que o `TransientError.is` da
60
+ * fila reconhece por duck-typing (o core não depende do queue): com ele as
61
+ * tentativas intermediárias vão ao Sentry como warning, e só a última como erro.
62
+ *
63
+ * `reason` marca as falhas que valem para a instância inteira, não para a
64
+ * mensagem: com a sessão fechada ou a credencial recusada, a próxima mensagem
65
+ * do mesmo lote tem o mesmo destino, e quem envia em série pode parar ali.
49
66
  */
67
+ export type WhatsappSendErrorReason = 'session_closed' | 'unauthorized';
68
+
50
69
  export class WhatsappSendError extends BadRequestException {
70
+ readonly isTransient: boolean;
71
+
51
72
  constructor(
52
73
  message: string,
53
74
  public readonly retryable: boolean,
75
+ public readonly reason?: WhatsappSendErrorReason,
54
76
  ) {
55
77
  super(message);
78
+ this.isTransient = retryable;
56
79
  }
57
80
  }
58
81
 
@@ -73,7 +96,7 @@ export class WhatsappSenderService {
73
96
  ) {}
74
97
 
75
98
  async send(input: WhatsappSendInput): Promise<void> {
76
- const profile = await this.resolveProfile(input.profileSlug);
99
+ const profile = await this.resolveProfile(input.profileSlug, input.providers);
77
100
 
78
101
  switch (profile.providerSlug) {
79
102
  case 'evolution-api':
@@ -88,9 +111,12 @@ export class WhatsappSenderService {
88
111
  }
89
112
 
90
113
  /** Diz se há perfil utilizável, para o chamador decidir por um fallback sem tentar o envio. */
91
- async isConfigured(profileSlug?: string): Promise<boolean> {
114
+ async isConfigured(
115
+ profileSlug?: string,
116
+ providers?: readonly string[],
117
+ ): Promise<boolean> {
92
118
  try {
93
- const profile = await this.resolveProfile(profileSlug);
119
+ const profile = await this.resolveProfile(profileSlug, providers);
94
120
 
95
121
  if (profile.providerSlug === 'evolution-api') {
96
122
  const { baseUrl, token, instance } = this.readEvolutionConfig(profile);
@@ -150,8 +176,12 @@ export class WhatsappSenderService {
150
176
  return normalized;
151
177
  }
152
178
 
153
- private async resolveProfile(profileSlug?: string): Promise<ResolvedProfile> {
179
+ private async resolveProfile(
180
+ profileSlug?: string,
181
+ providers?: readonly string[],
182
+ ): Promise<ResolvedProfile> {
154
183
  const slug = String(profileSlug ?? '').trim();
184
+ const allowed = providers?.length ? [...providers] : null;
155
185
 
156
186
  const profile = slug
157
187
  ? await this.prisma.integration_profile.findUnique({
@@ -162,7 +192,11 @@ export class WhatsappSenderService {
162
192
  },
163
193
  })
164
194
  : await this.prisma.integration_profile.findFirst({
165
- where: { is_active: true, integration_type: { slug: 'whatsapp' } },
195
+ where: {
196
+ is_active: true,
197
+ integration_type: { slug: 'whatsapp' },
198
+ ...(allowed ? { integration_provider: { slug: { in: allowed } } } : {}),
199
+ },
166
200
  orderBy: { updated_at: 'desc' },
167
201
  include: {
168
202
  integration_provider: { select: { slug: true } },
@@ -174,7 +208,9 @@ export class WhatsappSenderService {
174
208
  throw new NotFoundException(
175
209
  slug
176
210
  ? `Perfil de integração "${slug}" não encontrado.`
177
- : 'Nenhum perfil de integração de WhatsApp ativo encontrado.',
211
+ : allowed
212
+ ? `Nenhum perfil de WhatsApp ativo do provider ${allowed.join(' ou ')} encontrado.`
213
+ : 'Nenhum perfil de integração de WhatsApp ativo encontrado.',
178
214
  );
179
215
  }
180
216
 
@@ -184,10 +220,21 @@ export class WhatsappSenderService {
184
220
  );
185
221
  }
186
222
 
223
+ const providerSlug = String((profile as any).integration_provider?.slug ?? '');
224
+
225
+ // Perfil escolhido à mão que não é do provider exigido: é configuração, não
226
+ // instabilidade — reenviar daria no mesmo.
227
+ if (allowed && !allowed.includes(providerSlug)) {
228
+ throw new WhatsappSendError(
229
+ `Perfil de integração "${profile.slug}" usa o provider "${providerSlug}", e este envio só aceita ${allowed.join(' ou ')}.`,
230
+ false,
231
+ );
232
+ }
233
+
187
234
  return {
188
235
  id: profile.id,
189
236
  slug: profile.slug,
190
- providerSlug: String((profile as any).integration_provider?.slug ?? ''),
237
+ providerSlug,
191
238
  config: ((profile.config ?? {}) as Record<string, unknown>),
192
239
  };
193
240
  }
@@ -245,6 +292,7 @@ export class WhatsappSenderService {
245
292
  throw new WhatsappSendError(
246
293
  `Credenciais do WhatsApp inválidas (401). Verifique o token do perfil "${profile.slug}".`,
247
294
  false,
295
+ 'unauthorized',
248
296
  );
249
297
  }
250
298
 
@@ -258,6 +306,18 @@ export class WhatsappSenderService {
258
306
  );
259
307
  }
260
308
 
309
+ // Sessão da instância caída (celular deslogado ou Baileys reconectando):
310
+ // a Evolution responde 500 "Connection Closed". Segue retentável porque a
311
+ // reconexão costuma ser sozinha; se persistir, quem ler a falha precisa
312
+ // saber que a ação é reconectar a instância, não mexer no envio.
313
+ if (isEvolutionSessionClosed(detail)) {
314
+ throw new WhatsappSendError(
315
+ `A instância "${instance}" da Evolution está desconectada do WhatsApp (Connection Closed). Reconecte a instância (QR code) do perfil "${profile.slug}".`,
316
+ true,
317
+ 'session_closed',
318
+ );
319
+ }
320
+
261
321
  // Mesmo critério do provider oficial: 4xx é recusa estrutural, 5xx e
262
322
  // 429 podem passar numa próxima tentativa.
263
323
  throw new WhatsappSendError(
@@ -267,6 +327,24 @@ export class WhatsappSenderService {
267
327
  }
268
328
  }
269
329
 
330
+ /**
331
+ * Número padrão do perfil, mantido pela library `whatsapp` (sync com o WABA
332
+ * ou registro na primeira mensagem recebida).
333
+ *
334
+ * A tabela não é do core: num projeto sem a library `whatsapp` o delegate não
335
+ * existe, e o envio segue sem padrão — cai no erro de phone_number_id ausente.
336
+ */
337
+ private async resolveDefaultPhoneNumberId(profileId: number): Promise<string> {
338
+ const delegate = (this.prisma as any).whatsapp_phone_number;
339
+ if (!delegate?.findFirst) return '';
340
+
341
+ const found = await delegate.findFirst({
342
+ where: { integration_profile_id: profileId, is_default: true, status: 'active' },
343
+ select: { phone_number_id: true },
344
+ });
345
+ return String(found?.phone_number_id ?? '').trim();
346
+ }
347
+
270
348
  private async sendWithOfficial(
271
349
  profile: ResolvedProfile,
272
350
  input: WhatsappSendInput,
@@ -292,15 +370,16 @@ export class WhatsappSenderService {
292
370
  );
293
371
  }
294
372
 
295
- // O phone_number_id não vive no perfil: um WABA pode ter vários números,
296
- // então quem envia precisa dizer por qual deles a mensagem sai.
297
- const phoneNumberId = String(
298
- input.phoneNumberId ?? profile.config.phone_number_id ?? '',
299
- ).trim();
373
+ // O phone_number_id não vive no perfil: um WABA pode ter vários números.
374
+ // Quem envia pode dizer por qual deles a mensagem sai; sem isso, vale o
375
+ // número padrão do perfil.
376
+ const phoneNumberId =
377
+ String(input.phoneNumberId ?? profile.config.phone_number_id ?? '').trim() ||
378
+ (await this.resolveDefaultPhoneNumberId(profile.id));
300
379
 
301
380
  if (!phoneNumberId) {
302
381
  throw new BadRequestException(
303
- 'O provider whatsapp-official exige o phone_number_id do número remetente.',
382
+ `Perfil de WhatsApp "${profile.slug}" não tem número padrão: informe o phone_number_id do número remetente ou mande uma mensagem para o número, que ele é registrado.`,
304
383
  );
305
384
  }
306
385
 
@@ -322,7 +401,7 @@ export class WhatsappSenderService {
322
401
  type: 'body',
323
402
  parameters: (input.template.params ?? [input.text]).map((text) => ({
324
403
  type: 'text',
325
- text,
404
+ text: toTemplateParam(text),
326
405
  })),
327
406
  },
328
407
  ],
@@ -389,6 +468,35 @@ function isEvolutionNumberMissing(detail: string): boolean {
389
468
  }
390
469
  }
391
470
 
471
+ /**
472
+ * Reconhece o 500 da Evolution com a sessão do WhatsApp fechada:
473
+ * `{"response":{"message":"Connection Closed"}}` (às vezes com a string num array).
474
+ */
475
+ function isEvolutionSessionClosed(detail: string): boolean {
476
+ try {
477
+ const message = JSON.parse(detail)?.response?.message;
478
+ const texts = Array.isArray(message) ? message : [message];
479
+ return texts.some(
480
+ (item) => typeof item === 'string' && /connection closed/i.test(item),
481
+ );
482
+ } catch {
483
+ return false;
484
+ }
485
+ }
486
+
487
+ /**
488
+ * A Meta recusa parâmetro de template com quebra de linha, tab ou mais de
489
+ * quatro espaços seguidos (erro 132018) — e o texto que os módulos montam para
490
+ * o WhatsApp vem em parágrafos. Vazio também é recusado.
491
+ */
492
+ export function toTemplateParam(text: unknown): string {
493
+ const flat = String(text ?? '')
494
+ .replace(/\s*[\r\n\t]+\s*/g, ' · ')
495
+ .replace(/ {2,}/g, ' ')
496
+ .trim();
497
+ return flat || '-';
498
+ }
499
+
392
500
  /** Evolution espera base64 puro; um data URL vindo do chamador quebraria o envio. */
393
501
  function stripDataUrlPrefix(image: string): string {
394
502
  const match = /^data:[^;]+;base64,(.*)$/i.exec(image);