@hed-hog/core 0.0.384 → 0.0.386

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 (123) hide show
  1. package/dist/app-command/app-command.registry.d.ts +2 -0
  2. package/dist/app-command/app-command.registry.d.ts.map +1 -1
  3. package/dist/app-command/app-command.registry.js.map +1 -1
  4. package/dist/auth/auth.controller.d.ts +23 -1
  5. package/dist/auth/auth.controller.d.ts.map +1 -1
  6. package/dist/auth/auth.controller.js +27 -0
  7. package/dist/auth/auth.controller.js.map +1 -1
  8. package/dist/auth/auth.service.d.ts +56 -6
  9. package/dist/auth/auth.service.d.ts.map +1 -1
  10. package/dist/auth/auth.service.js +108 -0
  11. package/dist/auth/auth.service.js.map +1 -1
  12. package/dist/auth/dto/complete-account.dto.d.ts +9 -0
  13. package/dist/auth/dto/complete-account.dto.d.ts.map +1 -0
  14. package/dist/auth/dto/complete-account.dto.js +43 -0
  15. package/dist/auth/dto/complete-account.dto.js.map +1 -0
  16. package/dist/authorize/authorize.controller.d.ts +45 -0
  17. package/dist/authorize/authorize.controller.d.ts.map +1 -0
  18. package/dist/authorize/authorize.controller.js +120 -0
  19. package/dist/authorize/authorize.controller.js.map +1 -0
  20. package/dist/authorize/authorize.module.d.ts +8 -0
  21. package/dist/authorize/authorize.module.d.ts.map +1 -0
  22. package/dist/authorize/authorize.module.js +36 -0
  23. package/dist/authorize/authorize.module.js.map +1 -0
  24. package/dist/authorize/authorize.service.d.ts +79 -0
  25. package/dist/authorize/authorize.service.d.ts.map +1 -0
  26. package/dist/authorize/authorize.service.js +196 -0
  27. package/dist/authorize/authorize.service.js.map +1 -0
  28. package/dist/authorize/dto/authorize-info.dto.d.ts +8 -0
  29. package/dist/authorize/dto/authorize-info.dto.d.ts.map +1 -0
  30. package/dist/authorize/dto/authorize-info.dto.js +42 -0
  31. package/dist/authorize/dto/authorize-info.dto.js.map +1 -0
  32. package/dist/authorize/dto/consent.dto.d.ts +8 -0
  33. package/dist/authorize/dto/consent.dto.d.ts.map +1 -0
  34. package/dist/authorize/dto/consent.dto.js +42 -0
  35. package/dist/authorize/dto/consent.dto.js.map +1 -0
  36. package/dist/authorize/dto/token.dto.d.ts +8 -0
  37. package/dist/authorize/dto/token.dto.d.ts.map +1 -0
  38. package/dist/authorize/dto/token.dto.js +42 -0
  39. package/dist/authorize/dto/token.dto.js.map +1 -0
  40. package/dist/challenge/challenge.service.d.ts +7 -0
  41. package/dist/challenge/challenge.service.d.ts.map +1 -1
  42. package/dist/challenge/challenge.service.js +22 -0
  43. package/dist/challenge/challenge.service.js.map +1 -1
  44. package/dist/core.module.d.ts.map +1 -1
  45. package/dist/core.module.js +2 -0
  46. package/dist/core.module.js.map +1 -1
  47. package/dist/file/file.service.d.ts +7 -0
  48. package/dist/file/file.service.d.ts.map +1 -1
  49. package/dist/file/file.service.js +16 -0
  50. package/dist/file/file.service.js.map +1 -1
  51. package/dist/integration-profile/integration-profile.secrets.d.ts +6 -2
  52. package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
  53. package/dist/integration-profile/integration-profile.secrets.js +11 -2
  54. package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
  55. package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
  56. package/dist/integration-profile/integration-profile.service.js +18 -2
  57. package/dist/integration-profile/integration-profile.service.js.map +1 -1
  58. package/dist/integration-profile/integration-profile.utils.d.ts +10 -0
  59. package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
  60. package/dist/integration-profile/integration-profile.utils.js +24 -1
  61. package/dist/integration-profile/integration-profile.utils.js.map +1 -1
  62. package/dist/notification/notification.controller.d.ts +12 -12
  63. package/dist/notification/notification.service.d.ts +12 -12
  64. package/dist/oauth/interfaces/OAuthProvider.d.ts +1 -1
  65. package/dist/oauth/interfaces/OAuthProvider.d.ts.map +1 -1
  66. package/dist/oauth/oauth.controller.d.ts +26 -0
  67. package/dist/oauth/oauth.controller.d.ts.map +1 -1
  68. package/dist/oauth/oauth.controller.js +94 -0
  69. package/dist/oauth/oauth.controller.js.map +1 -1
  70. package/dist/oauth/oauth.service.d.ts +39 -1
  71. package/dist/oauth/oauth.service.d.ts.map +1 -1
  72. package/dist/oauth/oauth.service.js +115 -7
  73. package/dist/oauth/oauth.service.js.map +1 -1
  74. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +5 -1
  75. package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
  76. package/dist/oauth/providers/microsoft-entra-id.provider.js +37 -6
  77. package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
  78. package/dist/profile/profile.controller.d.ts +1 -1
  79. package/dist/profile/profile.service.d.ts +1 -1
  80. package/dist/session/session.controller.d.ts +1 -1
  81. package/dist/session/session.service.d.ts +3 -3
  82. package/dist/user/user.controller.d.ts +1 -1
  83. package/dist/user/user.service.d.ts +3 -3
  84. package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
  85. package/dist/webhook-integration/webhook-integration.service.js +7 -1
  86. package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
  87. package/hedhog/data/integration_provider.yaml +8 -0
  88. package/hedhog/data/integration_type.yaml +6 -0
  89. package/hedhog/data/mail.yaml +39 -0
  90. package/hedhog/data/route.yaml +12 -0
  91. package/hedhog/data/setting_group.yaml +15 -1
  92. package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +11 -7
  93. package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +9 -7
  94. package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +6 -2
  95. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +130 -34
  96. package/hedhog/frontend/app/roles/page.tsx.ejs +13 -12
  97. package/hedhog/frontend/messages/en.json +35 -2
  98. package/hedhog/frontend/messages/pt.json +35 -2
  99. package/package.json +10 -5
  100. package/src/app-command/app-command.registry.ts +2 -0
  101. package/src/auth/auth.controller.ts +21 -0
  102. package/src/auth/auth.service.ts +133 -0
  103. package/src/auth/dto/complete-account.dto.ts +24 -0
  104. package/src/authorize/authorize.controller.ts +99 -0
  105. package/src/authorize/authorize.module.ts +23 -0
  106. package/src/authorize/authorize.service.spec.ts +307 -0
  107. package/src/authorize/authorize.service.ts +245 -0
  108. package/src/authorize/dto/authorize-info.dto.ts +23 -0
  109. package/src/authorize/dto/consent.dto.ts +23 -0
  110. package/src/authorize/dto/token.dto.ts +23 -0
  111. package/src/challenge/challenge.service.ts +28 -0
  112. package/src/core.module.ts +2 -0
  113. package/src/file/file.service.ts +19 -0
  114. package/src/integration-profile/integration-profile.secrets.spec.ts +114 -0
  115. package/src/integration-profile/integration-profile.secrets.ts +11 -2
  116. package/src/integration-profile/integration-profile.service.ts +19 -0
  117. package/src/integration-profile/integration-profile.utils.ts +40 -0
  118. package/src/mcp/decorators/allow-mcp-token.decorator.ts +7 -7
  119. package/src/oauth/interfaces/OAuthProvider.ts +5 -1
  120. package/src/oauth/oauth.controller.ts +89 -0
  121. package/src/oauth/oauth.service.ts +190 -8
  122. package/src/oauth/providers/microsoft-entra-id.provider.ts +45 -5
  123. package/src/webhook-integration/webhook-integration.service.ts +18 -2
@@ -89,12 +89,17 @@ export class OAuthService {
89
89
  this.providers.set(linkedin.getProviderType(), linkedin);
90
90
  }
91
91
 
92
- async getAuthUrl(provider: user_account_provider_52222e2ecb_enum, callbackPath: string) {
92
+ async getAuthUrl(
93
+ provider: user_account_provider_52222e2ecb_enum,
94
+ callbackPath: string,
95
+ flow?: 'login' | 'register' | 'connect',
96
+ app?: string,
97
+ ) {
93
98
  const enabled = await this.isProviderEnabled(provider);
94
99
  if (!enabled) {
95
100
  throw new BadRequestException(`OAuth provider ${provider} is not enabled.`);
96
101
  }
97
- return this.getProvider(provider).getAuthUrl(callbackPath);
102
+ return this.getProvider(provider).getAuthUrl(callbackPath, flow, app);
98
103
  }
99
104
 
100
105
  async getMobileAuthUrl(
@@ -137,15 +142,16 @@ export class OAuthService {
137
142
  flow: 'login' | 'register' | 'connect',
138
143
  redirectApp?: string,
139
144
  ) {
145
+ // `self` means "the hub handles it locally"; a resolvable app key means the hub
146
+ // forwards the callback back to that app (origin-bound).
147
+ const app = this.sanitizeAppKey(redirectApp) ?? OAuthService.WEB_STATE_SELF_APP;
148
+
140
149
  // Single, flow-less callback per provider: the flow (and the initiating app)
141
150
  // travel in the signed state, so only ONE redirect URI needs to be registered
142
- // per provider in its OAuth console.
151
+ // per provider in its OAuth console. `flow`/`app` are forwarded so providers that
152
+ // need them (e.g. Microsoft Entra ID's backend bounce + `prompt=consent`) can adapt.
143
153
  const callbackPath = `/callback/${String(provider)}`;
144
- const authUrl = await this.getAuthUrl(provider, callbackPath);
145
-
146
- // Always carry the flow. `self` means "the hub handles it locally"; a resolvable
147
- // app key means the hub forwards the callback back to that app (origin-bound).
148
- const app = this.sanitizeAppKey(redirectApp) ?? OAuthService.WEB_STATE_SELF_APP;
154
+ const authUrl = await this.getAuthUrl(provider, callbackPath, flow, app);
149
155
 
150
156
  const url = new URL(authUrl);
151
157
  url.searchParams.set('state', this.buildWebState(app, flow));
@@ -549,6 +555,182 @@ export class OAuthService {
549
555
  return record?.redirect_uri ?? null;
550
556
  }
551
557
 
558
+ /** TTL for the short web-ref bounce token (Entra). Short: it briefly holds an encrypted profile. */
559
+ private readonly webRefTokenTtlMs = 2 * 60 * 1000;
560
+
561
+ /**
562
+ * Server-side bounce for providers whose authorization `code` is too large to
563
+ * safely ride a front-end app URL (Microsoft Entra ID). Exchanges the code here
564
+ * (once, under the callback lock), stores the resulting profile encrypted under
565
+ * an opaque single-use `ref`, and returns that `ref`. The app later redeems it via
566
+ * `redeemWebRef`, so the giant code never reaches a front-end ingress.
567
+ */
568
+ async beginWebRefCallback({
569
+ provider,
570
+ code,
571
+ state,
572
+ flow,
573
+ appOrigin,
574
+ }: {
575
+ provider: user_account_provider_52222e2ecb_enum;
576
+ code: string;
577
+ state?: string;
578
+ flow: 'login' | 'register' | 'connect';
579
+ appOrigin: string | null;
580
+ }): Promise<string> {
581
+ if (!code) {
582
+ throw new BadRequestException('Authorization code is required.');
583
+ }
584
+
585
+ // Match the redirect_uri used at authorize time (the API callback).
586
+ const settings = await this.setting.getSettingValues(['api-url']);
587
+ const apiRedirectUri = new URL(
588
+ '/oauth/microsoft-entra-id/callback',
589
+ String(settings['api-url'] ?? ''),
590
+ ).toString();
591
+
592
+ const callbackKey = this.buildCallbackKey(provider, flow, code);
593
+ const profile = await this.callbackCoordinator.execute(callbackKey, async () =>
594
+ this.getProvider(provider).getProfile(code, flow, apiRedirectUri),
595
+ );
596
+
597
+ const token = this.security.randomOpaque(24);
598
+ const signature = this.security.signHmacSha256(`oauth-web-ref:${token}`);
599
+ const tokenHash = this.security.hashSha256(token);
600
+ const expiresAt = new Date(Date.now() + this.webRefTokenTtlMs);
601
+ const profileEnc = this.security.encrypt(JSON.stringify(profile));
602
+
603
+ await this.prisma.$executeRaw(
604
+ Prisma.sql`
605
+ INSERT INTO oauth_web_ref_token (
606
+ token_hash, provider, flow_type, app_origin, profile_enc, expires_at, created_at
607
+ ) VALUES (
608
+ ${tokenHash},
609
+ ${String(provider).toLowerCase()},
610
+ ${flow},
611
+ ${appOrigin},
612
+ ${profileEnc},
613
+ ${expiresAt},
614
+ NOW()
615
+ )
616
+ `,
617
+ );
618
+
619
+ return `${token}.${signature}`;
620
+ }
621
+
622
+ /**
623
+ * Redeems a `ref` issued by `beginWebRefCallback`: single-use consume, origin-bind
624
+ * to the initiating app, then finalize the flow (sets the refresh cookie via `res`
625
+ * and returns `{ accessToken }`) — byte-for-byte the same response as `callback/login`.
626
+ */
627
+ async redeemWebRef({
628
+ res,
629
+ ref,
630
+ provider,
631
+ locale,
632
+ ipAddress,
633
+ userAgent,
634
+ userId,
635
+ requestOrigin,
636
+ requestReferer,
637
+ }: {
638
+ res: Response;
639
+ ref: string;
640
+ provider: user_account_provider_52222e2ecb_enum;
641
+ locale: string;
642
+ ipAddress: string;
643
+ userAgent: string;
644
+ userId?: number;
645
+ requestOrigin?: string;
646
+ requestReferer?: string;
647
+ }) {
648
+ const parts = String(ref ?? '').split('.');
649
+ const token = parts[0];
650
+ const signature = parts[1];
651
+
652
+ if (!token || !signature) {
653
+ throw new BadRequestException(
654
+ getLocaleText('oauth.callback.invalidState', locale, 'OAuth reference is invalid or expired. Start the sign-in flow again.'),
655
+ );
656
+ }
657
+
658
+ if (!this.security.verifyHmacSha256(`oauth-web-ref:${token}`, signature)) {
659
+ throw new BadRequestException(
660
+ getLocaleText('oauth.callback.invalidState', locale, 'OAuth reference is invalid or expired. Start the sign-in flow again.'),
661
+ );
662
+ }
663
+
664
+ const tokenHash = this.security.hashSha256(token);
665
+ const rows = await this.prisma.$queryRaw<
666
+ Array<{ profile_enc: string; flow_type: string; app_origin: string | null }>
667
+ >(
668
+ Prisma.sql`
669
+ UPDATE oauth_web_ref_token
670
+ SET consumed_at = NOW()
671
+ WHERE token_hash = ${tokenHash}
672
+ AND provider = ${String(provider).toLowerCase()}
673
+ AND expires_at > NOW()
674
+ AND consumed_at IS NULL
675
+ RETURNING profile_enc, flow_type, app_origin
676
+ `,
677
+ );
678
+
679
+ const row = rows[0];
680
+ if (!row) {
681
+ throw new BadRequestException(
682
+ getLocaleText('oauth.callback.alreadyProcessed', locale, 'This OAuth callback was already processed. Start the sign-in flow again.'),
683
+ );
684
+ }
685
+
686
+ const flow = row.flow_type as 'login' | 'register' | 'connect';
687
+
688
+ // Origin-bind at redeem (the backend callback's Origin was Microsoft, so the
689
+ // usual handleCallback binding can't run there). Skip only when no app origin
690
+ // was recorded (the hub's own "self" flow).
691
+ if (row.app_origin) {
692
+ const expectedOrigin = this.normalizeOrigin(row.app_origin);
693
+ const boundOrigin =
694
+ this.normalizeOrigin(requestOrigin) ?? this.normalizeOrigin(requestReferer);
695
+ if (!boundOrigin || expectedOrigin !== boundOrigin) {
696
+ throw new BadRequestException(
697
+ getLocaleText('oauth.callback.invalidState', locale, 'OAuth callback origin is invalid. Start the sign-in flow again.'),
698
+ );
699
+ }
700
+ }
701
+
702
+ if (flow === 'connect' && !userId) {
703
+ throw new BadRequestException(
704
+ getLocaleText('oauth.callback.invalidState', locale, 'OAuth reference is invalid or expired. Start the sign-in flow again.'),
705
+ );
706
+ }
707
+
708
+ let profile: any;
709
+ try {
710
+ profile = JSON.parse(this.security.decrypt(row.profile_enc));
711
+ } catch {
712
+ throw new BadRequestException(
713
+ getLocaleText('oauth.callback.invalidState', locale, 'OAuth reference is invalid or expired. Start the sign-in flow again.'),
714
+ );
715
+ }
716
+
717
+ try {
718
+ return await this.finalizeCallback({
719
+ res,
720
+ locale,
721
+ ipAddress,
722
+ userAgent,
723
+ provider,
724
+ type: flow,
725
+ profile,
726
+ userId,
727
+ includeRefreshTokenInBody: false,
728
+ });
729
+ } catch (error) {
730
+ throw this.mapCallbackError(locale, provider, flow, error);
731
+ }
732
+ }
733
+
552
734
  private async finalizeCallback({
553
735
  res,
554
736
  locale,
@@ -35,10 +35,22 @@ export class MicrosoftEntraIdProvider extends BaseOAuthProvider {
35
35
  return 'MICROSOFT-ENTRA-ID';
36
36
  }
37
37
 
38
+ // Fixed API callback used by the web flow so the ~2KB Entra `code` is received
39
+ // and exchanged server-side (never forwarded into a front-end app URL).
40
+ private static readonly API_CALLBACK_PATH = '/oauth/microsoft-entra-id/callback';
41
+
42
+ // Apps whose front-end callback pages know how to redeem the server-side bounce
43
+ // `ref`. Only these use the API bounce; every other web app (and mobile) keeps the
44
+ // legacy front-end redirect, so this change is additive and non-regressing. Add an
45
+ // app here once its callback pages handle `?ref=`. `self` is the hub (admin), which
46
+ // initiates Entra without a `redirectApp` and thus resolves to the sentinel `self`.
47
+ private static readonly API_BOUNCE_APPS = new Set(['class', 'self', 'training', 'partners']);
48
+
38
49
  private async loadConfig() {
39
50
  const settings = await this.setting.getSettingValues([
40
51
  'oauth-microsoft-entra-id-profile-id',
41
52
  'url',
53
+ 'api-url',
42
54
  ]);
43
55
  const profileSlug = String(settings['oauth-microsoft-entra-id-profile-id'] ?? '').trim();
44
56
  if (!profileSlug) throw new BadRequestException('Microsoft Entra ID OAuth profile not configured.');
@@ -56,19 +68,47 @@ export class MicrosoftEntraIdProvider extends BaseOAuthProvider {
56
68
  if (!tenantId) {
57
69
  throw new BadRequestException('Microsoft Entra ID OAuth profile is missing tenant_id.');
58
70
  }
59
- return { clientId, clientSecret, tenantId, baseUrl: String(settings['url'] ?? '') };
71
+ return {
72
+ clientId,
73
+ clientSecret,
74
+ tenantId,
75
+ baseUrl: String(settings['url'] ?? ''),
76
+ apiUrl: String(settings['api-url'] ?? ''),
77
+ };
60
78
  }
61
79
 
62
- async getAuthUrl(callbackPath: string) {
63
- const { clientId, tenantId, baseUrl } = await this.loadConfig();
80
+ /** Absolute redirect URI the web flow registers/exchanges against (the API). */
81
+ private buildApiRedirectUri(apiUrl: string) {
82
+ if (!String(apiUrl).trim()) {
83
+ throw new BadRequestException(
84
+ 'Microsoft Entra ID OAuth requires the `api-url` setting for the web callback.',
85
+ );
86
+ }
87
+ return this.buildRedirectUri(apiUrl, MicrosoftEntraIdProvider.API_CALLBACK_PATH);
88
+ }
89
+
90
+ async getAuthUrl(
91
+ callbackPath: string,
92
+ flow?: 'login' | 'register' | 'connect',
93
+ app?: string,
94
+ ) {
95
+ const { clientId, tenantId, baseUrl, apiUrl } = await this.loadConfig();
64
96
  const settings = await this.setting.getSettingValues(['oauth-microsoft-entra-id-scopes']);
65
97
  const baseScopes: string[] = settings['oauth-microsoft-entra-id-scopes'] ?? ['openid', 'profile', 'email'];
66
98
  const scopes = baseScopes.includes('offline_access')
67
99
  ? baseScopes
68
100
  : [...baseScopes, 'offline_access'];
69
101
 
70
- const redirectURI = this.buildRedirectUri(baseUrl, callbackPath);
71
- const isConnectFlow = callbackPath.includes('/connect');
102
+ // Only apps that can redeem a `ref` bounce through the API (where the ~2KB code
103
+ // is exchanged server-side); every other web app and mobile keep the front-end
104
+ // callback, so this stays additive and non-regressing.
105
+ const useApiBounce = !!app && MicrosoftEntraIdProvider.API_BOUNCE_APPS.has(app);
106
+ const redirectURI = useApiBounce
107
+ ? this.buildApiRedirectUri(apiUrl)
108
+ : this.buildRedirectUri(baseUrl, callbackPath);
109
+ const isConnectFlow = useApiBounce
110
+ ? flow === 'connect'
111
+ : callbackPath.includes('/connect');
72
112
  const params = new URLSearchParams({
73
113
  client_id: clientId,
74
114
  redirect_uri: redirectURI,
@@ -610,6 +610,10 @@ export class WebhookIntegrationService {
610
610
  body,
611
611
  remoteIp,
612
612
  webhookHeaders: normalizeJsonObject(headers) || {},
613
+ // `rawBody` is stashed onto the request by the API's global body-parser `verify`
614
+ // callback (apps/api/src/main.ts); typed loosely here since this package doesn't
615
+ // own that Express.Request augmentation.
616
+ webhookRawBody: (req as Request & { rawBody?: Buffer }).rawBody,
613
617
  startedAt,
614
618
  });
615
619
  }
@@ -625,10 +629,18 @@ export class WebhookIntegrationService {
625
629
  body: any;
626
630
  remoteIp: string | null;
627
631
  webhookHeaders: Record<string, unknown>;
632
+ webhookRawBody?: Buffer;
628
633
  startedAt: number;
629
634
  }) {
630
- const { integration, uuid, body, remoteIp, webhookHeaders, startedAt } =
631
- input;
635
+ const {
636
+ integration,
637
+ uuid,
638
+ body,
639
+ remoteIp,
640
+ webhookHeaders,
641
+ webhookRawBody,
642
+ startedAt,
643
+ } = input;
632
644
  const actionLogs: ActionExecutionLog[] = [];
633
645
 
634
646
  try {
@@ -639,6 +651,7 @@ export class WebhookIntegrationService {
639
651
  try {
640
652
  const result = await this.executeAction(action, body, {
641
653
  webhookHeaders,
654
+ webhookRawBody,
642
655
  remoteIp,
643
656
  publicUuid: uuid,
644
657
  integrationId: integration.id,
@@ -811,6 +824,7 @@ export class WebhookIntegrationService {
811
824
  body: any,
812
825
  context?: {
813
826
  webhookHeaders?: Record<string, unknown>;
827
+ webhookRawBody?: Buffer;
814
828
  remoteIp?: string | null;
815
829
  publicUuid?: string;
816
830
  integrationId?: number;
@@ -1138,6 +1152,7 @@ export class WebhookIntegrationService {
1138
1152
  body: any,
1139
1153
  context?: {
1140
1154
  webhookHeaders?: Record<string, unknown>;
1155
+ webhookRawBody?: Buffer;
1141
1156
  remoteIp?: string | null;
1142
1157
  publicUuid?: string;
1143
1158
  integrationId?: number;
@@ -1172,6 +1187,7 @@ export class WebhookIntegrationService {
1172
1187
  result = await this.commandRegistry.dispatch(slug, renderedParams, {
1173
1188
  webhookBody: body,
1174
1189
  webhookHeaders: context?.webhookHeaders,
1190
+ webhookRawBody: context?.webhookRawBody,
1175
1191
  remoteIp: context?.remoteIp,
1176
1192
  publicUuid: context?.publicUuid,
1177
1193
  integrationId: context?.integrationId,