@hed-hog/core 0.0.370 → 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 (248) 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_provider.yaml +9 -0
  158. package/hedhog/data/integration_type.yaml +6 -0
  159. package/hedhog/data/menu.yaml +6 -0
  160. package/hedhog/data/role.yaml +7 -0
  161. package/hedhog/data/route.yaml +36 -0
  162. package/hedhog/data/setting_group.yaml +119 -13
  163. package/hedhog/frontend/app/access-log/page.tsx.ejs +35 -3
  164. package/hedhog/frontend/app/account/components/active-sessions.tsx.ejs +36 -3
  165. package/hedhog/frontend/app/account/components/mcp-tokens.tsx.ejs +39 -4
  166. package/hedhog/frontend/app/account/components/tokens.tsx.ejs +39 -4
  167. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +35 -2
  168. package/hedhog/frontend/app/configurations/[slug]/components/byte-size-input.tsx.ejs +160 -0
  169. package/hedhog/frontend/app/configurations/[slug]/components/duration-input.tsx.ejs +157 -0
  170. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +244 -63
  171. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +33 -1
  172. package/hedhog/frontend/app/dashboard/management/tabs/component-roles-tab.tsx.ejs +35 -3
  173. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +36 -4
  174. package/hedhog/frontend/app/dashboard/management/tabs/dashboard-roles-tab.tsx.ejs +35 -3
  175. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +36 -4
  176. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +35 -3
  177. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +141 -29
  178. package/hedhog/frontend/app/integrations-webhooks/page.tsx.ejs +118 -11
  179. package/hedhog/frontend/app/mail/log/page.tsx.ejs +37 -4
  180. package/hedhog/frontend/app/menu/page.tsx.ejs +34 -2
  181. package/hedhog/frontend/app/preferences/page.tsx.ejs +77 -0
  182. package/hedhog/frontend/app/roles/menus.tsx.ejs +35 -3
  183. package/hedhog/frontend/app/roles/page.tsx.ejs +34 -2
  184. package/hedhog/frontend/app/roles/routes.tsx.ejs +36 -4
  185. package/hedhog/frontend/app/roles/users.tsx.ejs +36 -4
  186. package/hedhog/frontend/app/routes/page.tsx.ejs +34 -2
  187. package/hedhog/frontend/app/setting-list/page.tsx.ejs +34 -2
  188. package/hedhog/frontend/app/users/page.tsx.ejs +51 -21
  189. package/hedhog/frontend/messages/en.json +69 -4
  190. package/hedhog/frontend/messages/pt.json +69 -4
  191. package/hedhog/table/setting.yaml +2 -0
  192. package/hedhog/table/webhook_call_log.yaml +49 -46
  193. package/package.json +8 -3
  194. package/src/ai/ai.service.ts +9 -6
  195. package/src/app-command/app-command.module.ts +9 -0
  196. package/src/{webhook-integration/webhook-command.registry.ts → app-command/app-command.registry.ts} +9 -9
  197. package/src/auth/auth.controller.ts +20 -4
  198. package/src/auth/auth.service.ts +10 -36
  199. package/src/auth/dto/forget.dto.ts +5 -1
  200. package/src/auth/guards/auth.guard.ts +4 -1
  201. package/src/challenge/challenge.service.ts +30 -19
  202. package/src/file/dto/upload.dto.ts +25 -1
  203. package/src/file/file.controller.ts +31 -18
  204. package/src/file/file.service.ts +341 -16
  205. package/src/file/image-optimization.service.ts +111 -46
  206. package/src/file/provider/s3.provider.ts +16 -7
  207. package/src/file/provider/spaces.provider.ts +15 -5
  208. package/src/index.ts +4 -1
  209. package/src/integration/services/event-subscriber.registry.ts +15 -0
  210. package/src/integration/services/outbox.processor.ts +2 -5
  211. package/src/integration/services/outbox.service.ts +35 -17
  212. package/src/integration-profile/integration-credential-crypto.service.ts +146 -0
  213. package/src/integration-profile/integration-profile.controller.ts +2 -0
  214. package/src/integration-profile/integration-profile.module.ts +5 -2
  215. package/src/integration-profile/integration-profile.secrets.ts +51 -0
  216. package/src/integration-profile/integration-profile.service.ts +335 -54
  217. package/src/integration-profile/integration-profile.utils.ts +56 -0
  218. package/src/language/en.json +5 -1
  219. package/src/language/pt.json +5 -1
  220. package/src/mail/mail.controller.ts +5 -29
  221. package/src/mail/mail.module.ts +5 -5
  222. package/src/mail/mail.service.ts +37 -13
  223. package/src/mcp-chat/mcp-chat.service.ts +5 -5
  224. package/src/oauth/oauth-callback-coordinator.service.spec.ts +159 -0
  225. package/src/oauth/providers/facebook.provider.ts +3 -3
  226. package/src/oauth/providers/github.provider.ts +3 -3
  227. package/src/oauth/providers/google.provider.ts +3 -3
  228. package/src/oauth/providers/microsoft-entra-id.provider.ts +3 -3
  229. package/src/oauth/providers/microsoft.provider.ts +3 -3
  230. package/src/pdf/pdf.service.ts +19 -6
  231. package/src/profile/profile.controller.ts +21 -14
  232. package/src/profile/profile.service.ts +23 -20
  233. package/src/security/security.service.spec.ts +222 -0
  234. package/src/setting/setting.service.ts +15 -2
  235. package/src/token/token.service.ts +2 -1
  236. package/src/user/user.controller.ts +26 -16
  237. package/src/user/user.service.ts +25 -0
  238. package/src/utils/webauthn-origin.ts +38 -0
  239. package/src/webhook-integration/webhook-integration.controller.ts +23 -0
  240. package/src/webhook-integration/webhook-integration.module.ts +3 -3
  241. package/src/webhook-integration/webhook-integration.service.ts +115 -5
  242. package/dist/webhook-integration/webhook-command.registry.d.ts +0 -22
  243. package/dist/webhook-integration/webhook-command.registry.d.ts.map +0 -1
  244. package/dist/webhook-integration/webhook-command.registry.js.map +0 -1
  245. package/src/menu/menu.service.spec.ts +0 -247
  246. package/src/route/route.service.spec.ts +0 -300
  247. package/src/screen/screen.service.spec.ts +0 -298
  248. package/src/user/user.service.spec.ts +0 -294
@@ -7,6 +7,7 @@ import {
7
7
  S3,
8
8
  S3Client,
9
9
  } from '@aws-sdk/client-s3';
10
+ import { Upload } from '@aws-sdk/lib-storage';
10
11
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
11
12
  import { BadRequestException } from '@nestjs/common';
12
13
  import { createReadStream } from 'fs';
@@ -157,18 +158,26 @@ export class S3Provider extends AbstractProvider {
157
158
  filePath: string,
158
159
  options?: { filename?: string; mimetype?: string },
159
160
  ): Promise<any> {
160
- const s3 = await this.getClient();
161
+ const client = await this.getS3Client();
161
162
  const originalName = options?.filename ?? basename(filePath);
162
163
  const storedFilename = this.getFilename(originalName);
163
164
  const key = [destination, storedFilename].join('/');
164
- const command = new PutObjectCommand({
165
- Bucket: this.setting['storage-s3-bucket'],
166
- Key: key,
167
- Body: createReadStream(filePath),
168
- ContentType: options?.mimetype,
165
+
166
+ // Multipart streaming upload: nunca buffeia o arquivo em memória e suporta
167
+ // objetos acima do limite de 5GB do PutObject único (ex.: pacotes SCORM grandes).
168
+ const upload = new Upload({
169
+ client,
170
+ params: {
171
+ Bucket: this.setting['storage-s3-bucket'],
172
+ Key: key,
173
+ Body: createReadStream(filePath),
174
+ ContentType: options?.mimetype,
175
+ },
176
+ queueSize: 4,
177
+ partSize: 16 * 1024 * 1024,
169
178
  });
170
179
 
171
- await s3.send(command);
180
+ await upload.done();
172
181
  return storedFilename;
173
182
  }
174
183
 
@@ -7,6 +7,7 @@ import {
7
7
  S3,
8
8
  S3Client,
9
9
  } from '@aws-sdk/client-s3';
10
+ import { Upload } from '@aws-sdk/lib-storage';
10
11
  import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
11
12
  import { BadRequestException } from '@nestjs/common';
12
13
  import { createReadStream } from 'fs';
@@ -181,19 +182,28 @@ export class SpacesProvider extends AbstractProvider {
181
182
  filePath: string,
182
183
  options?: { filename?: string; mimetype?: string },
183
184
  ): Promise<any> {
184
- const s3 = await this.getClient();
185
+ const client = await this.getS3Client();
185
186
  const originalName = options?.filename ?? basename(filePath);
186
187
  const storedFilename = this.getFilename(originalName);
187
188
  const key = [destination, storedFilename].join('/');
188
- await s3.send(
189
- new PutObjectCommand({
189
+
190
+ // Upload por streaming do disco: o lib-storage envia um PutObject único abaixo
191
+ // do partSize e só ativa multipart acima dele, suportando objetos acima do
192
+ // limite de 5GB do PutObject (ex.: pacotes SCORM grandes) sem buffeiar na RAM.
193
+ const upload = new Upload({
194
+ client,
195
+ params: {
190
196
  Bucket: this.setting['storage-spaces-bucket'],
191
197
  Key: key,
192
198
  Body: createReadStream(filePath),
193
199
  ContentType: options?.mimetype,
194
200
  ACL: 'public-read',
195
- }),
196
- );
201
+ },
202
+ queueSize: 4,
203
+ partSize: 16 * 1024 * 1024,
204
+ });
205
+
206
+ await upload.done();
197
207
  return storedFilename;
198
208
  }
199
209
 
package/src/index.ts CHANGED
@@ -25,6 +25,8 @@ export * from './mail/mail.service';
25
25
  export * from './integration-profile/integration-profile.module';
26
26
  export * from './integration-profile/integration-profile.service';
27
27
  export * from './integration-profile/integration-profile.utils';
28
+ export * from './integration-profile/integration-profile.secrets';
29
+ export * from './integration-profile/integration-credential-crypto.service';
28
30
 
29
31
  export * from './file/file.module';
30
32
  export * from './file/file.service';
@@ -37,9 +39,10 @@ export * from './setting/setting.service';
37
39
  export * from './ai/ai.module';
38
40
  export * from './ai/ai.service';
39
41
 
42
+ export * from './app-command/app-command.module';
43
+ export * from './app-command/app-command.registry';
40
44
  export * from './webhook-integration/event-webhook.service';
41
45
  export * from './webhook-integration/integration-event-catalog.service';
42
- export * from './webhook-integration/webhook-command.registry';
43
46
  export * from './webhook-integration/webhook-integration.module';
44
47
  export * from './webhook-integration/webhook-integration.service';
45
48
 
@@ -80,6 +80,21 @@ export class EventSubscriberRegistry {
80
80
  return count;
81
81
  }
82
82
 
83
+ /**
84
+ * Remove all handlers whose consumerName starts with the given prefix.
85
+ * Used to clean up subscriptions for a specific agent before re-registering.
86
+ */
87
+ removeByConsumerPrefix(prefix: string): void {
88
+ for (const [eventName, handlers] of this.subscribers.entries()) {
89
+ const filtered = handlers.filter((h) => !h.consumerName.startsWith(prefix));
90
+ if (filtered.length === 0) {
91
+ this.subscribers.delete(eventName);
92
+ } else {
93
+ this.subscribers.set(eventName, filtered);
94
+ }
95
+ }
96
+ }
97
+
83
98
  /**
84
99
  * Clear all handlers (useful for testing)
85
100
  */
@@ -27,8 +27,8 @@ export class OutboxProcessor {
27
27
  const batchSize = options?.batchSizeOverride ?? settings.batchSize;
28
28
  const leaseMs = settings.processingLeaseMs;
29
29
 
30
- // Find pending events
31
- const events = await this.outboxService.findPending(batchSize, leaseMs);
30
+ // Atomically claim pending events — prevents duplicate processing across replicas
31
+ const events = await this.outboxService.findPendingAndLock(batchSize, leaseMs);
32
32
 
33
33
  if (events.length === 0) {
34
34
  return 0;
@@ -42,9 +42,6 @@ export class OutboxProcessor {
42
42
 
43
43
  for (const event of events) {
44
44
  try {
45
- // Mark as processing with lease
46
- await this.outboxService.markProcessing(event.id, leaseMs);
47
-
48
45
  // Get handlers for this event
49
46
  const handlers = this.registry.getHandlers(event.eventName);
50
47
 
@@ -102,30 +102,48 @@ export class OutboxService {
102
102
  }
103
103
 
104
104
  /**
105
- * Find pending events available for processing, ordered by creation time
105
+ * Atomically claim a batch of pending events using SELECT FOR UPDATE SKIP LOCKED.
106
+ * Prevents duplicate processing across multiple API replicas: each replica gets
107
+ * its own non-overlapping set of rows with no blocking between them.
106
108
  */
107
- async findPending(
109
+ async findPendingAndLock(
108
110
  limit: number,
109
111
  leaseMs: number,
110
112
  ): Promise<OutboxEvent[]> {
111
113
  const now = new Date();
112
114
  const leaseThreshold = new Date(now.getTime() - leaseMs);
113
-
114
- const records = await this.prisma.outbox_event.findMany({
115
- where: {
116
- OR: [
117
- { status: OutboxEventStatus.PENDING },
118
- {
119
- status: OutboxEventStatus.PROCESSING,
120
- available_at: { lte: leaseThreshold },
121
- },
122
- ],
123
- },
124
- orderBy: { created_at: 'asc' },
125
- take: limit,
115
+ const leaseUntil = new Date(now.getTime() + leaseMs);
116
+
117
+ return this.prisma.$transaction(async (tx) => {
118
+ const rows = await tx.$queryRaw<OutboxEventRecord[]>`
119
+ SELECT * FROM outbox_event
120
+ WHERE status = 'pending'::"outbox_event_status_e5d55ba10d_enum"
121
+ OR (status = 'processing'::"outbox_event_status_e5d55ba10d_enum"
122
+ AND available_at <= ${leaseThreshold})
123
+ ORDER BY created_at ASC
124
+ LIMIT ${limit}
125
+ FOR UPDATE SKIP LOCKED
126
+ `;
127
+
128
+ if (rows.length === 0) return [];
129
+
130
+ const ids = rows.map((r) => r.id);
131
+ await tx.$executeRaw`
132
+ UPDATE outbox_event
133
+ SET status = 'processing'::"outbox_event_status_e5d55ba10d_enum",
134
+ available_at = ${leaseUntil},
135
+ updated_at = now()
136
+ WHERE id = ANY(${ids}::int[])
137
+ `;
138
+
139
+ return rows.map((r) =>
140
+ this.toDomainEvent({
141
+ ...r,
142
+ status: OutboxEventStatus.PROCESSING,
143
+ available_at: leaseUntil,
144
+ }),
145
+ );
126
146
  });
127
-
128
- return records.map((record) => this.toDomainEvent(record as OutboxEventRecord));
129
147
  }
130
148
 
131
149
  /**
@@ -0,0 +1,146 @@
1
+ import { registerIntegrationConfigTransformer } from '@hed-hog/api-prisma';
2
+ import {
3
+ Inject,
4
+ Injectable,
5
+ Logger,
6
+ OnModuleInit,
7
+ forwardRef,
8
+ } from '@nestjs/common';
9
+ import { SecurityService } from '../security/security.service';
10
+ import { getSensitiveConfigKeys } from './integration-profile.secrets';
11
+
12
+ /** Prefixo de envelope dos segredos cifrados em repouso. */
13
+ export const ENC_ENVELOPE_PREFIX = 'enc.v1.';
14
+
15
+ /** Placeholder devolvido à UI no lugar de um segredo armazenado. */
16
+ export const SECRET_MASK = '********';
17
+
18
+ type Config = Record<string, unknown> | null | undefined;
19
+
20
+ /**
21
+ * Cifra/decifra os segredos dos Perfis de Integração em repouso.
22
+ *
23
+ * - Escrita: `encryptConfig` cifra (envelope `enc.v1.`) as chaves sensíveis do provider.
24
+ * - Leitura: um middleware Prisma `$use` (registrado em onModuleInit) decifra
25
+ * `integration_profile.config` de forma transparente em toda leitura/resultado, então os
26
+ * ~25 consumidores que leem o perfil direto via Prisma não mudam.
27
+ * - UI: `maskConfig` esconde segredos; `mergeKeepingStoredSecrets` implementa
28
+ * "deixe em branco para manter".
29
+ *
30
+ * Valores legados sem o prefixo passam intactos (sem janela de quebra antes do backfill).
31
+ */
32
+ @Injectable()
33
+ export class IntegrationCredentialCryptoService implements OnModuleInit {
34
+ private readonly logger = new Logger(IntegrationCredentialCryptoService.name);
35
+
36
+ constructor(
37
+ @Inject(forwardRef(() => SecurityService))
38
+ private readonly securityService: SecurityService,
39
+ ) {}
40
+
41
+ onModuleInit() {
42
+ // Registra a decifra transparente de integration_profile.config (query extension
43
+ // no PrismaService). A partir daqui, toda leitura do perfil devolve config decifrado.
44
+ registerIntegrationConfigTransformer((config) => this.decryptConfig(config));
45
+ this.logger.log(
46
+ 'Integration credential decryption registered (integration_profile.config).',
47
+ );
48
+ }
49
+
50
+ private isEnveloped(value: unknown): value is string {
51
+ return typeof value === 'string' && value.startsWith(ENC_ENVELOPE_PREFIX);
52
+ }
53
+
54
+ /** Cifra um valor único com envelope (idempotente). */
55
+ encryptValue(value: string): string {
56
+ if (this.isEnveloped(value)) return value;
57
+ return ENC_ENVELOPE_PREFIX + this.securityService.encrypt(value);
58
+ }
59
+
60
+ /** Decifra um valor com envelope; valores sem envelope passam intactos. */
61
+ decryptValue(value: unknown): unknown {
62
+ if (!this.isEnveloped(value)) return value;
63
+ try {
64
+ return this.securityService.decrypt(value.slice(ENC_ENVELOPE_PREFIX.length));
65
+ } catch (err) {
66
+ // Não derruba a query: loga e devolve como veio (ex.: ENCRYPTION_SECRET trocado).
67
+ this.logger.error(
68
+ `Failed to decrypt integration secret: ${err instanceof Error ? err.message : String(err)}`,
69
+ );
70
+ return value;
71
+ }
72
+ }
73
+
74
+ /** Cifra as chaves sensíveis do provider no config (idempotente, ignora vazios). */
75
+ encryptConfig(providerSlug: string, config: Config): Config {
76
+ if (!config) return config;
77
+ const keys = getSensitiveConfigKeys(providerSlug);
78
+ if (keys.length === 0) return config;
79
+ const out: Record<string, unknown> = { ...config };
80
+ for (const key of keys) {
81
+ const value = out[key];
82
+ if (typeof value === 'string' && value.length > 0) {
83
+ out[key] = this.encryptValue(value);
84
+ }
85
+ }
86
+ return out;
87
+ }
88
+
89
+ /** Decifra qualquer valor com envelope no config (não precisa do provider slug). */
90
+ decryptConfig(config: Config): Config {
91
+ if (!config) return config;
92
+ let changed = false;
93
+ const out: Record<string, unknown> = { ...config };
94
+ for (const [key, value] of Object.entries(out)) {
95
+ if (this.isEnveloped(value)) {
96
+ out[key] = this.decryptValue(value);
97
+ changed = true;
98
+ }
99
+ }
100
+ return changed ? out : config;
101
+ }
102
+
103
+ /** Mascara os segredos do provider para resposta à UI (não envia o valor real). */
104
+ maskConfig(
105
+ providerSlug: string,
106
+ config: Config,
107
+ placeholder = SECRET_MASK,
108
+ ): Config {
109
+ if (!config) return config;
110
+ const keys = getSensitiveConfigKeys(providerSlug);
111
+ if (keys.length === 0) return config;
112
+ const out: Record<string, unknown> = { ...config };
113
+ for (const key of keys) {
114
+ const value = out[key];
115
+ if (typeof value === 'string' && value.length > 0) {
116
+ out[key] = placeholder;
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+
122
+ /**
123
+ * "Deixe em branco para manter": para cada chave sensível, se o valor recebido está
124
+ * vazio ou é o placeholder de máscara, herda o valor já armazenado (decifrado).
125
+ */
126
+ mergeKeepingStoredSecrets(
127
+ providerSlug: string,
128
+ incoming: Config,
129
+ storedDecrypted: Config,
130
+ ): Config {
131
+ const merged: Record<string, unknown> = { ...(incoming ?? {}) };
132
+ const stored = storedDecrypted ?? {};
133
+ for (const key of getSensitiveConfigKeys(providerSlug)) {
134
+ const value = merged[key];
135
+ const isBlank =
136
+ value === undefined ||
137
+ value === null ||
138
+ value === '' ||
139
+ value === SECRET_MASK;
140
+ if (isBlank && typeof stored[key] === 'string') {
141
+ merged[key] = stored[key];
142
+ }
143
+ }
144
+ return merged;
145
+ }
146
+ }
@@ -55,12 +55,14 @@ export class IntegrationProfileController {
55
55
  @Query('typeId') typeId?: string,
56
56
  @Query('typeSlug') typeSlug?: string,
57
57
  @Query('providerSlug') providerSlug?: string,
58
+ @Query('slug') slug?: string,
58
59
  ) {
59
60
  return this.integrationProfileService.list(
60
61
  paginationParams,
61
62
  typeId ? parseInt(typeId, 10) : undefined,
62
63
  typeSlug,
63
64
  providerSlug,
65
+ slug,
64
66
  );
65
67
  }
66
68
 
@@ -1,6 +1,8 @@
1
1
  import { PaginationModule } from '@hed-hog/api-pagination';
2
2
  import { PrismaModule } from '@hed-hog/api-prisma';
3
3
  import { forwardRef, Module } from '@nestjs/common';
4
+ import { SecurityModule } from '../security/security.module';
5
+ import { IntegrationCredentialCryptoService } from './integration-credential-crypto.service';
4
6
  import { IntegrationProfileController } from './integration-profile.controller';
5
7
  import { IntegrationProfileService } from './integration-profile.service';
6
8
 
@@ -8,9 +10,10 @@ import { IntegrationProfileService } from './integration-profile.service';
8
10
  imports: [
9
11
  forwardRef(() => PrismaModule),
10
12
  forwardRef(() => PaginationModule),
13
+ forwardRef(() => SecurityModule),
11
14
  ],
12
15
  controllers: [IntegrationProfileController],
13
- providers: [IntegrationProfileService],
14
- exports: [IntegrationProfileService],
16
+ providers: [IntegrationProfileService, IntegrationCredentialCryptoService],
17
+ exports: [IntegrationProfileService, IntegrationCredentialCryptoService],
15
18
  })
16
19
  export class IntegrationProfileModule {}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Mapa AUTORITATIVO das chaves sensíveis (segredos) por provider de integração.
3
+ *
4
+ * SEGURANÇA-CRÍTICO: este mapa é a fonte única de verdade do que é cifrado em repouso
5
+ * (integration-credential-crypto.service) e mascarado na exportação/UI. Qualquer chave
6
+ * esquecida aqui NÃO será cifrada. Deve ser mantido em sincronia com os campos
7
+ * `type: 'password'` do form do admin (PROVIDER_FIELDS em
8
+ * apps/admin/src/app/(app)/(libraries)/core/integration/profiles/page.tsx).
9
+ *
10
+ * Não usar heurística por nome de chave: campos públicos como `site_key`, `public_key` e
11
+ * `access_key_id` contêm "key" mas NÃO são segredos.
12
+ */
13
+ export const INTEGRATION_SECRET_KEYS: Record<string, string[]> = {
14
+ // Email
15
+ smtp: ['password'],
16
+ gmail: ['client_secret', 'refresh_token'],
17
+ ses: ['secret_access_key'],
18
+ // Mensageria
19
+ 'evolution-api': ['token'],
20
+ 'whatsapp-official': ['access_token'],
21
+ // Storage
22
+ s3: ['secret_access_key'],
23
+ gcs: ['key_file_json'],
24
+ 'azure-blob': ['key'],
25
+ 's3-compatible': ['secret_access_key'],
26
+ // IA
27
+ openai: ['api_key'],
28
+ gemini: ['api_key'],
29
+ claude: ['api_key'],
30
+ // OAuth
31
+ 'google-oauth': ['client_secret'],
32
+ 'github-oauth': ['client_secret'],
33
+ 'microsoft-oauth': ['client_secret'],
34
+ 'facebook-oauth': ['client_secret'],
35
+ 'microsoft-entra-id-oauth': ['client_secret'],
36
+ // Pagamento
37
+ stripe: ['secret_key', 'webhook_secret'],
38
+ mercado_pago: ['access_token', 'webhook_secret'],
39
+ // Captcha
40
+ recaptcha: ['secret_key'],
41
+ 'cloudflare-turnstile': ['secret_key'],
42
+ altcha: ['hmac_key'],
43
+ // Infraestrutura (autoscaling)
44
+ digitalocean: ['api_token'],
45
+ kubernetes: ['token', 'ca_cert'],
46
+ };
47
+
48
+ /** Chaves sensíveis de um provider (vazio se desconhecido). */
49
+ export function getSensitiveConfigKeys(providerSlug: string): string[] {
50
+ return INTEGRATION_SECRET_KEYS[providerSlug.toLowerCase()] ?? [];
51
+ }