@hed-hog/core 0.0.364 → 0.0.365

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 (185) hide show
  1. package/dist/ai/ai.controller.d.ts +4 -4
  2. package/dist/ai/ai.module.d.ts.map +1 -1
  3. package/dist/ai/ai.module.js +2 -0
  4. package/dist/ai/ai.module.js.map +1 -1
  5. package/dist/ai/ai.service.d.ts +7 -5
  6. package/dist/ai/ai.service.d.ts.map +1 -1
  7. package/dist/ai/ai.service.js +63 -31
  8. package/dist/ai/ai.service.js.map +1 -1
  9. package/dist/ai-instruction/ai-execution.service.d.ts +45 -0
  10. package/dist/ai-instruction/ai-execution.service.d.ts.map +1 -0
  11. package/dist/ai-instruction/ai-execution.service.js +203 -0
  12. package/dist/ai-instruction/ai-execution.service.js.map +1 -0
  13. package/dist/ai-instruction/ai-instruction.controller.d.ts +25 -0
  14. package/dist/ai-instruction/ai-instruction.controller.d.ts.map +1 -0
  15. package/dist/ai-instruction/ai-instruction.controller.js +117 -0
  16. package/dist/ai-instruction/ai-instruction.controller.js.map +1 -0
  17. package/dist/ai-instruction/ai-instruction.module.d.ts +3 -0
  18. package/dist/ai-instruction/ai-instruction.module.d.ts.map +1 -0
  19. package/dist/ai-instruction/ai-instruction.module.js +27 -0
  20. package/dist/ai-instruction/ai-instruction.module.js.map +1 -0
  21. package/dist/ai-instruction/ai-instruction.service.d.ts +38 -0
  22. package/dist/ai-instruction/ai-instruction.service.d.ts.map +1 -0
  23. package/dist/ai-instruction/ai-instruction.service.js +289 -0
  24. package/dist/ai-instruction/ai-instruction.service.js.map +1 -0
  25. package/dist/ai-instruction/ai-prompt-builder.service.d.ts +53 -0
  26. package/dist/ai-instruction/ai-prompt-builder.service.d.ts.map +1 -0
  27. package/dist/ai-instruction/ai-prompt-builder.service.js +132 -0
  28. package/dist/ai-instruction/ai-prompt-builder.service.js.map +1 -0
  29. package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts +12 -0
  30. package/dist/ai-instruction/dto/create-ai-instruction.dto.d.ts.map +1 -0
  31. package/dist/ai-instruction/dto/create-ai-instruction.dto.js +57 -0
  32. package/dist/ai-instruction/dto/create-ai-instruction.dto.js.map +1 -0
  33. package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts +10 -0
  34. package/dist/ai-instruction/dto/update-ai-instruction.dto.d.ts.map +1 -0
  35. package/dist/ai-instruction/dto/update-ai-instruction.dto.js +56 -0
  36. package/dist/ai-instruction/dto/update-ai-instruction.dto.js.map +1 -0
  37. package/dist/ai-instruction/types/ai-instruction.types.d.ts +78 -0
  38. package/dist/ai-instruction/types/ai-instruction.types.d.ts.map +1 -0
  39. package/dist/ai-instruction/types/ai-instruction.types.js +3 -0
  40. package/dist/ai-instruction/types/ai-instruction.types.js.map +1 -0
  41. package/dist/auth/auth.controller.d.ts +13 -5
  42. package/dist/auth/auth.controller.d.ts.map +1 -1
  43. package/dist/auth/auth.controller.js +39 -20
  44. package/dist/auth/auth.controller.js.map +1 -1
  45. package/dist/auth/auth.service.d.ts +18 -7
  46. package/dist/auth/auth.service.d.ts.map +1 -1
  47. package/dist/auth/auth.service.js +20 -16
  48. package/dist/auth/auth.service.js.map +1 -1
  49. package/dist/core.module.d.ts.map +1 -1
  50. package/dist/core.module.js +3 -0
  51. package/dist/core.module.js.map +1 -1
  52. package/dist/file/file.service.d.ts +37 -0
  53. package/dist/file/file.service.d.ts.map +1 -1
  54. package/dist/file/file.service.js +94 -4
  55. package/dist/file/file.service.js.map +1 -1
  56. package/dist/file/provider/abstract.provider.d.ts +6 -2
  57. package/dist/file/provider/abstract.provider.d.ts.map +1 -1
  58. package/dist/file/provider/abstract.provider.js.map +1 -1
  59. package/dist/file/provider/azure.provider.d.ts +4 -1
  60. package/dist/file/provider/azure.provider.d.ts.map +1 -1
  61. package/dist/file/provider/azure.provider.js +7 -2
  62. package/dist/file/provider/azure.provider.js.map +1 -1
  63. package/dist/file/provider/gcs.provider.d.ts +4 -1
  64. package/dist/file/provider/gcs.provider.d.ts.map +1 -1
  65. package/dist/file/provider/gcs.provider.js +7 -2
  66. package/dist/file/provider/gcs.provider.js.map +1 -1
  67. package/dist/file/provider/local.provider.d.ts +2 -2
  68. package/dist/file/provider/local.provider.d.ts.map +1 -1
  69. package/dist/file/provider/local.provider.js +7 -2
  70. package/dist/file/provider/local.provider.js.map +1 -1
  71. package/dist/file/provider/s3.provider.d.ts +4 -1
  72. package/dist/file/provider/s3.provider.d.ts.map +1 -1
  73. package/dist/file/provider/s3.provider.js +9 -6
  74. package/dist/file/provider/s3.provider.js.map +1 -1
  75. package/dist/file/provider/spaces.provider.d.ts +4 -1
  76. package/dist/file/provider/spaces.provider.d.ts.map +1 -1
  77. package/dist/file/provider/spaces.provider.js +9 -5
  78. package/dist/file/provider/spaces.provider.js.map +1 -1
  79. package/dist/integration-profile/integration-profile.controller.d.ts +3 -3
  80. package/dist/integration-profile/integration-profile.service.d.ts +3 -3
  81. package/dist/mcp-chat/mcp-chat.controller.d.ts +2 -2
  82. package/dist/mcp-chat/mcp-chat.module.d.ts.map +1 -1
  83. package/dist/mcp-chat/mcp-chat.module.js +2 -0
  84. package/dist/mcp-chat/mcp-chat.module.js.map +1 -1
  85. package/dist/mcp-chat/mcp-chat.service.d.ts +8 -3
  86. package/dist/mcp-chat/mcp-chat.service.d.ts.map +1 -1
  87. package/dist/mcp-chat/mcp-chat.service.js +100 -20
  88. package/dist/mcp-chat/mcp-chat.service.js.map +1 -1
  89. package/dist/notification/notification.service.d.ts +1 -0
  90. package/dist/notification/notification.service.d.ts.map +1 -1
  91. package/dist/notification/notification.service.js +29 -1
  92. package/dist/notification/notification.service.js.map +1 -1
  93. package/dist/oauth/oauth.service.d.ts +1 -0
  94. package/dist/oauth/oauth.service.d.ts.map +1 -1
  95. package/dist/oauth/oauth.service.js +55 -1
  96. package/dist/oauth/oauth.service.js.map +1 -1
  97. package/dist/profile/events/profile-avatar-updated.event.d.ts +7 -0
  98. package/dist/profile/events/profile-avatar-updated.event.d.ts.map +1 -0
  99. package/dist/profile/events/profile-avatar-updated.event.js +12 -0
  100. package/dist/profile/events/profile-avatar-updated.event.js.map +1 -0
  101. package/dist/profile/events/profile-email-changed.event.d.ts +7 -0
  102. package/dist/profile/events/profile-email-changed.event.d.ts.map +1 -0
  103. package/dist/profile/events/profile-email-changed.event.js +12 -0
  104. package/dist/profile/events/profile-email-changed.event.js.map +1 -0
  105. package/dist/profile/events/profile-updated.event.d.ts +7 -0
  106. package/dist/profile/events/profile-updated.event.d.ts.map +1 -0
  107. package/dist/profile/events/profile-updated.event.js +12 -0
  108. package/dist/profile/events/profile-updated.event.js.map +1 -0
  109. package/dist/profile/profile.service.d.ts +3 -1
  110. package/dist/profile/profile.service.d.ts.map +1 -1
  111. package/dist/profile/profile.service.js +28 -17
  112. package/dist/profile/profile.service.js.map +1 -1
  113. package/dist/session/session.controller.d.ts +1 -0
  114. package/dist/session/session.controller.d.ts.map +1 -1
  115. package/dist/session/session.service.d.ts +9 -4
  116. package/dist/session/session.service.d.ts.map +1 -1
  117. package/dist/session/session.service.js +61 -51
  118. package/dist/session/session.service.js.map +1 -1
  119. package/dist/setting/setting.controller.d.ts +5 -5
  120. package/dist/setting/setting.service.d.ts +5 -5
  121. package/dist/setting/setting.service.d.ts.map +1 -1
  122. package/dist/setting/setting.service.js +2 -0
  123. package/dist/setting/setting.service.js.map +1 -1
  124. package/dist/task/task.service.d.ts +8 -11
  125. package/dist/task/task.service.d.ts.map +1 -1
  126. package/dist/task/task.service.js +10 -16
  127. package/dist/task/task.service.js.map +1 -1
  128. package/dist/user/user.controller.d.ts +1 -0
  129. package/dist/user/user.controller.d.ts.map +1 -1
  130. package/dist/user/user.service.d.ts +3 -0
  131. package/dist/user/user.service.d.ts.map +1 -1
  132. package/dist/webhook-integration/webhook-command.registry.d.ts +9 -2
  133. package/dist/webhook-integration/webhook-command.registry.d.ts.map +1 -1
  134. package/dist/webhook-integration/webhook-command.registry.js.map +1 -1
  135. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  136. package/dist/webhook-integration/webhook-integration.service.js +13 -4
  137. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  138. package/hedhog/data/integration_provider.yaml +17 -0
  139. package/hedhog/data/integration_type.yaml +6 -0
  140. package/hedhog/data/route.yaml +86 -2
  141. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +109 -0
  142. package/hedhog/frontend/app/configurations/layout.tsx.ejs +6 -6
  143. package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +169 -14
  144. package/hedhog/frontend/messages/en.json +11 -2
  145. package/hedhog/frontend/messages/pt.json +11 -2
  146. package/hedhog/table/ai_execution.yaml +79 -0
  147. package/hedhog/table/ai_execution_step.yaml +65 -0
  148. package/hedhog/table/ai_instruction.yaml +43 -0
  149. package/hedhog/table/ai_instruction_version.yaml +30 -0
  150. package/hedhog/table/ai_model_pricing.yaml +35 -0
  151. package/hedhog/table/user_session.yaml +2 -0
  152. package/package.json +5 -4
  153. package/src/ai/ai.module.ts +2 -0
  154. package/src/ai/ai.service.ts +64 -31
  155. package/src/ai-instruction/ai-execution.service.ts +244 -0
  156. package/src/ai-instruction/ai-instruction.controller.ts +73 -0
  157. package/src/ai-instruction/ai-instruction.module.ts +14 -0
  158. package/src/ai-instruction/ai-instruction.service.ts +343 -0
  159. package/src/ai-instruction/ai-prompt-builder.service.ts +142 -0
  160. package/src/ai-instruction/dto/create-ai-instruction.dto.ts +36 -0
  161. package/src/ai-instruction/dto/update-ai-instruction.dto.ts +35 -0
  162. package/src/ai-instruction/types/ai-instruction.types.ts +90 -0
  163. package/src/auth/auth.controller.ts +23 -5
  164. package/src/auth/auth.service.ts +26 -15
  165. package/src/core.module.ts +3 -0
  166. package/src/file/file.service.ts +136 -4
  167. package/src/file/provider/abstract.provider.ts +4 -2
  168. package/src/file/provider/azure.provider.ts +6 -3
  169. package/src/file/provider/gcs.provider.ts +5 -3
  170. package/src/file/provider/local.provider.ts +7 -3
  171. package/src/file/provider/s3.provider.ts +8 -6
  172. package/src/file/provider/spaces.provider.ts +8 -7
  173. package/src/mcp-chat/mcp-chat.module.ts +19 -17
  174. package/src/mcp-chat/mcp-chat.service.ts +95 -9
  175. package/src/notification/notification.service.ts +32 -1
  176. package/src/oauth/oauth.service.ts +74 -1
  177. package/src/profile/events/profile-avatar-updated.event.ts +8 -0
  178. package/src/profile/events/profile-email-changed.event.ts +8 -0
  179. package/src/profile/events/profile-updated.event.ts +8 -0
  180. package/src/profile/profile.service.ts +25 -16
  181. package/src/session/session.service.ts +70 -52
  182. package/src/setting/setting.service.ts +10 -8
  183. package/src/task/task.service.ts +10 -16
  184. package/src/webhook-integration/webhook-command.registry.ts +10 -2
  185. package/src/webhook-integration/webhook-integration.service.ts +31 -4
@@ -40,7 +40,7 @@ export class SessionService {
40
40
  private readonly paginationService: PaginationService,
41
41
  ) { }
42
42
 
43
- async create(locale: string, userId: number, ipAddress: string, userAgent: string) {
43
+ async create(locale: string, userId: number, ipAddress: string, userAgent: string, browserId?: string | null) {
44
44
  if (!userId) {
45
45
  throw new BadRequestException(getLocaleText('session.userIdRequired', locale, 'User ID is required to create a session.'));
46
46
  }
@@ -60,9 +60,9 @@ export class SessionService {
60
60
  const token = await this.token.createOpaqueToken();
61
61
  const hash = this.security.hashWithPepper(token)
62
62
  const maxSessions = Number(settings['max-concurrent-sessions']) || 0;
63
-
63
+
64
64
  if (maxSessions > 0) {
65
- await this.enforceSessionLimit(userId, maxSessions);
65
+ await this.enforceSessionLimit(userId, maxSessions, false, browserId);
66
66
  }
67
67
 
68
68
  const session = await this.prisma.user_session.create({
@@ -71,40 +71,49 @@ export class SessionService {
71
71
  ip_address: ipAddress,
72
72
  user_agent: userAgent,
73
73
  expires_at: new Date(Date.now() + ((settings['refresh-token-expiration-minutes'] || 43200) * 60 * 1000)),
74
- hash
74
+ hash,
75
+ browser_id: browserId ?? null,
75
76
  },
76
77
  });
77
78
 
78
79
  return { token, session };
79
80
  }
80
81
 
81
- async refresh(locale: string, refreshToken: string, ipAddress: string, userAgent: string) {
82
+ async refresh(locale: string, refreshToken: string, ipAddress: string, userAgent: string, browserId?: string | null) {
82
83
  if (!refreshToken) {
83
84
  throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
84
85
  }
85
86
 
86
87
  const hash = this.security.hashWithPepper(refreshToken);
87
- const session = await this.prisma.user_session.findFirst({
88
- where: {
89
- hash,
90
- revoked_at: null,
91
- expires_at: {
92
- gt: new Date(),
93
- },
94
- },
95
- select: {
96
- id: true,
97
- user_id: true,
98
- ip_address: true,
99
- user_agent: true,
100
- },
88
+
89
+ const rawSession = await this.prisma.user_session.findFirst({
90
+ where: { hash },
91
+ select: { id: true, user_id: true, ip_address: true, user_agent: true, revoked_at: true, expires_at: true, browser_id: true },
101
92
  });
102
93
 
103
- if (!session) {
104
- this.logger.warn(`[SessionRefresh] Refresh rejected: session not found or expired for token hash prefix ${hash.slice(0, 8)}`);
94
+ if (!rawSession) {
95
+ this.logger.warn(`[SessionRefresh] Refresh rejected: hash prefix ${hash.slice(0, 8)} not found in DB`);
96
+ throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
97
+ }
98
+
99
+ if (rawSession.revoked_at !== null) {
100
+ this.logger.warn(`[SessionRefresh] Refresh rejected: sessionId=${rawSession.id} userId=${rawSession.user_id} revoked_at=${rawSession.revoked_at.toISOString()} hash prefix ${hash.slice(0, 8)}`);
105
101
  throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
106
102
  }
107
103
 
104
+ if (rawSession.expires_at <= new Date()) {
105
+ this.logger.warn(`[SessionRefresh] Refresh rejected: sessionId=${rawSession.id} userId=${rawSession.user_id} expired expires_at=${rawSession.expires_at.toISOString()} hash prefix ${hash.slice(0, 8)}`);
106
+ throw new UnauthorizedException(getLocaleText('accessDenied', locale, 'Access denied.'));
107
+ }
108
+
109
+ const session = {
110
+ id: rawSession.id,
111
+ user_id: rawSession.user_id,
112
+ ip_address: rawSession.ip_address,
113
+ user_agent: rawSession.user_agent,
114
+ browser_id: rawSession.browser_id,
115
+ };
116
+
108
117
  const settings = await this.setting.getSettingValues([
109
118
  'refresh-token-expiration-minutes',
110
119
  'max-concurrent-sessions',
@@ -112,7 +121,7 @@ export class SessionService {
112
121
 
113
122
  const maxSessions = Number(settings['max-concurrent-sessions']) || 0;
114
123
  if (maxSessions > 0) {
115
- await this.enforceSessionLimit(session.user_id, maxSessions, true);
124
+ await this.enforceSessionLimit(session.user_id, maxSessions, true, browserId);
116
125
  }
117
126
 
118
127
  const nextRefreshToken = await this.token.createOpaqueToken();
@@ -142,6 +151,7 @@ export class SessionService {
142
151
  user_agent: nextUserAgent,
143
152
  expires_at: expiresAt,
144
153
  hash: nextHash,
154
+ browser_id: session.browser_id ?? browserId ?? null,
145
155
  },
146
156
  });
147
157
 
@@ -367,51 +377,59 @@ export class SessionService {
367
377
 
368
378
  /**
369
379
  * Enforce maximum concurrent sessions limit for a user.
370
- * If the limit is reached, expires the oldest sessions to make room for new ones.
380
+ * Sessions from the same browser (same browser_id) count as a single slot.
381
+ * Sessions without a browser_id each count as their own slot (legacy/unknown clients).
371
382
  *
372
383
  * @param userId - The user ID to check
373
- * @param maxSessions - Maximum number of concurrent sessions allowed
384
+ * @param maxSessions - Maximum number of concurrent browser slots allowed
374
385
  * @param isRotation - When true, the calling flow is replacing an existing session (refresh),
375
386
  * not adding a new one, so no extra slot needs to be freed.
387
+ * @param currentBrowserId - The browser_id of the session being created/refreshed.
376
388
  */
377
- private async enforceSessionLimit(userId: number, maxSessions: number, isRotation = false): Promise<void> {
389
+ private async enforceSessionLimit(userId: number, maxSessions: number, isRotation = false, currentBrowserId?: string | null): Promise<void> {
378
390
  const activeSessions = await this.prisma.user_session.findMany({
379
391
  where: {
380
392
  user_id: userId,
381
393
  revoked_at: null,
382
- expires_at: {
383
- gt: new Date()
384
- }
385
- },
386
- orderBy: {
387
- created_at: 'asc'
394
+ expires_at: { gt: new Date() }
388
395
  },
389
- select: {
390
- id: true
391
- }
396
+ orderBy: { created_at: 'asc' },
397
+ select: { id: true, browser_id: true }
392
398
  });
393
399
 
394
- // During a rotation the current session is replaced (not added), so the net change is 0.
395
- // During creation a new session is added, so we need one extra free slot (+1).
396
- const slotsNeeded = isRotation ? 0 : 1;
397
- const sessionsToExpire = activeSessions.length - maxSessions + slotsNeeded;
398
- this.logger.debug(`[SessionLimit] userId=${userId} activeSessions=${activeSessions.length} maxSessions=${maxSessions} isRotation=${isRotation} toExpire=${sessionsToExpire}`);
399
- if (sessionsToExpire > 0) {
400
- const sessionIdsToExpire = activeSessions
401
- .slice(0, sessionsToExpire)
402
- .map(s => s.id);
400
+ // Group sessions into browser slots. Sessions sharing a browser_id = 1 slot.
401
+ // Sessions with null browser_id each count as their own slot (legacy clients).
402
+ // Map insertion order is preserved, and since activeSessions is ordered created_at asc,
403
+ // the oldest slot is always first.
404
+ type Slot = { ids: number[] };
405
+ const slotMap = new Map<string, Slot>();
406
+ for (const s of activeSessions) {
407
+ const key = s.browser_id ?? `__null_${s.id}`;
408
+ if (!slotMap.has(key)) slotMap.set(key, { ids: [] });
409
+ slotMap.get(key)!.ids.push(s.id);
410
+ }
411
+
412
+ const slots = Array.from(slotMap.values());
413
+ const currentSlotCount = slots.length;
403
414
 
404
- this.logger.warn(`[SessionLimit] Expiring ${sessionsToExpire} oldest session(s) for userId=${userId}: [${sessionIdsToExpire.join(',')}]`);
415
+ // A new browser slot is created only when:
416
+ // - it's not a rotation (rotation keeps the same slot), AND
417
+ // - the current browser doesn't already have an active slot
418
+ const currentBrowserHasSlot = Boolean(currentBrowserId && slotMap.has(currentBrowserId));
419
+ const isNewSlot = !isRotation && !currentBrowserHasSlot;
405
420
 
421
+ const slotsToRevoke = currentSlotCount - maxSessions + (isNewSlot ? 1 : 0);
422
+ this.logger.debug(`[SessionLimit] userId=${userId} slots=${currentSlotCount} maxSessions=${maxSessions} isRotation=${isRotation} browserId=${currentBrowserId ?? 'null'} newSlot=${isNewSlot} toRevoke=${Math.max(0, slotsToRevoke)}`);
423
+
424
+ if (slotsToRevoke > 0) {
425
+ const sessionIdsToRevoke: number[] = [];
426
+ for (let i = 0; i < slotsToRevoke; i++) {
427
+ sessionIdsToRevoke.push(...slots[i].ids);
428
+ }
429
+ this.logger.warn(`[SessionLimit] Revoking ${slotsToRevoke} oldest browser slot(s) for userId=${userId}: sessions [${sessionIdsToRevoke.join(',')}]`);
406
430
  await this.prisma.user_session.updateMany({
407
- where: {
408
- id: {
409
- in: sessionIdsToExpire
410
- }
411
- },
412
- data: {
413
- expires_at: new Date()
414
- }
431
+ where: { id: { in: sessionIdsToRevoke } },
432
+ data: { revoked_at: new Date() }
415
433
  });
416
434
  }
417
435
  }
@@ -3,14 +3,14 @@ import { getLocaleText, LocaleService } from '@hed-hog/api-locale';
3
3
  import { PaginationDTO, PaginationService } from '@hed-hog/api-pagination';
4
4
  import { PrismaService, setting_type_0f055cd5ea_enum } from '@hed-hog/api-prisma';
5
5
  import {
6
- BadRequestException,
7
- forwardRef,
8
- Inject,
9
- Injectable,
10
- Logger,
11
- NotFoundException,
12
- OnModuleDestroy,
13
- OnModuleInit,
6
+ BadRequestException,
7
+ forwardRef,
8
+ Inject,
9
+ Injectable,
10
+ Logger,
11
+ NotFoundException,
12
+ OnModuleDestroy,
13
+ OnModuleInit,
14
14
  } from '@nestjs/common';
15
15
  import { randomUUID } from 'node:crypto';
16
16
  import * as pako from 'pako';
@@ -422,6 +422,7 @@ export class SettingService implements OnModuleInit, OnModuleDestroy {
422
422
  async getSystemSettings(): Promise<any> {
423
423
  const locales = await this.localeService.getEnables();
424
424
  const slugs = [
425
+ 'language',
425
426
  'system-name',
426
427
  'system-slogan',
427
428
  'icon-url',
@@ -476,6 +477,7 @@ export class SettingService implements OnModuleInit, OnModuleDestroy {
476
477
  'operations.comment-edit-window',
477
478
  'lms-video-conversion-enabled',
478
479
  'lms-video-frame-capture-interval-seconds',
480
+ 'url'
479
481
  ];
480
482
  const setting = await this.getSettingValues(slugs);
481
483
  return { locales, setting };
@@ -128,20 +128,17 @@ export class TasksService {
128
128
  }
129
129
 
130
130
  /**
131
- * Limpa challenges de verificação de identificadores de usuário (email, telefone, etc.)
132
- * que expiraram e não foram verificados, removendo também os identificadores órfãos.
133
- *
134
- * Remove em cascata:
135
- * 1. user_identifier_challenge: challenges expirados não verificados
136
- * 2. user_identifier: identificadores que nunca foram verificados e perderam seus challenges
131
+ * Limpa challenges de verificação de identificadores de usuário (email, telefone, etc.)
132
+ * que expiraram e não foram verificados.
133
+ *
134
+ * Remove em cascata:
135
+ * 1. user_identifier_challenge: challenges expirados não verificados
137
136
  *
138
137
  * Por quê: Quando um usuário tenta adicionar um novo email/telefone, é gerado um challenge
139
138
  * para verificação. Se o usuário não verifica dentro do prazo, tanto o challenge quanto
140
- * o identificador não verificado devem ser removidos para evitar:
141
- * - Acúmulo de dados órfãos (identificadores que nunca serão usados)
142
- * - Bloqueio indevido de emails/telefones (um email não verificado não deve impedir
143
- * outro usuário de cadastrá-lo)
144
- * - Confusão na interface (mostrar identificadores pendentes indefinidamente)
139
+ * o challenge devem ser removidos para evitar:
140
+ * - Acúmulo de challenges órfãos
141
+ * - Confusão na interface (mostrar verificações pendentes indefinidamente)
145
142
  *
146
143
  * Limite: 20.000 registros por execução para evitar lock prolongado das tabelas.
147
144
  */
@@ -166,14 +163,11 @@ export class TasksService {
166
163
  AND c.verified_at IS NULL
167
164
  RETURNING c.user_identifier_id
168
165
  )
169
- DELETE FROM user_identifier ui
170
- USING deleted_challenges
171
- WHERE ui.id = deleted_challenges.user_identifier_id
172
- AND ui.verified_at IS NULL
166
+ SELECT 1
173
167
  `;
174
168
 
175
169
  this.logger.verbose(
176
- `Expired user identifier challenges and unverified identifiers cleaned up in ${Date.now() - startAt}ms`,
170
+ `Expired user identifier challenges cleaned up in ${Date.now() - startAt}ms`,
177
171
  );
178
172
  }
179
173
 
@@ -1,5 +1,13 @@
1
1
  import { Injectable } from '@nestjs/common';
2
2
 
3
+ export interface WebhookCommandContext extends Record<string, unknown> {
4
+ webhookBody?: unknown;
5
+ webhookHeaders?: Record<string, unknown>;
6
+ remoteIp?: string | null;
7
+ publicUuid?: string;
8
+ integrationId?: number;
9
+ }
10
+
3
11
  export interface WebhookCommand {
4
12
  slug: string;
5
13
  name: string;
@@ -7,7 +15,7 @@ export interface WebhookCommand {
7
15
  inputSchema: Record<string, unknown>;
8
16
  handler: (
9
17
  params: Record<string, unknown>,
10
- context?: Record<string, unknown>,
18
+ context?: WebhookCommandContext,
11
19
  ) => Promise<unknown>;
12
20
  }
13
21
 
@@ -30,7 +38,7 @@ export class WebhookCommandRegistry {
30
38
  async dispatch(
31
39
  slug: string,
32
40
  params: Record<string, unknown>,
33
- context?: Record<string, unknown>,
41
+ context?: WebhookCommandContext,
34
42
  ): Promise<unknown> {
35
43
  const command = this.find(slug);
36
44
  if (!command) {
@@ -581,7 +581,12 @@ export class WebhookIntegrationService {
581
581
  const actionStartedAt = Date.now();
582
582
 
583
583
  try {
584
- const result = await this.executeAction(action, body);
584
+ const result = await this.executeAction(action, body, {
585
+ webhookHeaders: normalizeJsonObject(headers) || {},
586
+ remoteIp,
587
+ publicUuid: uuid,
588
+ integrationId: integration.id,
589
+ });
585
590
  const durationMs = Date.now() - actionStartedAt;
586
591
 
587
592
  actionResults.push({
@@ -695,7 +700,16 @@ export class WebhookIntegrationService {
695
700
  }
696
701
  }
697
702
 
698
- private async executeAction(action: any, body: any) {
703
+ private async executeAction(
704
+ action: any,
705
+ body: any,
706
+ context?: {
707
+ webhookHeaders?: Record<string, unknown>;
708
+ remoteIp?: string | null;
709
+ publicUuid?: string;
710
+ integrationId?: number;
711
+ },
712
+ ) {
699
713
  if (action.type === 'email') {
700
714
  return this.executeEmailAction(action, body);
701
715
  }
@@ -713,7 +727,7 @@ export class WebhookIntegrationService {
713
727
  }
714
728
 
715
729
  if (action.type === 'app_command') {
716
- return this.executeAppCommandAction(action, body);
730
+ return this.executeAppCommandAction(action, body, context);
717
731
  }
718
732
 
719
733
  throw new BadRequestException(`Unsupported action type: ${action.type}`);
@@ -1013,7 +1027,16 @@ export class WebhookIntegrationService {
1013
1027
  };
1014
1028
  }
1015
1029
 
1016
- private async executeAppCommandAction(action: any, body: any) {
1030
+ private async executeAppCommandAction(
1031
+ action: any,
1032
+ body: any,
1033
+ context?: {
1034
+ webhookHeaders?: Record<string, unknown>;
1035
+ remoteIp?: string | null;
1036
+ publicUuid?: string;
1037
+ integrationId?: number;
1038
+ },
1039
+ ) {
1017
1040
  this.validateActionConfiguration(action);
1018
1041
 
1019
1042
  const slug = String(action.app_command_slug || '').trim();
@@ -1042,6 +1065,10 @@ export class WebhookIntegrationService {
1042
1065
  try {
1043
1066
  result = await this.commandRegistry.dispatch(slug, renderedParams, {
1044
1067
  webhookBody: body,
1068
+ webhookHeaders: context?.webhookHeaders,
1069
+ remoteIp: context?.remoteIp,
1070
+ publicUuid: context?.publicUuid,
1071
+ integrationId: context?.integrationId,
1045
1072
  });
1046
1073
  } catch (error) {
1047
1074
  this.attachErrorRequestPayload(error, requestPayload);