@hed-hog/core 0.0.385 → 0.0.387
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.
- package/dist/app-command/app-command.registry.d.ts +2 -0
- package/dist/app-command/app-command.registry.d.ts.map +1 -1
- package/dist/app-command/app-command.registry.js.map +1 -1
- package/dist/auth/auth.controller.d.ts +24 -1
- package/dist/auth/auth.controller.d.ts.map +1 -1
- package/dist/auth/auth.controller.js +27 -0
- package/dist/auth/auth.controller.js.map +1 -1
- package/dist/auth/auth.service.d.ts +52 -1
- package/dist/auth/auth.service.d.ts.map +1 -1
- package/dist/auth/auth.service.js +108 -0
- package/dist/auth/auth.service.js.map +1 -1
- package/dist/auth/dto/complete-account.dto.d.ts +9 -0
- package/dist/auth/dto/complete-account.dto.d.ts.map +1 -0
- package/dist/auth/dto/complete-account.dto.js +43 -0
- package/dist/auth/dto/complete-account.dto.js.map +1 -0
- package/dist/authorize/authorize.controller.d.ts +45 -0
- package/dist/authorize/authorize.controller.d.ts.map +1 -0
- package/dist/authorize/authorize.controller.js +120 -0
- package/dist/authorize/authorize.controller.js.map +1 -0
- package/dist/authorize/authorize.module.d.ts +8 -0
- package/dist/authorize/authorize.module.d.ts.map +1 -0
- package/dist/authorize/authorize.module.js +36 -0
- package/dist/authorize/authorize.module.js.map +1 -0
- package/dist/authorize/authorize.service.d.ts +79 -0
- package/dist/authorize/authorize.service.d.ts.map +1 -0
- package/dist/authorize/authorize.service.js +196 -0
- package/dist/authorize/authorize.service.js.map +1 -0
- package/dist/authorize/dto/authorize-info.dto.d.ts +8 -0
- package/dist/authorize/dto/authorize-info.dto.d.ts.map +1 -0
- package/dist/authorize/dto/authorize-info.dto.js +42 -0
- package/dist/authorize/dto/authorize-info.dto.js.map +1 -0
- package/dist/authorize/dto/consent.dto.d.ts +8 -0
- package/dist/authorize/dto/consent.dto.d.ts.map +1 -0
- package/dist/authorize/dto/consent.dto.js +42 -0
- package/dist/authorize/dto/consent.dto.js.map +1 -0
- package/dist/authorize/dto/token.dto.d.ts +8 -0
- package/dist/authorize/dto/token.dto.d.ts.map +1 -0
- package/dist/authorize/dto/token.dto.js +42 -0
- package/dist/authorize/dto/token.dto.js.map +1 -0
- package/dist/challenge/challenge.service.d.ts +7 -0
- package/dist/challenge/challenge.service.d.ts.map +1 -1
- package/dist/challenge/challenge.service.js +22 -0
- package/dist/challenge/challenge.service.js.map +1 -1
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +2 -0
- package/dist/core.module.js.map +1 -1
- package/dist/file/file.service.d.ts +7 -0
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +16 -0
- package/dist/file/file.service.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts +6 -2
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +14 -3
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +7 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +91 -4
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/integration-profile.utils.d.ts +28 -0
- package/dist/integration-profile/integration-profile.utils.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.utils.js +40 -1
- package/dist/integration-profile/integration-profile.utils.js.map +1 -1
- package/dist/oauth/interfaces/OAuthProvider.d.ts +1 -1
- package/dist/oauth/interfaces/OAuthProvider.d.ts.map +1 -1
- package/dist/oauth/oauth.controller.d.ts +26 -0
- package/dist/oauth/oauth.controller.d.ts.map +1 -1
- package/dist/oauth/oauth.controller.js +94 -0
- package/dist/oauth/oauth.controller.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +39 -1
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js +115 -7
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts +5 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.d.ts.map +1 -1
- package/dist/oauth/providers/microsoft-entra-id.provider.js +37 -6
- package/dist/oauth/providers/microsoft-entra-id.provider.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +4 -3
- package/dist/profile/profile.controller.d.ts.map +1 -1
- package/dist/profile/profile.controller.js.map +1 -1
- package/dist/profile/profile.service.d.ts +5 -4
- package/dist/profile/profile.service.d.ts.map +1 -1
- package/dist/profile/profile.service.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.d.ts +5 -8
- package/dist/webhook-integration/webhook-integration.controller.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.controller.js +31 -8
- package/dist/webhook-integration/webhook-integration.controller.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.module.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.module.js +5 -0
- package/dist/webhook-integration/webhook-integration.module.js.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts +33 -1
- package/dist/webhook-integration/webhook-integration.service.d.ts.map +1 -1
- package/dist/webhook-integration/webhook-integration.service.js +116 -5
- package/dist/webhook-integration/webhook-integration.service.js.map +1 -1
- package/dist/webhook-integration/webhook-protocol.registry.d.ts +63 -0
- package/dist/webhook-integration/webhook-protocol.registry.d.ts.map +1 -0
- package/dist/webhook-integration/webhook-protocol.registry.js +51 -0
- package/dist/webhook-integration/webhook-protocol.registry.js.map +1 -0
- package/hedhog/data/integration_provider.yaml +8 -0
- package/hedhog/data/integration_type.yaml +6 -0
- package/hedhog/data/mail.yaml +39 -0
- package/hedhog/data/route.yaml +12 -0
- package/hedhog/data/setting_group.yaml +15 -1
- package/hedhog/frontend/app/account/components/regenerate-codes-dialog.tsx.ejs +11 -7
- package/hedhog/frontend/app/account/components/verify-before-add-dialog.tsx.ejs +9 -7
- package/hedhog/frontend/app/account/hooks/use-mfa-setup.ts.ejs +6 -2
- package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +130 -34
- package/hedhog/frontend/app/integration/profiles/page.tsx.ejs +16 -8
- package/hedhog/frontend/messages/en.json +55 -1
- package/hedhog/frontend/messages/pt.json +55 -1
- package/hedhog/table/webhook_integration.yaml +64 -43
- package/package.json +10 -5
- package/src/app-command/app-command.registry.ts +2 -0
- package/src/auth/auth.controller.ts +23 -1
- package/src/auth/auth.service.ts +135 -1
- package/src/auth/dto/complete-account.dto.ts +24 -0
- package/src/authorize/authorize.controller.ts +99 -0
- package/src/authorize/authorize.module.ts +23 -0
- package/src/authorize/authorize.service.spec.ts +307 -0
- package/src/authorize/authorize.service.ts +245 -0
- package/src/authorize/dto/authorize-info.dto.ts +23 -0
- package/src/authorize/dto/consent.dto.ts +23 -0
- package/src/authorize/dto/token.dto.ts +23 -0
- package/src/challenge/challenge.service.ts +28 -0
- package/src/core.module.ts +2 -0
- package/src/file/file.service.ts +19 -0
- package/src/index.ts +1 -0
- package/src/integration-profile/integration-profile.secrets.spec.ts +114 -0
- package/src/integration-profile/integration-profile.secrets.ts +14 -3
- package/src/integration-profile/integration-profile.service.ts +129 -3
- package/src/integration-profile/integration-profile.utils.spec.ts +75 -0
- package/src/integration-profile/integration-profile.utils.ts +79 -0
- package/src/oauth/interfaces/OAuthProvider.ts +5 -1
- package/src/oauth/oauth.controller.ts +89 -0
- package/src/oauth/oauth.service.ts +190 -8
- package/src/oauth/providers/microsoft-entra-id.provider.ts +45 -5
- package/src/profile/profile.controller.ts +6 -2
- package/src/profile/profile.service.ts +5 -3
- package/src/webhook-integration/webhook-integration.controller.ts +40 -2
- package/src/webhook-integration/webhook-integration.module.ts +5 -0
- package/src/webhook-integration/webhook-integration.service.ts +151 -2
- package/src/webhook-integration/webhook-protocol.registry.spec.ts +70 -0
- package/src/webhook-integration/webhook-protocol.registry.ts +93 -0
|
@@ -89,12 +89,17 @@ export class OAuthService {
|
|
|
89
89
|
this.providers.set(linkedin.getProviderType(), linkedin);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
async getAuthUrl(
|
|
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 {
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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,
|
|
@@ -2,6 +2,10 @@ import { NoRole, User } from '@hed-hog/api';
|
|
|
2
2
|
import { Locale } from '@hed-hog/api-locale';
|
|
3
3
|
import { Body, Controller, Delete, Get, Headers, Ip, Param, ParseIntPipe, Post, Put, UploadedFile, UseInterceptors } from '@nestjs/common';
|
|
4
4
|
import { FileInterceptor } from '@nestjs/platform-express';
|
|
5
|
+
import type {
|
|
6
|
+
PublicKeyCredentialCreationOptionsJSON,
|
|
7
|
+
PublicKeyCredentialRequestOptionsJSON,
|
|
8
|
+
} from '@simplewebauthn/server';
|
|
5
9
|
import { ChangeEmailDto } from './dto/change-email.dto';
|
|
6
10
|
import { ChangePasswordDto } from './dto/change-password.dto';
|
|
7
11
|
import { EmailVerificationDto } from './dto/email-verification-confirm.dto';
|
|
@@ -161,7 +165,7 @@ export class ProfileController {
|
|
|
161
165
|
@Body() { name }: GenerateWebAuthnRegistrationOptionsDto,
|
|
162
166
|
@Locale() locale: string,
|
|
163
167
|
@Headers('origin') origin: string
|
|
164
|
-
) {
|
|
168
|
+
): Promise<PublicKeyCredentialCreationOptionsJSON> {
|
|
165
169
|
return this.profileService.generateWebAuthnRegistrationOptions(locale, id, name, origin);
|
|
166
170
|
}
|
|
167
171
|
|
|
@@ -180,7 +184,7 @@ export class ProfileController {
|
|
|
180
184
|
@User() { id },
|
|
181
185
|
@Locale() locale: string,
|
|
182
186
|
@Headers('origin') origin: string
|
|
183
|
-
) {
|
|
187
|
+
): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
|
184
188
|
return this.profileService.generateWebAuthnAuthenticationOptions(locale, id, origin);
|
|
185
189
|
}
|
|
186
190
|
|
|
@@ -5,6 +5,8 @@ import { BadRequestException, forwardRef, Inject, Injectable, NotFoundException
|
|
|
5
5
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
|
6
6
|
import { JwtService } from '@nestjs/jwt';
|
|
7
7
|
import type {
|
|
8
|
+
PublicKeyCredentialCreationOptionsJSON,
|
|
9
|
+
PublicKeyCredentialRequestOptionsJSON,
|
|
8
10
|
VerifiedAuthenticationResponse,
|
|
9
11
|
VerifiedRegistrationResponse,
|
|
10
12
|
} from '@simplewebauthn/server';
|
|
@@ -1221,7 +1223,7 @@ export class ProfileService {
|
|
|
1221
1223
|
}
|
|
1222
1224
|
}
|
|
1223
1225
|
|
|
1224
|
-
async generateWebAuthnRegistrationOptions(locale: string, userId: number, name: string, requestOrigin?: string) {
|
|
1226
|
+
async generateWebAuthnRegistrationOptions(locale: string, userId: number, name: string, requestOrigin?: string): Promise<PublicKeyCredentialCreationOptionsJSON> {
|
|
1225
1227
|
try {
|
|
1226
1228
|
const user = await this.prisma.user.findFirst({
|
|
1227
1229
|
where: { id: userId },
|
|
@@ -1367,7 +1369,7 @@ export class ProfileService {
|
|
|
1367
1369
|
}
|
|
1368
1370
|
}
|
|
1369
1371
|
|
|
1370
|
-
async generateWebAuthnAuthenticationOptions(locale: string, userId: number, requestOrigin?: string) {
|
|
1372
|
+
async generateWebAuthnAuthenticationOptions(locale: string, userId: number, requestOrigin?: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
|
1371
1373
|
try {
|
|
1372
1374
|
const user = await this.prisma.user.findFirst({
|
|
1373
1375
|
where: { id: userId },
|
|
@@ -1500,7 +1502,7 @@ export class ProfileService {
|
|
|
1500
1502
|
}
|
|
1501
1503
|
}
|
|
1502
1504
|
|
|
1503
|
-
async generateWebAuthnAuthenticationOptionsForLogin(locale: string, mfaToken: string) {
|
|
1505
|
+
async generateWebAuthnAuthenticationOptionsForLogin(locale: string, mfaToken: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
|
1504
1506
|
try {
|
|
1505
1507
|
const decoded = this.jwt.decode(mfaToken) as any;
|
|
1506
1508
|
if (!decoded || !decoded.userId) {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
Res,
|
|
16
16
|
} from '@nestjs/common';
|
|
17
17
|
import { Request, Response } from 'express';
|
|
18
|
+
import { WebhookProtocolResponse } from './webhook-protocol.registry';
|
|
18
19
|
import { WebhookIntegrationService } from './webhook-integration.service';
|
|
19
20
|
import {
|
|
20
21
|
CreateWebhookActionDTO,
|
|
@@ -145,12 +146,24 @@ export class PublicWebhookController {
|
|
|
145
146
|
constructor(private readonly service: WebhookIntegrationService) {}
|
|
146
147
|
|
|
147
148
|
@Post(':uuid')
|
|
148
|
-
execute(
|
|
149
|
+
async execute(
|
|
149
150
|
@Param('uuid') uuid: string,
|
|
150
151
|
@Body() body: any,
|
|
152
|
+
@Query() query: any,
|
|
151
153
|
@Headers() headers: any,
|
|
152
154
|
@Req() req: Request,
|
|
155
|
+
@Res({ passthrough: true }) res: Response,
|
|
153
156
|
) {
|
|
157
|
+
const handled = await this.service.executeProtocol(
|
|
158
|
+
uuid,
|
|
159
|
+
'POST',
|
|
160
|
+
body,
|
|
161
|
+
query,
|
|
162
|
+
headers,
|
|
163
|
+
req,
|
|
164
|
+
);
|
|
165
|
+
if (handled) return sendProtocolResponse(res, handled);
|
|
166
|
+
|
|
154
167
|
return this.service.executePublic(uuid, body, headers, req);
|
|
155
168
|
}
|
|
156
169
|
|
|
@@ -158,15 +171,40 @@ export class PublicWebhookController {
|
|
|
158
171
|
* GET variant so a webhook can be triggered by a plain clickable link (e.g.
|
|
159
172
|
* approve/reject links sent by email/WhatsApp). Query params are used as the
|
|
160
173
|
* payload for action templates (`{{body.decision}}` ← `?decision=approve`).
|
|
174
|
+
*
|
|
175
|
+
* Webhooks with a protocol take a different path: for them a GET is the
|
|
176
|
+
* provider's verification handshake, not a trigger — it must not run actions.
|
|
161
177
|
*/
|
|
162
178
|
@Get(':uuid')
|
|
163
|
-
executeGet(
|
|
179
|
+
async executeGet(
|
|
164
180
|
@Param('uuid') uuid: string,
|
|
165
181
|
@Query() query: any,
|
|
166
182
|
@Headers() headers: any,
|
|
167
183
|
@Req() req: Request,
|
|
184
|
+
@Res({ passthrough: true }) res: Response,
|
|
168
185
|
) {
|
|
186
|
+
const handled = await this.service.executeProtocol(
|
|
187
|
+
uuid,
|
|
188
|
+
'GET',
|
|
189
|
+
query ?? {},
|
|
190
|
+
query,
|
|
191
|
+
headers,
|
|
192
|
+
req,
|
|
193
|
+
);
|
|
194
|
+
if (handled) return sendProtocolResponse(res, handled);
|
|
195
|
+
|
|
169
196
|
return this.service.executePublic(uuid, query ?? {}, headers, req);
|
|
170
197
|
}
|
|
171
198
|
}
|
|
172
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Aplica status/content-type que o adaptador pediu. Devolver a string crua importa:
|
|
202
|
+
* provedores como a Meta comparam o corpo da resposta literalmente, então um
|
|
203
|
+
* envelope JSON reprovaria a verificação.
|
|
204
|
+
*/
|
|
205
|
+
function sendProtocolResponse(res: Response, result: WebhookProtocolResponse) {
|
|
206
|
+
res.status(result.status);
|
|
207
|
+
if (result.contentType) res.type(result.contentType);
|
|
208
|
+
return result.body;
|
|
209
|
+
}
|
|
210
|
+
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
WebhookIntegrationController,
|
|
17
17
|
} from './webhook-integration.controller';
|
|
18
18
|
import { WebhookIntegrationService } from './webhook-integration.service';
|
|
19
|
+
import { WebhookProtocolRegistry } from './webhook-protocol.registry';
|
|
19
20
|
|
|
20
21
|
@Module({
|
|
21
22
|
imports: [
|
|
@@ -38,12 +39,16 @@ import { WebhookIntegrationService } from './webhook-integration.service';
|
|
|
38
39
|
IntegrationEventCatalogService,
|
|
39
40
|
WebhookIntegrationService,
|
|
40
41
|
EventWebhookService,
|
|
42
|
+
WebhookProtocolRegistry,
|
|
41
43
|
],
|
|
42
44
|
exports: [
|
|
43
45
|
IntegrationEventCatalogService,
|
|
44
46
|
AppCommandModule,
|
|
45
47
|
WebhookIntegrationService,
|
|
46
48
|
EventWebhookService,
|
|
49
|
+
// Exportado para que outros módulos registrem protocolos de entrada
|
|
50
|
+
// (ex.: a library whatsapp registra o protocolo da Meta).
|
|
51
|
+
WebhookProtocolRegistry,
|
|
47
52
|
],
|
|
48
53
|
})
|
|
49
54
|
export class WebhookIntegrationModule {}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
BadRequestException,
|
|
8
8
|
HttpException,
|
|
9
9
|
Injectable,
|
|
10
|
+
Logger,
|
|
10
11
|
NotFoundException,
|
|
11
12
|
} from '@nestjs/common';
|
|
12
13
|
import { EventEmitter } from 'events';
|
|
@@ -25,6 +26,11 @@ import {
|
|
|
25
26
|
UpdateWebhookActionDTO,
|
|
26
27
|
} from './dto/webhook-action.dto';
|
|
27
28
|
import { AppCommandRegistry } from '../app-command/app-command.registry';
|
|
29
|
+
import {
|
|
30
|
+
WebhookProtocolRegistry,
|
|
31
|
+
WebhookProtocolRequest,
|
|
32
|
+
WebhookProtocolResponse,
|
|
33
|
+
} from './webhook-protocol.registry';
|
|
28
34
|
import {
|
|
29
35
|
createPlainToken,
|
|
30
36
|
createPublicUuid,
|
|
@@ -58,6 +64,7 @@ type WebhookActionPayload =
|
|
|
58
64
|
|
|
59
65
|
@Injectable()
|
|
60
66
|
export class WebhookIntegrationService {
|
|
67
|
+
private readonly logger = new Logger(WebhookIntegrationService.name);
|
|
61
68
|
private readonly logEmitter = new EventEmitter();
|
|
62
69
|
private readonly webhookActionSelect = {
|
|
63
70
|
id: true,
|
|
@@ -105,6 +112,7 @@ export class WebhookIntegrationService {
|
|
|
105
112
|
private readonly settingService: SettingService,
|
|
106
113
|
private readonly commandRegistry: AppCommandRegistry,
|
|
107
114
|
private readonly mcpApiKeyService: McpApiKeyService,
|
|
115
|
+
private readonly protocolRegistry: WebhookProtocolRegistry,
|
|
108
116
|
) {}
|
|
109
117
|
|
|
110
118
|
private get integrations() {
|
|
@@ -551,6 +559,131 @@ export class WebhookIntegrationService {
|
|
|
551
559
|
});
|
|
552
560
|
}
|
|
553
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Cria (ou devolve) o webhook de um perfil de integração que fala um protocolo
|
|
564
|
+
* próprio. Idempotente: a chave é (protocol, integration_profile_id), então
|
|
565
|
+
* chamar de novo devolve o mesmo UUID — e a URL já registrada no painel do
|
|
566
|
+
* provedor continua válida.
|
|
567
|
+
*
|
|
568
|
+
* Existe para que módulos não precisem montar a URL pública por conta própria:
|
|
569
|
+
* `/webhook/:uuid` e a base vinda dos settings ficam sob responsabilidade do core.
|
|
570
|
+
*/
|
|
571
|
+
async provisionProtocolWebhook(input: {
|
|
572
|
+
slug: string;
|
|
573
|
+
name: string;
|
|
574
|
+
protocol: string;
|
|
575
|
+
integrationProfileId: number;
|
|
576
|
+
description?: string;
|
|
577
|
+
}) {
|
|
578
|
+
const existing = await this.integrations.findFirst({
|
|
579
|
+
where: {
|
|
580
|
+
protocol: input.protocol,
|
|
581
|
+
integration_profile_id: input.integrationProfileId,
|
|
582
|
+
},
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
if (existing) return this.withPublicUrl(existing);
|
|
586
|
+
|
|
587
|
+
const created = await this.integrations.create({
|
|
588
|
+
data: {
|
|
589
|
+
slug: await this.ensureProvisionSlug(input.slug),
|
|
590
|
+
name: input.name,
|
|
591
|
+
description: input.description ?? null,
|
|
592
|
+
protocol: input.protocol,
|
|
593
|
+
integration_profile_id: input.integrationProfileId,
|
|
594
|
+
public_uuid: createPublicUuid(),
|
|
595
|
+
status: 'active',
|
|
596
|
+
// Webhooks de protocolo não usam o token estático do core: o adaptador
|
|
597
|
+
// faz a própria autenticação (HMAC, no caso da Meta).
|
|
598
|
+
require_token: false,
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
this.logger.log(
|
|
603
|
+
`Provisioned "${input.protocol}" webhook ${created.id} for integration profile ${input.integrationProfileId}`,
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
return this.withPublicUrl(created);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** Sufixa o slug até achar um livre (o slug é unique na tabela). */
|
|
610
|
+
private async ensureProvisionSlug(base: string): Promise<string> {
|
|
611
|
+
const slug = base.trim();
|
|
612
|
+
if (!(await this.integrations.findUnique({ where: { slug } }))) return slug;
|
|
613
|
+
|
|
614
|
+
for (let i = 2; ; i++) {
|
|
615
|
+
const candidate = `${slug}-${i}`;
|
|
616
|
+
if (!(await this.integrations.findUnique({ where: { slug: candidate } }))) {
|
|
617
|
+
return candidate;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Entrega a chamada ao adaptador de protocolo do webhook, se houver um.
|
|
624
|
+
*
|
|
625
|
+
* Devolve `null` quando o webhook é clássico (protocol nulo) ou quando nenhum
|
|
626
|
+
* módulo registrou o protocolo — nesse caso o chamador segue pelo fluxo normal
|
|
627
|
+
* de token + actions.
|
|
628
|
+
*
|
|
629
|
+
* Webhooks com protocolo NÃO passam por validateToken/validateIp/validateBodySchema:
|
|
630
|
+
* o adaptador faz a própria autenticação (a Meta, por exemplo, assina com HMAC).
|
|
631
|
+
*/
|
|
632
|
+
async executeProtocol(
|
|
633
|
+
uuid: string,
|
|
634
|
+
method: 'GET' | 'POST',
|
|
635
|
+
body: any,
|
|
636
|
+
query: any,
|
|
637
|
+
headers: any,
|
|
638
|
+
req: Request,
|
|
639
|
+
): Promise<WebhookProtocolResponse | null> {
|
|
640
|
+
const integration = await this.integrations.findUnique({
|
|
641
|
+
where: { public_uuid: uuid },
|
|
642
|
+
select: {
|
|
643
|
+
id: true,
|
|
644
|
+
slug: true,
|
|
645
|
+
status: true,
|
|
646
|
+
protocol: true,
|
|
647
|
+
integration_profile_id: true,
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
if (!integration?.protocol) return null;
|
|
652
|
+
|
|
653
|
+
const adapter = this.protocolRegistry.get(integration.protocol);
|
|
654
|
+
if (!adapter) {
|
|
655
|
+
// Protocolo gravado mas nenhum módulo o registrou (library desabilitada,
|
|
656
|
+
// por exemplo). Não cai no fluxo de actions: seria autenticar com o token
|
|
657
|
+
// clássico um webhook que deveria exigir a auth do provedor.
|
|
658
|
+
this.logger.error(
|
|
659
|
+
`Webhook ${integration.id} declares protocol "${integration.protocol}", but no adapter is registered.`,
|
|
660
|
+
);
|
|
661
|
+
throw new NotFoundException('Webhook not found.');
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (integration.status !== 'active') {
|
|
665
|
+
throw new NotFoundException('Webhook not found.');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const request: WebhookProtocolRequest = {
|
|
669
|
+
integration: {
|
|
670
|
+
id: integration.id,
|
|
671
|
+
slug: integration.slug,
|
|
672
|
+
integration_profile_id: integration.integration_profile_id ?? null,
|
|
673
|
+
},
|
|
674
|
+
body,
|
|
675
|
+
query: (query ?? {}) as Record<string, string>,
|
|
676
|
+
headers: (headers ?? {}) as Record<string, string | string[] | undefined>,
|
|
677
|
+
rawBody: (req as Request & { rawBody?: Buffer }).rawBody,
|
|
678
|
+
remoteIp: this.getRemoteIp(req),
|
|
679
|
+
req,
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
return method === 'GET'
|
|
683
|
+
? adapter.verify(request)
|
|
684
|
+
: adapter.handle(request);
|
|
685
|
+
}
|
|
686
|
+
|
|
554
687
|
async executePublic(uuid: string, body: any, headers: any, req: Request) {
|
|
555
688
|
const startedAt = Date.now();
|
|
556
689
|
const remoteIp = this.getRemoteIp(req);
|
|
@@ -610,6 +743,10 @@ export class WebhookIntegrationService {
|
|
|
610
743
|
body,
|
|
611
744
|
remoteIp,
|
|
612
745
|
webhookHeaders: normalizeJsonObject(headers) || {},
|
|
746
|
+
// `rawBody` is stashed onto the request by the API's global body-parser `verify`
|
|
747
|
+
// callback (apps/api/src/main.ts); typed loosely here since this package doesn't
|
|
748
|
+
// own that Express.Request augmentation.
|
|
749
|
+
webhookRawBody: (req as Request & { rawBody?: Buffer }).rawBody,
|
|
613
750
|
startedAt,
|
|
614
751
|
});
|
|
615
752
|
}
|
|
@@ -625,10 +762,18 @@ export class WebhookIntegrationService {
|
|
|
625
762
|
body: any;
|
|
626
763
|
remoteIp: string | null;
|
|
627
764
|
webhookHeaders: Record<string, unknown>;
|
|
765
|
+
webhookRawBody?: Buffer;
|
|
628
766
|
startedAt: number;
|
|
629
767
|
}) {
|
|
630
|
-
const {
|
|
631
|
-
|
|
768
|
+
const {
|
|
769
|
+
integration,
|
|
770
|
+
uuid,
|
|
771
|
+
body,
|
|
772
|
+
remoteIp,
|
|
773
|
+
webhookHeaders,
|
|
774
|
+
webhookRawBody,
|
|
775
|
+
startedAt,
|
|
776
|
+
} = input;
|
|
632
777
|
const actionLogs: ActionExecutionLog[] = [];
|
|
633
778
|
|
|
634
779
|
try {
|
|
@@ -639,6 +784,7 @@ export class WebhookIntegrationService {
|
|
|
639
784
|
try {
|
|
640
785
|
const result = await this.executeAction(action, body, {
|
|
641
786
|
webhookHeaders,
|
|
787
|
+
webhookRawBody,
|
|
642
788
|
remoteIp,
|
|
643
789
|
publicUuid: uuid,
|
|
644
790
|
integrationId: integration.id,
|
|
@@ -811,6 +957,7 @@ export class WebhookIntegrationService {
|
|
|
811
957
|
body: any,
|
|
812
958
|
context?: {
|
|
813
959
|
webhookHeaders?: Record<string, unknown>;
|
|
960
|
+
webhookRawBody?: Buffer;
|
|
814
961
|
remoteIp?: string | null;
|
|
815
962
|
publicUuid?: string;
|
|
816
963
|
integrationId?: number;
|
|
@@ -1138,6 +1285,7 @@ export class WebhookIntegrationService {
|
|
|
1138
1285
|
body: any,
|
|
1139
1286
|
context?: {
|
|
1140
1287
|
webhookHeaders?: Record<string, unknown>;
|
|
1288
|
+
webhookRawBody?: Buffer;
|
|
1141
1289
|
remoteIp?: string | null;
|
|
1142
1290
|
publicUuid?: string;
|
|
1143
1291
|
integrationId?: number;
|
|
@@ -1172,6 +1320,7 @@ export class WebhookIntegrationService {
|
|
|
1172
1320
|
result = await this.commandRegistry.dispatch(slug, renderedParams, {
|
|
1173
1321
|
webhookBody: body,
|
|
1174
1322
|
webhookHeaders: context?.webhookHeaders,
|
|
1323
|
+
webhookRawBody: context?.webhookRawBody,
|
|
1175
1324
|
remoteIp: context?.remoteIp,
|
|
1176
1325
|
publicUuid: context?.publicUuid,
|
|
1177
1326
|
integrationId: context?.integrationId,
|