@hed-hog/core 0.0.368 → 0.0.371

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 (271) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +2 -0
  3. package/dist/ai/ai.service.d.ts.map +1 -1
  4. package/dist/ai/ai.service.js +10 -7
  5. package/dist/ai/ai.service.js.map +1 -1
  6. package/dist/app-command/app-command.module.d.ts +3 -0
  7. package/dist/app-command/app-command.module.d.ts.map +1 -0
  8. package/dist/app-command/app-command.module.js +22 -0
  9. package/dist/app-command/app-command.module.js.map +1 -0
  10. package/dist/app-command/app-command.registry.d.ts +22 -0
  11. package/dist/app-command/app-command.registry.d.ts.map +1 -0
  12. package/dist/{webhook-integration/webhook-command.registry.js → app-command/app-command.registry.js} +6 -6
  13. package/dist/app-command/app-command.registry.js.map +1 -0
  14. package/dist/auth/auth.controller.d.ts +2 -2
  15. package/dist/auth/auth.controller.d.ts.map +1 -1
  16. package/dist/auth/auth.controller.js +20 -7
  17. package/dist/auth/auth.controller.js.map +1 -1
  18. package/dist/auth/auth.service.d.ts +3 -3
  19. package/dist/auth/auth.service.d.ts.map +1 -1
  20. package/dist/auth/auth.service.js +10 -36
  21. package/dist/auth/auth.service.js.map +1 -1
  22. package/dist/auth/dto/forget.dto.d.ts +1 -0
  23. package/dist/auth/dto/forget.dto.d.ts.map +1 -1
  24. package/dist/auth/dto/forget.dto.js +5 -0
  25. package/dist/auth/dto/forget.dto.js.map +1 -1
  26. package/dist/auth/guards/auth.guard.d.ts.map +1 -1
  27. package/dist/auth/guards/auth.guard.js +4 -1
  28. package/dist/auth/guards/auth.guard.js.map +1 -1
  29. package/dist/challenge/challenge.service.d.ts +8 -7
  30. package/dist/challenge/challenge.service.d.ts.map +1 -1
  31. package/dist/challenge/challenge.service.js +27 -19
  32. package/dist/challenge/challenge.service.js.map +1 -1
  33. package/dist/file/dto/upload.dto.d.ts +3 -0
  34. package/dist/file/dto/upload.dto.d.ts.map +1 -1
  35. package/dist/file/dto/upload.dto.js +22 -0
  36. package/dist/file/dto/upload.dto.js.map +1 -1
  37. package/dist/file/file.controller.d.ts +2 -1
  38. package/dist/file/file.controller.d.ts.map +1 -1
  39. package/dist/file/file.controller.js +29 -16
  40. package/dist/file/file.controller.js.map +1 -1
  41. package/dist/file/file.service.d.ts +51 -1
  42. package/dist/file/file.service.d.ts.map +1 -1
  43. package/dist/file/file.service.js +250 -18
  44. package/dist/file/file.service.js.map +1 -1
  45. package/dist/file/image-optimization.service.d.ts +17 -4
  46. package/dist/file/image-optimization.service.d.ts.map +1 -1
  47. package/dist/file/image-optimization.service.js +101 -46
  48. package/dist/file/image-optimization.service.js.map +1 -1
  49. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  50. package/dist/file/provider/s3.provider.js +15 -7
  51. package/dist/file/provider/s3.provider.js.map +1 -1
  52. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  53. package/dist/file/provider/spaces.provider.js +18 -8
  54. package/dist/file/provider/spaces.provider.js.map +1 -1
  55. package/dist/index.d.ts +4 -1
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +4 -1
  58. package/dist/index.js.map +1 -1
  59. package/dist/integration/services/event-subscriber.registry.d.ts +5 -0
  60. package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -1
  61. package/dist/integration/services/event-subscriber.registry.js +15 -0
  62. package/dist/integration/services/event-subscriber.registry.js.map +1 -1
  63. package/dist/integration/services/outbox.processor.d.ts.map +1 -1
  64. package/dist/integration/services/outbox.processor.js +2 -4
  65. package/dist/integration/services/outbox.processor.js.map +1 -1
  66. package/dist/integration/services/outbox.service.d.ts +4 -2
  67. package/dist/integration/services/outbox.service.d.ts.map +1 -1
  68. package/dist/integration/services/outbox.service.js +26 -15
  69. package/dist/integration/services/outbox.service.js.map +1 -1
  70. package/dist/integration-profile/integration-credential-crypto.service.d.ts +43 -0
  71. package/dist/integration-profile/integration-credential-crypto.service.d.ts.map +1 -0
  72. package/dist/integration-profile/integration-credential-crypto.service.js +142 -0
  73. package/dist/integration-profile/integration-credential-crypto.service.js.map +1 -0
  74. package/dist/integration-profile/integration-profile.controller.d.ts +6 -7
  75. package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
  76. package/dist/integration-profile/integration-profile.controller.js +4 -3
  77. package/dist/integration-profile/integration-profile.controller.js.map +1 -1
  78. package/dist/integration-profile/integration-profile.module.d.ts.map +1 -1
  79. package/dist/integration-profile/integration-profile.module.js +5 -2
  80. package/dist/integration-profile/integration-profile.module.js.map +1 -1
  81. package/dist/integration-profile/integration-profile.secrets.d.ts +16 -0
  82. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -0
  83. package/dist/integration-profile/integration-profile.secrets.js +56 -0
  84. package/dist/integration-profile/integration-profile.secrets.js.map +1 -0
  85. package/dist/integration-profile/integration-profile.service.d.ts +125 -8
  86. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  87. package/dist/integration-profile/integration-profile.service.js +257 -68
  88. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  89. package/dist/integration-profile/integration-profile.utils.d.ts +17 -0
  90. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  91. package/dist/integration-profile/integration-profile.utils.js +31 -0
  92. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  93. package/dist/language/en.json +178 -0
  94. package/dist/language/pt.json +178 -0
  95. package/dist/mail/mail.controller.d.ts.map +1 -1
  96. package/dist/mail/mail.controller.js +5 -28
  97. package/dist/mail/mail.controller.js.map +1 -1
  98. package/dist/mail/mail.module.js +18 -18
  99. package/dist/mail/mail.module.js.map +1 -1
  100. package/dist/mail/mail.service.d.ts +13 -2
  101. package/dist/mail/mail.service.d.ts.map +1 -1
  102. package/dist/mail/mail.service.js +25 -15
  103. package/dist/mail/mail.service.js.map +1 -1
  104. package/dist/mcp-chat/mcp-chat.service.js +6 -5
  105. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  106. package/dist/oauth/providers/facebook.provider.js +5 -5
  107. package/dist/oauth/providers/facebook.provider.js.map +1 -1
  108. package/dist/oauth/providers/github.provider.js +5 -5
  109. package/dist/oauth/providers/github.provider.js.map +1 -1
  110. package/dist/oauth/providers/google.provider.js +5 -5
  111. package/dist/oauth/providers/google.provider.js.map +1 -1
  112. package/dist/oauth/providers/microsoft-entra-id.provider.js +5 -5
  113. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  114. package/dist/oauth/providers/microsoft.provider.js +5 -5
  115. package/dist/oauth/providers/microsoft.provider.js.map +1 -1
  116. package/dist/pdf/pdf.service.d.ts +4 -0
  117. package/dist/pdf/pdf.service.d.ts.map +1 -1
  118. package/dist/pdf/pdf.service.js +16 -9
  119. package/dist/pdf/pdf.service.js.map +1 -1
  120. package/dist/profile/profile.controller.d.ts +7 -7
  121. package/dist/profile/profile.controller.d.ts.map +1 -1
  122. package/dist/profile/profile.controller.js +28 -21
  123. package/dist/profile/profile.controller.js.map +1 -1
  124. package/dist/profile/profile.service.d.ts +7 -7
  125. package/dist/profile/profile.service.d.ts.map +1 -1
  126. package/dist/profile/profile.service.js +21 -25
  127. package/dist/profile/profile.service.js.map +1 -1
  128. package/dist/setting/setting.service.d.ts +1 -0
  129. package/dist/setting/setting.service.d.ts.map +1 -1
  130. package/dist/setting/setting.service.js +15 -2
  131. package/dist/setting/setting.service.js.map +1 -1
  132. package/dist/token/token.service.d.ts.map +1 -1
  133. package/dist/token/token.service.js +2 -1
  134. package/dist/token/token.service.js.map +1 -1
  135. package/dist/user/user.controller.d.ts +9 -0
  136. package/dist/user/user.controller.d.ts.map +1 -1
  137. package/dist/user/user.controller.js +13 -0
  138. package/dist/user/user.controller.js.map +1 -1
  139. package/dist/user/user.service.d.ts +7 -0
  140. package/dist/user/user.service.d.ts.map +1 -1
  141. package/dist/user/user.service.js +19 -0
  142. package/dist/user/user.service.js.map +1 -1
  143. package/dist/utils/webauthn-origin.d.ts +11 -0
  144. package/dist/utils/webauthn-origin.d.ts.map +1 -0
  145. package/dist/utils/webauthn-origin.js +37 -0
  146. package/dist/utils/webauthn-origin.js.map +1 -0
  147. package/dist/webhook-integration/webhook-integration.controller.d.ts +13 -0
  148. package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
  149. package/dist/webhook-integration/webhook-integration.controller.js +29 -0
  150. package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
  151. package/dist/webhook-integration/webhook-integration.module.js +3 -3
  152. package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
  153. package/dist/webhook-integration/webhook-integration.service.d.ts +18 -2
  154. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  155. package/dist/webhook-integration/webhook-integration.service.js +110 -6
  156. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  157. package/hedhog/data/integration_event_catalog.yaml +118 -118
  158. package/hedhog/data/integration_provider.yaml +9 -0
  159. package/hedhog/data/integration_type.yaml +6 -0
  160. package/hedhog/data/menu.yaml +6 -0
  161. package/hedhog/data/role.yaml +7 -0
  162. package/hedhog/data/route.yaml +192 -207
  163. package/hedhog/data/setting_group.yaml +119 -13
  164. package/hedhog/frontend/app/access-log/page.tsx.ejs +35 -3
  165. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +36 -3
  166. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +39 -4
  167. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +39 -4
  168. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +35 -2
  169. package/hedhog/frontend/app/configurations/[slug]/components/byte-size-input.tsx.ejs +160 -0
  170. package/hedhog/frontend/app/configurations/[slug]/components/duration-input.tsx.ejs +157 -0
  171. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +244 -63
  172. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +33 -1
  173. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +35 -3
  174. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +36 -4
  175. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +35 -3
  176. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +36 -4
  177. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +35 -3
  178. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +141 -29
  179. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +118 -11
  180. package/hedhog/frontend/app/mail/log/page.tsx.ejs +37 -4
  181. package/hedhog/frontend/app/menu/page.tsx.ejs +34 -2
  182. package/hedhog/frontend/app/preferences/page.tsx.ejs +77 -0
  183. package/hedhog/frontend/app/roles/menus.tsx.ejs +35 -3
  184. package/hedhog/frontend/app/roles/page.tsx.ejs +34 -2
  185. package/hedhog/frontend/app/roles/routes.tsx.ejs +36 -4
  186. package/hedhog/frontend/app/roles/users.tsx.ejs +36 -4
  187. package/hedhog/frontend/app/routes/page.tsx.ejs +34 -2
  188. package/hedhog/frontend/app/setting-list/page.tsx.ejs +34 -2
  189. package/hedhog/frontend/app/users/page.tsx.ejs +51 -21
  190. package/hedhog/frontend/messages/en.json +69 -4
  191. package/hedhog/frontend/messages/pt.json +69 -4
  192. package/hedhog/table/event_webhook.yaml +55 -55
  193. package/hedhog/table/event_webhook_delivery_log.yaml +66 -66
  194. package/hedhog/table/event_webhook_event.yaml +26 -26
  195. package/hedhog/table/integration_event_catalog.yaml +41 -41
  196. package/hedhog/table/mcp_conversation.yaml +12 -12
  197. package/hedhog/table/mcp_conversation_message.yaml +23 -23
  198. package/hedhog/table/mcp_tool_call_log.yaml +22 -22
  199. package/hedhog/table/route.yaml +33 -33
  200. package/hedhog/table/setting.yaml +2 -0
  201. package/hedhog/table/webhook_call_log.yaml +3 -0
  202. package/hedhog/table/webhook_integration.yaml +43 -43
  203. package/package.json +9 -4
  204. package/src/ai/ai.service.ts +9 -6
  205. package/src/app-command/app-command.module.ts +9 -0
  206. package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts} +9 -9
  207. package/src/auth/auth.controller.ts +20 -4
  208. package/src/auth/auth.service.ts +10 -36
  209. package/src/auth/dto/forget.dto.ts +5 -1
  210. package/src/auth/guards/auth.guard.ts +4 -1
  211. package/src/challenge/challenge.service.ts +30 -19
  212. package/src/file/dto/upload.dto.ts +25 -1
  213. package/src/file/file.controller.ts +31 -18
  214. package/src/file/file.service.ts +341 -16
  215. package/src/file/image-optimization.service.ts +111 -46
  216. package/src/file/provider/s3.provider.ts +16 -7
  217. package/src/file/provider/spaces.provider.ts +15 -5
  218. package/src/index.ts +4 -1
  219. package/src/integration/services/event-subscriber.registry.ts +15 -0
  220. package/src/integration/services/outbox.processor.ts +2 -5
  221. package/src/integration/services/outbox.service.ts +35 -17
  222. package/src/integration/types/event.types.ts +4 -4
  223. package/src/integration-profile/integration-credential-crypto.service.ts +146 -0
  224. package/src/integration-profile/integration-profile.controller.ts +2 -0
  225. package/src/integration-profile/integration-profile.module.ts +5 -2
  226. package/src/integration-profile/integration-profile.secrets.ts +51 -0
  227. package/src/integration-profile/integration-profile.service.ts +335 -54
  228. package/src/integration-profile/integration-profile.utils.ts +56 -0
  229. package/src/language/en.json +5 -1
  230. package/src/language/pt.json +5 -1
  231. package/src/mail/mail.controller.ts +5 -29
  232. package/src/mail/mail.module.ts +5 -5
  233. package/src/mail/mail.service.ts +37 -13
  234. package/src/mcp/decorators/mcp-tool.decorator.ts +7 -7
  235. package/src/mcp/guards/mcp-permission.guard.ts +51 -51
  236. package/src/mcp/interfaces/mcp-tool-definition.interface.ts +6 -6
  237. package/src/mcp/mcp-server.service.ts +87 -87
  238. package/src/mcp/types/mcp-context.type.ts +6 -6
  239. package/src/mcp-chat/dto/create-conversation.dto.ts +7 -7
  240. package/src/mcp-chat/dto/send-message.dto.ts +7 -7
  241. package/src/mcp-chat/mcp-chat.service.ts +5 -5
  242. package/src/notification/events/notification-stale-progress-check.event.ts +5 -5
  243. package/src/oauth/oauth-callback-coordinator.service.spec.ts +159 -0
  244. package/src/oauth/providers/facebook.provider.ts +3 -3
  245. package/src/oauth/providers/github.provider.ts +3 -3
  246. package/src/oauth/providers/google.provider.ts +3 -3
  247. package/src/oauth/providers/microsoft-entra-id.provider.ts +3 -3
  248. package/src/oauth/providers/microsoft.provider.ts +3 -3
  249. package/src/pdf/pdf.service.ts +19 -6
  250. package/src/profile/profile.controller.ts +21 -14
  251. package/src/profile/profile.service.ts +23 -20
  252. package/src/route/dto/create.dto.ts +32 -32
  253. package/src/route/dto/update.dto.ts +31 -31
  254. package/src/security/security.service.spec.ts +222 -0
  255. package/src/setting/setting.service.ts +15 -2
  256. package/src/token/token.service.ts +2 -1
  257. package/src/user/user.controller.ts +26 -16
  258. package/src/user/user.service.ts +25 -0
  259. package/src/utils/webauthn-origin.ts +38 -0
  260. package/src/webhook-integration/integration-event-catalog.controller.ts +50 -50
  261. package/src/webhook-integration/integration-event-catalog.service.ts +117 -117
  262. package/src/webhook-integration/webhook-integration.controller.ts +23 -0
  263. package/src/webhook-integration/webhook-integration.module.ts +3 -3
  264. package/src/webhook-integration/webhook-integration.service.ts +115 -5
  265. package/dist/webhook-integration/webhook-command.registry.d.ts +0 -22
  266. package/dist/webhook-integration/webhook-command.registry.d.ts.map +0 -1
  267. package/dist/webhook-integration/webhook-command.registry.js.map +0 -1
  268. package/src/menu/menu.service.spec.ts +0 -247
  269. package/src/route/route.service.spec.ts +0 -300
  270. package/src/screen/screen.service.spec.ts +0 -298
  271. package/src/user/user.service.spec.ts +0 -294
@@ -16,6 +16,8 @@ import {
16
16
  ImportProfileItemDTO,
17
17
  } from './dto/import-confirm.dto';
18
18
  import { UpdateDTO } from './dto/update.dto';
19
+ import { IntegrationCredentialCryptoService } from './integration-credential-crypto.service';
20
+ import { getSensitiveConfigKeys } from './integration-profile.secrets';
19
21
 
20
22
  type IntegrationProfileExportItem = {
21
23
  slug: string;
@@ -58,6 +60,8 @@ export class IntegrationProfileService {
58
60
  private readonly prismaService: PrismaService,
59
61
  @Inject(forwardRef(() => PaginationService))
60
62
  private readonly paginationService: PaginationService,
63
+ @Inject(forwardRef(() => IntegrationCredentialCryptoService))
64
+ private readonly credentialCrypto: IntegrationCredentialCryptoService,
61
65
  ) {}
62
66
 
63
67
  async listTypes() {
@@ -101,11 +105,13 @@ export class IntegrationProfileService {
101
105
  typeId?: number,
102
106
  typeSlug?: string,
103
107
  providerSlug?: string,
108
+ slug?: string,
104
109
  ) {
105
110
  const fields = ['slug', 'name'];
106
111
  const OR = this.prismaService.createInsensitiveSearch(fields, paginationParams);
107
112
  const normalizedTypeSlug = String(typeSlug ?? '').trim();
108
113
  const normalizedProviderSlug = String(providerSlug ?? '').trim();
114
+ const normalizedSlug = String(slug ?? '').trim();
109
115
 
110
116
  const result = await this.paginationService.paginate(
111
117
  this.prismaService.integration_profile,
@@ -120,6 +126,7 @@ export class IntegrationProfileService {
120
126
  ...(normalizedProviderSlug
121
127
  ? { integration_provider: { slug: normalizedProviderSlug } }
122
128
  : {}),
129
+ ...(normalizedSlug ? { slug: normalizedSlug } : {}),
123
130
  },
124
131
  include: {
125
132
  integration_type: {
@@ -166,6 +173,11 @@ export class IntegrationProfileService {
166
173
 
167
174
  result.data = items.map((p: any) => ({
168
175
  ...p,
176
+ // Não devolve segredos na listagem (middleware já decifrou; mascaramos).
177
+ config: this.credentialCrypto.maskConfig(
178
+ p.integration_provider?.slug ?? '',
179
+ p.config as Record<string, unknown>,
180
+ ),
169
181
  uses_count: countMap.get(p.id) ?? 0,
170
182
  }));
171
183
  }
@@ -173,6 +185,98 @@ export class IntegrationProfileService {
173
185
  return result;
174
186
  }
175
187
 
188
+ /**
189
+ * Resolve o config do perfil de integração ATIVO para um par tipo/provider — uso
190
+ * server-side por serviços que precisam das credenciais (ex.: InfraScaler do
191
+ * autoscaling). Opcionalmente fixa um slug específico (vindo de uma setting). É o ponto
192
+ * único de acesso às credenciais armazenadas no Perfil de Integração.
193
+ */
194
+ async getActiveProfileConfig(
195
+ typeSlug: string,
196
+ providerSlug: string,
197
+ options?: { profileSlug?: string; profileId?: number },
198
+ ): Promise<{ id: number; slug: string; config: Record<string, unknown> }> {
199
+ // O middleware do PrismaService decifra config nesta leitura (uso interno).
200
+ const profile = await this.prismaService.integration_profile.findFirst({
201
+ where: {
202
+ is_active: true,
203
+ integration_type: { slug: typeSlug },
204
+ integration_provider: { slug: providerSlug },
205
+ ...(options?.profileId ? { id: options.profileId } : {}),
206
+ ...(options?.profileSlug ? { slug: options.profileSlug } : {}),
207
+ },
208
+ orderBy: [{ updated_at: 'desc' }, { id: 'desc' }],
209
+ select: { id: true, slug: true, config: true },
210
+ });
211
+
212
+ if (!profile) {
213
+ const ref = options?.profileId
214
+ ? ` / id ${options.profileId}`
215
+ : options?.profileSlug
216
+ ? ` / slug "${options.profileSlug}"`
217
+ : '';
218
+ throw new NotFoundException(
219
+ `No active integration profile found for type "${typeSlug}" / provider "${providerSlug}"${ref}.`,
220
+ );
221
+ }
222
+
223
+ return {
224
+ id: profile.id,
225
+ slug: profile.slug,
226
+ config: (profile.config as Record<string, unknown>) ?? {},
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Resolve um perfil ATIVO do tipo `infrastructure` (DigitalOcean, Kubernetes, …) por
232
+ * SLUG (vindo de uma setting) ou por ID (seleção direta na UI) — ou o mais recente se
233
+ * nada for dado. Retorna o slug do provider (para dispatch) e o config já decifrado
234
+ * pelo middleware. Uso interno do autoscaling.
235
+ */
236
+ async getActiveInfrastructureProfile(
237
+ options?: { profileSlug?: string; profileId?: number },
238
+ ): Promise<{
239
+ id: number;
240
+ slug: string;
241
+ providerSlug: string;
242
+ config: Record<string, unknown>;
243
+ }> {
244
+ const normalizedSlug = String(options?.profileSlug ?? '').trim();
245
+ const profile = await this.prismaService.integration_profile.findFirst({
246
+ where: {
247
+ is_active: true,
248
+ integration_type: { slug: 'infrastructure' },
249
+ ...(options?.profileId ? { id: options.profileId } : {}),
250
+ ...(normalizedSlug ? { slug: normalizedSlug } : {}),
251
+ },
252
+ orderBy: [{ updated_at: 'desc' }, { id: 'desc' }],
253
+ select: {
254
+ id: true,
255
+ slug: true,
256
+ config: true,
257
+ integration_provider: { select: { slug: true } },
258
+ },
259
+ });
260
+
261
+ if (!profile) {
262
+ const ref = options?.profileId
263
+ ? ` for id ${options.profileId}`
264
+ : normalizedSlug
265
+ ? ` for slug "${normalizedSlug}"`
266
+ : '';
267
+ throw new NotFoundException(
268
+ `No active infrastructure integration profile found${ref}.`,
269
+ );
270
+ }
271
+
272
+ return {
273
+ id: profile.id,
274
+ slug: profile.slug,
275
+ providerSlug: profile.integration_provider?.slug ?? '',
276
+ config: (profile.config as Record<string, unknown>) ?? {},
277
+ };
278
+ }
279
+
176
280
  async get(id: number) {
177
281
  const profile = await this.prismaService.integration_profile.findUnique({
178
282
  where: { id },
@@ -203,25 +307,71 @@ export class IntegrationProfileService {
203
307
  );
204
308
  }
205
309
 
206
- return profile;
310
+ // Não devolve segredos ao cliente (middleware já decifrou; mascaramos para a UI).
311
+ return {
312
+ ...profile,
313
+ config: this.credentialCrypto.maskConfig(
314
+ profile.integration_provider?.slug ?? '',
315
+ profile.config as Record<string, unknown>,
316
+ ),
317
+ };
318
+ }
319
+
320
+ /**
321
+ * Igual a `get(id)`, porém resolvendo o perfil pelo SLUG (valor estável guardado em
322
+ * settings). Reusa `get()` para manter o mesmo include e o mascaramento de segredos.
323
+ */
324
+ async getBySlug(slug: string) {
325
+ const normalized = String(slug ?? '').trim();
326
+ if (!normalized) {
327
+ throw new NotFoundException('Integration profile slug is empty.');
328
+ }
329
+
330
+ const found = await this.prismaService.integration_profile.findUnique({
331
+ where: { slug: normalized },
332
+ select: { id: true },
333
+ });
334
+
335
+ if (!found) {
336
+ throw new NotFoundException(
337
+ `Integration profile with slug "${normalized}" not found.`,
338
+ );
339
+ }
340
+
341
+ return this.get(found.id);
207
342
  }
208
343
 
209
344
  async create(data: CreateDTO) {
210
345
  await this.ensureSlugIsAvailable(data.slug);
211
346
  await this.validateTypeAndProvider(data.type_id, data.provider_id);
212
347
 
348
+ const providerSlug = await this.getProviderSlug(data.provider_id);
349
+ const config = this.credentialCrypto.encryptConfig(
350
+ providerSlug,
351
+ (data.config as Record<string, unknown>) ?? null,
352
+ );
353
+
213
354
  return this.prismaService.integration_profile.create({
214
355
  data: {
215
356
  slug: data.slug.trim(),
216
357
  name: data.name.trim(),
217
358
  type_id: data.type_id,
218
359
  provider_id: data.provider_id,
219
- config: (data.config as object) ?? null,
360
+ config: (config as object) ?? null,
220
361
  is_active: data.is_active ?? true,
221
362
  },
222
363
  });
223
364
  }
224
365
 
366
+ /** Resolve o slug do provider a partir do id (para saber as chaves sensíveis). */
367
+ private async getProviderSlug(providerId: number): Promise<string> {
368
+ const provider = await this.prismaService.integration_provider.findUnique({
369
+ where: { id: providerId },
370
+ select: { slug: true },
371
+ });
372
+ return provider?.slug ?? '';
373
+ }
374
+
225
375
  async update({ id, data }: { id: number; data: UpdateDTO }) {
226
376
  const existing = await this.prismaService.integration_profile.findUnique({
227
377
  where: { id },
@@ -241,6 +391,20 @@ export class IntegrationProfileService {
241
391
  const provider_id = data.provider_id ?? existing.provider_id;
242
392
  await this.validateTypeAndProvider(type_id, provider_id);
243
393
 
394
+ const providerSlug = await this.getProviderSlug(provider_id);
395
+ // `existing.config` já vem DECIFRADO pelo middleware. Em update parcial, mantemos os
396
+ // segredos enviados em branco ("deixe em branco para manter") e recifra-mos tudo.
397
+ const storedDecrypted = (existing.config as Record<string, unknown>) ?? null;
398
+ const finalPlain =
399
+ data.config !== undefined
400
+ ? this.credentialCrypto.mergeKeepingStoredSecrets(
401
+ providerSlug,
402
+ (data.config as Record<string, unknown>) ?? null,
403
+ storedDecrypted,
404
+ )
405
+ : storedDecrypted;
406
+ const config = this.credentialCrypto.encryptConfig(providerSlug, finalPlain);
407
+
244
408
  return this.prismaService.integration_profile.update({
245
409
  where: { id },
246
410
  data: {
@@ -248,10 +412,7 @@ export class IntegrationProfileService {
248
412
  name: data.name?.trim() ?? existing.name,
249
413
  type_id,
250
414
  provider_id,
251
- config:
252
- data.config !== undefined
253
- ? ((data.config as object) ?? null)
254
- : existing.config,
415
+ config: (config as object) ?? null,
255
416
  is_active: data.is_active ?? existing.is_active,
256
417
  },
257
418
  });
@@ -296,10 +457,25 @@ export class IntegrationProfileService {
296
457
  );
297
458
  }
298
459
 
299
- buildMailConfigFromIntegration(provider.slug, data.config);
460
+ // "Deixe em branco para manter": ao testar na edição, herda os segredos já
461
+ // armazenados (decifrados pelo middleware) para os campos vazios/mascarados.
462
+ let effectiveConfig = (data.config as Record<string, unknown>) ?? null;
463
+ const stored = await this.prismaService.integration_profile.findUnique({
464
+ where: { slug: data.slug.trim() },
465
+ select: { config: true },
466
+ });
467
+ if (stored?.config) {
468
+ effectiveConfig = this.credentialCrypto.mergeKeepingStoredSecrets(
469
+ provider.slug,
470
+ effectiveConfig,
471
+ stored.config as Record<string, unknown>,
472
+ ) as Record<string, unknown>;
473
+ }
474
+
475
+ buildMailConfigFromIntegration(provider.slug, effectiveConfig);
300
476
 
301
477
  const destination = String(
302
- (data.config as Record<string, unknown> | null | undefined)?.from_email ??
478
+ (effectiveConfig as Record<string, unknown> | null | undefined)?.from_email ??
303
479
  '',
304
480
  ).trim();
305
481
 
@@ -320,9 +496,36 @@ export class IntegrationProfileService {
320
496
  ids?: number[];
321
497
  includeSecrets?: boolean;
322
498
  }): Promise<Buffer> {
499
+ const items = await this.exportProfileItemsByIds(ids, includeSecrets);
500
+
501
+ const envelope: IntegrationProfileImportEnvelope = {
502
+ type: 'integration-profile',
503
+ version: 1,
504
+ generated_at: new Date().toISOString(),
505
+ include_secrets: includeSecrets,
506
+ // O envelope de integration-profile não carrega o id de origem.
507
+ data: items.map(({ id: _id, ...rest }) => rest),
508
+ };
509
+
510
+ const compressed = pako.gzip(JSON.stringify(envelope));
511
+ return Buffer.from(compressed);
512
+ }
513
+
514
+ /**
515
+ * Returns export-ready profile items (slug/name/type_slug/provider_slug/config/
516
+ * is_active) plus the source-environment `id`. Used by callers that must reference
517
+ * profiles by id — e.g. the agent workflow export, which remaps node
518
+ * `integrationProfileId` through id → slug → (destination) id. `config` is decrypted
519
+ * defensively and sanitized per `includeSecrets`.
520
+ */
521
+ async exportProfileItemsByIds(
522
+ ids: number[] | undefined,
523
+ includeSecrets: boolean,
524
+ ): Promise<(IntegrationProfileExportItem & { id: number })[]> {
323
525
  const rows = await this.prismaService.integration_profile.findMany({
324
526
  where: ids?.length ? { id: { in: ids } } : undefined,
325
527
  select: {
528
+ id: true,
326
529
  slug: true,
327
530
  name: true,
328
531
  config: true,
@@ -337,29 +540,72 @@ export class IntegrationProfileService {
337
540
  orderBy: [{ slug: 'asc' }],
338
541
  });
339
542
 
340
- const payload: IntegrationProfileExportItem[] = rows.map((row) => ({
543
+ return rows.map((row) => ({
544
+ id: row.id,
341
545
  slug: row.slug,
342
546
  name: row.name,
343
547
  type_slug: row.integration_type.slug,
344
548
  provider_slug: row.integration_provider.slug,
345
549
  config: this.sanitizeConfigForExport(
346
550
  row.integration_provider.slug,
347
- row.config as Record<string, unknown> | null,
551
+ // Decrypt defensivo: garante que o arquivo nunca leve um envelope preso à chave
552
+ // da origem (o middleware já decifra, isto é redundância de segurança).
553
+ this.credentialCrypto.decryptConfig(
554
+ row.config as Record<string, unknown> | null,
555
+ ) as Record<string, unknown> | null,
348
556
  includeSecrets,
349
557
  ),
350
558
  is_active: row.is_active,
351
559
  }));
560
+ }
352
561
 
353
- const envelope: IntegrationProfileImportEnvelope = {
354
- type: 'integration-profile',
355
- version: 1,
356
- generated_at: new Date().toISOString(),
357
- include_secrets: includeSecrets,
358
- data: payload,
359
- };
562
+ /**
563
+ * Validates that each item's type_slug/provider_slug exist and that the provider
564
+ * belongs to the type. Returns a map of item slug → list of errors (empty = valid).
565
+ * Reused by external importers (e.g. the agent workflow import) to validate the
566
+ * credential profiles bundled inside a workflow file.
567
+ */
568
+ async validateImportItems(
569
+ items: { slug: string; type_slug: string; provider_slug: string }[],
570
+ ): Promise<Map<string, string[]>> {
571
+ const typeMap = new Map(
572
+ (
573
+ await this.prismaService.integration_type.findMany({
574
+ select: { id: true, slug: true },
575
+ })
576
+ ).map((row) => [row.slug.toLowerCase(), row]),
577
+ );
360
578
 
361
- const compressed = pako.gzip(JSON.stringify(envelope));
362
- return Buffer.from(compressed);
579
+ const providerMap = new Map(
580
+ (
581
+ await this.prismaService.integration_provider.findMany({
582
+ select: { id: true, slug: true, type_id: true },
583
+ })
584
+ ).map((row) => [row.slug.toLowerCase(), row]),
585
+ );
586
+
587
+ const out = new Map<string, string[]>();
588
+ for (const item of items) {
589
+ const errors: string[] = [];
590
+ const typeSlug = String(item.type_slug ?? '').trim().toLowerCase();
591
+ const providerSlug = String(item.provider_slug ?? '').trim().toLowerCase();
592
+
593
+ const type = typeMap.get(typeSlug);
594
+ const provider = providerMap.get(providerSlug);
595
+ if (!typeSlug) errors.push('Missing type_slug.');
596
+ else if (!type) errors.push(`Unknown type_slug "${typeSlug}".`);
597
+ if (!providerSlug) errors.push('Missing provider_slug.');
598
+ else if (!provider) errors.push(`Unknown provider_slug "${providerSlug}".`);
599
+ if (type && provider && provider.type_id !== type.id) {
600
+ errors.push(
601
+ `Provider "${providerSlug}" does not belong to type "${typeSlug}".`,
602
+ );
603
+ }
604
+
605
+ out.set(item.slug, errors);
606
+ }
607
+
608
+ return out;
363
609
  }
364
610
 
365
611
  async validateImportFile(file: MulterFile) {
@@ -509,6 +755,59 @@ export class IntegrationProfileService {
509
755
  throw new BadRequestException('No profiles provided for import.');
510
756
  }
511
757
 
758
+ const { counts } = await this.ensureProfilesBySlug(
759
+ profiles,
760
+ conflictResolutions,
761
+ );
762
+
763
+ return {
764
+ success: true,
765
+ total_received: profiles.length,
766
+ created: counts.created,
767
+ replaced: counts.replaced,
768
+ renamed: counts.renamed,
769
+ ignored: counts.ignored,
770
+ imported: counts.created + counts.replaced + counts.renamed,
771
+ };
772
+ }
773
+
774
+ /**
775
+ * Creates/updates integration profiles from import items, applying per-slug
776
+ * conflict resolutions, and returns a map from each item's (original) slug to the
777
+ * resulting profile id + applied action — plus aggregate counts. Re-encrypts config
778
+ * with the destination system key. Used by `confirmImport` (counts) and by the agent
779
+ * workflow import (which needs the resulting ids to remap node integrationProfileId).
780
+ *
781
+ * For `ignore` on a conflicting slug, the existing profile is left untouched but its
782
+ * id is still returned so callers can relink references to the local profile.
783
+ */
784
+ async ensureProfilesBySlug(
785
+ profiles: ImportProfileItemDTO[],
786
+ conflictResolutions?: ConflictResolutionDTO[],
787
+ ): Promise<{
788
+ results: Map<
789
+ string,
790
+ {
791
+ id: number;
792
+ slug: string;
793
+ action: 'created' | 'replaced' | 'renamed' | 'ignored';
794
+ }
795
+ >;
796
+ counts: { created: number; replaced: number; renamed: number; ignored: number };
797
+ }> {
798
+ const results = new Map<
799
+ string,
800
+ {
801
+ id: number;
802
+ slug: string;
803
+ action: 'created' | 'replaced' | 'renamed' | 'ignored';
804
+ }
805
+ >();
806
+
807
+ if (!profiles?.length) {
808
+ return { results, counts: { created: 0, replaced: 0, renamed: 0, ignored: 0 } };
809
+ }
810
+
512
811
  const resolutionMap = new Map<string, ConflictResolutionDTO['action']>(
513
812
  (conflictResolutions ?? []).map((resolution) => [
514
813
  resolution.slug,
@@ -596,18 +895,27 @@ export class IntegrationProfileService {
596
895
  name,
597
896
  type_id: type.id,
598
897
  provider_id: provider.id,
599
- config: (item.config as object | null) ?? null,
898
+ // Import traz texto puro (do .hedhog); cifra com a chave do sistema de DESTINO.
899
+ config: (this.credentialCrypto.encryptConfig(
900
+ providerSlug,
901
+ (item.config as Record<string, unknown>) ?? null,
902
+ ) as object | null) ?? null,
600
903
  is_active: item.is_active ?? true,
601
904
  };
602
905
 
603
906
  if (!existing) {
604
- await this.prismaService.integration_profile.create({ data });
907
+ const createdRow = await this.prismaService.integration_profile.create({
908
+ data,
909
+ select: { id: true },
910
+ });
911
+ results.set(slug, { id: createdRow.id, slug, action: 'created' });
605
912
  created += 1;
606
913
  continue;
607
914
  }
608
915
 
609
916
  const action = resolutionMap.get(slug) ?? 'ignore';
610
917
  if (action === 'ignore') {
918
+ results.set(slug, { id: existing.id, slug, action: 'ignored' });
611
919
  ignored += 1;
612
920
  continue;
613
921
  }
@@ -623,29 +931,24 @@ export class IntegrationProfileService {
623
931
  is_active: data.is_active,
624
932
  },
625
933
  });
934
+ results.set(slug, { id: existing.id, slug, action: 'replaced' });
626
935
  replaced += 1;
627
936
  continue;
628
937
  }
629
938
 
630
939
  const newSlug = await this.generateUniqueSlug(slug);
631
- await this.prismaService.integration_profile.create({
940
+ const renamedRow = await this.prismaService.integration_profile.create({
632
941
  data: {
633
942
  ...data,
634
943
  slug: newSlug,
635
944
  },
945
+ select: { id: true },
636
946
  });
947
+ results.set(slug, { id: renamedRow.id, slug: newSlug, action: 'renamed' });
637
948
  renamed += 1;
638
949
  }
639
950
 
640
- return {
641
- success: true,
642
- total_received: profiles.length,
643
- created,
644
- replaced,
645
- renamed,
646
- ignored,
647
- imported: created + replaced + renamed,
648
- };
951
+ return { results, counts: { created, replaced, renamed, ignored } };
649
952
  }
650
953
 
651
954
  private async ensureSlugIsAvailable(slug: string) {
@@ -731,7 +1034,7 @@ export class IntegrationProfileService {
731
1034
  ): Record<string, unknown> | null {
732
1035
  if (!config || includeSecrets) return config;
733
1036
 
734
- const hiddenKeys = this.getSensitiveConfigKeys(providerSlug);
1037
+ const hiddenKeys = getSensitiveConfigKeys(providerSlug);
735
1038
  if (hiddenKeys.length === 0) return config;
736
1039
 
737
1040
  const sanitized = { ...config };
@@ -742,28 +1045,6 @@ export class IntegrationProfileService {
742
1045
  return sanitized;
743
1046
  }
744
1047
 
745
- private getSensitiveConfigKeys(providerSlug: string): string[] {
746
- const map: Record<string, string[]> = {
747
- smtp: ['password'],
748
- gmail: ['client_secret', 'refresh_token'],
749
- ses: ['secret_access_key'],
750
- 'evolution-api': ['token'],
751
- 'whatsapp-official': ['access_token'],
752
- s3: ['secret_access_key'],
753
- gcs: ['key_file_json'],
754
- 'azure-blob': ['key'],
755
- 's3-compatible': ['secret_access_key'],
756
- openai: ['api_key'],
757
- gemini: ['api_key'],
758
- claude: ['api_key'],
759
- 'google-oauth': ['client_secret'],
760
- 'github-oauth': ['client_secret'],
761
- 'microsoft-oauth': ['client_secret'],
762
- };
763
-
764
- return map[providerSlug.toLowerCase()] ?? [];
765
- }
766
-
767
1048
  private async generateUniqueSlug(baseSlug: string): Promise<string> {
768
1049
  let candidate = `${baseSlug}-copy`;
769
1050
  let suffix = 1;
@@ -150,6 +150,62 @@ export function buildOAuthConfigFromIntegration(
150
150
  };
151
151
  }
152
152
 
153
+ // ─── Infrastructure utilities ──────────────────────────────────────────────────
154
+
155
+ export type DigitalOceanConfig = {
156
+ apiToken: string;
157
+ clusterId: string;
158
+ region: string;
159
+ videoNodePoolId: string;
160
+ videoNodePoolName: string;
161
+ };
162
+
163
+ /** Maps an integration_profile config JSON to the DigitalOcean API credentials/targets. */
164
+ export function buildDigitalOceanConfigFromIntegration(
165
+ providerSlug: string,
166
+ config: unknown,
167
+ ): DigitalOceanConfig {
168
+ if (providerSlug.toLowerCase() !== 'digitalocean') {
169
+ throw new BadRequestException(
170
+ `Unsupported infrastructure provider: ${providerSlug}`,
171
+ );
172
+ }
173
+ const cfg = (config as Cfg) ?? {};
174
+ return {
175
+ apiToken: String(cfg.api_token ?? ''),
176
+ clusterId: String(cfg.cluster_id ?? ''),
177
+ region: String(cfg.region ?? ''),
178
+ videoNodePoolId: String(cfg.video_node_pool_id ?? ''),
179
+ videoNodePoolName: String(cfg.video_node_pool_name ?? ''),
180
+ };
181
+ }
182
+
183
+ export type KubernetesConfig = {
184
+ apiServer: string;
185
+ token: string;
186
+ caCert: string;
187
+ namespace: string;
188
+ };
189
+
190
+ /** Maps an integration_profile config JSON to read-only Kubernetes API access (opcional). */
191
+ export function buildKubernetesConfigFromIntegration(
192
+ providerSlug: string,
193
+ config: unknown,
194
+ ): KubernetesConfig {
195
+ if (providerSlug.toLowerCase() !== 'kubernetes') {
196
+ throw new BadRequestException(
197
+ `Unsupported infrastructure provider: ${providerSlug}`,
198
+ );
199
+ }
200
+ const cfg = (config as Cfg) ?? {};
201
+ return {
202
+ apiServer: String(cfg.api_server ?? ''),
203
+ token: String(cfg.token ?? ''),
204
+ caCert: String(cfg.ca_cert ?? ''),
205
+ namespace: String(cfg.namespace ?? 'hcode'),
206
+ };
207
+ }
208
+
153
209
  // ─── AI utilities ─────────────────────────────────────────────────────────────
154
210
 
155
211
  /** Maps an integration_profile config JSON to the API key and optional org needed by AI providers. */
@@ -25,7 +25,11 @@
25
25
  "emailExists": "Email already in use."
26
26
  },
27
27
  "forgot": {
28
- "rateLimit": "Too many requests. Please try again later."
28
+ "rateLimit": "Too many requests. Please try again later.",
29
+ "userNotFound": "User not found."
30
+ },
31
+ "challenge": {
32
+ "invalidOrExpired": "The provided code is invalid or has expired."
29
33
  },
30
34
  "reset": {
31
35
  "password": {
@@ -25,7 +25,11 @@
25
25
  "emailExists": "E-mail já está em uso."
26
26
  },
27
27
  "forgot": {
28
- "rateLimit": "Muitas solicitações. Por favor, tente novamente mais tarde."
28
+ "rateLimit": "Muitas solicitações. Por favor, tente novamente mais tarde.",
29
+ "userNotFound": "Usuário não encontrado."
30
+ },
31
+ "challenge": {
32
+ "invalidOrExpired": "O código informado é inválido ou expirou."
29
33
  },
30
34
  "reset": {
31
35
  "password": {