From c30746e2d451e09ac54e2504f374a8a7525756ca Mon Sep 17 00:00:00 2001 From: Theo Zourzouvillys Date: Thu, 13 Aug 2026 16:21:19 -0800 Subject: [PATCH 1/2] fix(ui): populate enterprise SSO redirect urls when continuing a sign-up --- .../signup-enterprise-sso-redirect-urls.md | 10 ++++ .../__tests__/completeSignUpFlow.test.ts | 27 ++++++++++ .../internal/clerk-js/completeSignUpFlow.ts | 11 +++- packages/ui/src/common/EmailLinkVerify.tsx | 6 ++- packages/ui/src/components/SignIn/index.tsx | 1 + .../src/components/SignUp/SignUpContinue.tsx | 26 ++-------- .../components/SignUp/SignUpEmailLinkCard.tsx | 16 +----- .../components/SignUp/SignUpProtectCheck.tsx | 18 ++----- .../ui/src/components/SignUp/SignUpStart.tsx | 36 ++----------- .../SignUp/SignUpVerificationCodeForm.tsx | 15 ++---- .../__tests__/SignUpProtectCheck.test.tsx | 26 ++++++++++ packages/ui/src/components/SignUp/index.tsx | 1 + .../SignUp/useCompleteSignUpFlow.ts | 52 +++++++++++++++++++ 13 files changed, 145 insertions(+), 100 deletions(-) create mode 100644 .changeset/signup-enterprise-sso-redirect-urls.md create mode 100644 packages/ui/src/components/SignUp/useCompleteSignUpFlow.ts diff --git a/.changeset/signup-enterprise-sso-redirect-urls.md b/.changeset/signup-enterprise-sso-redirect-urls.md new file mode 100644 index 00000000000..4905b830f27 --- /dev/null +++ b/.changeset/signup-enterprise-sso-redirect-urls.md @@ -0,0 +1,10 @@ +--- +'@clerk/shared': patch +'@clerk/ui': patch +--- + +Fix sign-ups that continue into an enterprise SSO connection failing with `invalid_redirect_url` ("Redirect url invalid") instead of redirecting to the identity provider. + +A sign-up does not always know it requires `enterprise_sso` when the form is first submitted — the requirement appears once the identity behind the sign-up is resolved, which can happen several steps later. Whichever step was active at that point performed the hand-off to the identity provider, and most of them did so without the redirect URLs it requires, so the request was rejected and the sign-up dead-ended with no way to continue. Retrying reproduced it every time. Flows that reached SSO directly from the first sign-up form were unaffected, which is why this only showed up on some sign-ups. + +The redirect URLs are now derived from the sign-up context wherever the flow continues, so the hand-off works from every step: the continue form, email-link and code verification, and the verification step that precedes them. diff --git a/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts b/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts index df15c09b583..28ed2bbcbf7 100644 --- a/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts +++ b/packages/shared/src/internal/clerk-js/__tests__/completeSignUpFlow.test.ts @@ -239,6 +239,33 @@ describe('completeSignUpFlow', () => { }); }); + it.each([ + ['redirectUrl is missing', { redirectUrlComplete: 'https://example.com/done' }], + ['redirectUrlComplete is missing', { redirectUrl: 'https://example.com/acs' }], + ['both are missing', {}], + ['redirectUrl is empty', { redirectUrl: '', redirectUrlComplete: 'https://example.com/done' }], + ['redirectUrlComplete is empty', { redirectUrl: 'https://example.com/acs', redirectUrlComplete: '' }], + ])('throws rather than starting an Enterprise SSO flow when %s', (_label, urls) => { + const mockSignUp = { + status: 'missing_requirements', + missingFields: ['enterprise_sso'], + authenticateWithRedirect: mockAuthenticateWithRedirect, + } as unknown as SignUpResource; + + expect(() => + completeSignUpFlow({ + signUp: mockSignUp, + handleComplete: mockHandleComplete, + navigate: mockNavigate, + ...urls, + }), + ).toThrow(/redirectUrl/); + + expect(mockAuthenticateWithRedirect).not.toHaveBeenCalled(); + expect(mockNavigate).not.toHaveBeenCalled(); + expect(mockHandleComplete).not.toHaveBeenCalled(); + }); + it('forwards clerk ticket and status query params when navigating to verify email', async () => { const mockSignUp = { status: 'missing_requirements', diff --git a/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts b/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts index d2bae881512..727ac9b8e9b 100644 --- a/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts +++ b/packages/shared/src/internal/clerk-js/completeSignUpFlow.ts @@ -22,8 +22,8 @@ export const completeSignUpFlow = ({ continuePath, navigate, handleComplete, - redirectUrl = '', - redirectUrlComplete = '', + redirectUrl, + redirectUrlComplete, oidcPrompt, }: CompleteSignUpFlowProps): Promise | undefined => { if (signUp.status === 'complete') { @@ -32,6 +32,13 @@ export const completeSignUpFlow = ({ return handleComplete && handleComplete(); } else if (signUp.status === 'missing_requirements') { if (signUp.missingFields.some(mf => mf === 'enterprise_sso')) { + // FAPI rejects an empty redirect url, which reaches the user as a dead end rather than the caller as a bug. + if (!redirectUrl || !redirectUrlComplete) { + throw new Error( + 'completeSignUpFlow: `redirectUrl` and `redirectUrlComplete` are required to continue a sign-up that is missing `enterprise_sso`.', + ); + } + return signUp.authenticateWithRedirect({ strategy: 'enterprise_sso', redirectUrl, diff --git a/packages/ui/src/common/EmailLinkVerify.tsx b/packages/ui/src/common/EmailLinkVerify.tsx index 411aa31ada6..fa624460eaf 100644 --- a/packages/ui/src/common/EmailLinkVerify.tsx +++ b/packages/ui/src/common/EmailLinkVerify.tsx @@ -14,6 +14,8 @@ import { EmailLinkStatusCard } from './EmailLinkStatusCard'; export type EmailLinkVerifyProps = { redirectUrlComplete?: string; redirectUrl?: string; + /** SSO callback url, required only when the verified sign-up still has to hand off to an enterprise connection. */ + ssoCallbackUrl?: string; verifyEmailPath?: string; verifyPhonePath?: string; continuePath?: string; @@ -21,7 +23,7 @@ export type EmailLinkVerifyProps = { }; export const EmailLinkVerify = (props: EmailLinkVerifyProps) => { - const { redirectUrl, redirectUrlComplete, verifyEmailPath, verifyPhonePath, continuePath } = props; + const { redirectUrl, redirectUrlComplete, ssoCallbackUrl, verifyEmailPath, verifyPhonePath, continuePath } = props; const { handleEmailLinkVerification } = useClerk(); const { navigate } = useRouter(); const signUp = useCoreSignUp(); @@ -50,6 +52,8 @@ export const EmailLinkVerify = (props: EmailLinkVerifyProps) => { protectCheckPath: '../protect-check', continuePath, navigate, + redirectUrl: ssoCallbackUrl, + redirectUrlComplete: redirectUrlComplete || '/', }); } catch (err: any) { if ( diff --git a/packages/ui/src/components/SignIn/index.tsx b/packages/ui/src/components/SignIn/index.tsx index 6cea9742adb..46b784ceede 100644 --- a/packages/ui/src/components/SignIn/index.tsx +++ b/packages/ui/src/components/SignIn/index.tsx @@ -117,6 +117,7 @@ function SignInRoutes(): JSX.Element { ( getInitialActiveIdentifier(attributes, userSettings.signUp.progressive), ); - const ctx = useSignUpContext(); + const completeSignUpFlow = useCompleteSignUpFlow(); // TODO: This form should be shared between SignUpStart and SignUpContinue const formState = { @@ -181,18 +173,6 @@ function SignUpContinueInternal() { verifyEmailPath: './verify-email-address', verifyPhonePath: './verify-phone-number', protectCheckPath: '../protect-check', - handleComplete: () => { - removeClerkQueryParam('__clerk_ticket'); - removeClerkQueryParam('__clerk_invitation_token'); - return clerk.setActive({ - session: res.createdSessionId, - navigate: async ({ session, decorateUrl }) => { - await navigateOnSetActive({ session, redirectUrl: afterSignUpUrl, decorateUrl }); - }, - }); - }, - navigate, - oidcPrompt: ctx.oidcPrompt, }), ) .catch(err => handleError(err, fieldsToSubmit, card.setError)) diff --git a/packages/ui/src/components/SignUp/SignUpEmailLinkCard.tsx b/packages/ui/src/components/SignUp/SignUpEmailLinkCard.tsx index 6d905da85db..34a9767cf40 100644 --- a/packages/ui/src/components/SignUp/SignUpEmailLinkCard.tsx +++ b/packages/ui/src/components/SignUp/SignUpEmailLinkCard.tsx @@ -1,4 +1,3 @@ -import { useClerk } from '@clerk/shared/react'; import type { SignUpResource } from '@clerk/shared/types'; import React from 'react'; @@ -10,17 +9,14 @@ import { useCoreSignUp, useSignUpContext } from '../../contexts'; import { Flow, localizationKeys, useLocalizations } from '../../customizables'; import { useCardState } from '../../elements/contexts'; import { useEmailLink } from '../../hooks/useEmailLink'; -import { useRouter } from '../../router'; -import { completeSignUpFlow } from './util'; +import { useCompleteSignUpFlow } from './useCompleteSignUpFlow'; export const SignUpEmailLinkCard = () => { const { t } = useLocalizations(); const signUp = useCoreSignUp(); const signUpContext = useSignUpContext(); - const { afterSignUpUrl, navigateOnSetActive } = signUpContext; const card = useCardState(); - const { navigate } = useRouter(); - const { setActive } = useClerk(); + const completeSignUpFlow = useCompleteSignUpFlow(); const [showVerifyModal, setShowVerifyModal] = React.useState(false); const { startEmailLinkFlow, cancelEmailLinkFlow } = useEmailLink(signUp); @@ -57,14 +53,6 @@ export const SignUpEmailLinkCard = () => { verifyEmailPath: '../verify-email-address', verifyPhonePath: '../verify-phone-number', protectCheckPath: '../protect-check', - handleComplete: () => - setActive({ - session: su.createdSessionId, - navigate: async ({ session, decorateUrl }) => { - await navigateOnSetActive({ session, redirectUrl: afterSignUpUrl, decorateUrl }); - }, - }), - navigate, }); } }; diff --git a/packages/ui/src/components/SignUp/SignUpProtectCheck.tsx b/packages/ui/src/components/SignUp/SignUpProtectCheck.tsx index 1b883af8547..4356435394f 100644 --- a/packages/ui/src/components/SignUp/SignUpProtectCheck.tsx +++ b/packages/ui/src/components/SignUp/SignUpProtectCheck.tsx @@ -1,4 +1,3 @@ -import { useClerk } from '@clerk/shared/react'; import type { SignUpProps, SignUpResource } from '@clerk/shared/types'; import { type ComponentType, useEffect, useRef, useState } from 'react'; @@ -7,7 +6,7 @@ import { useCardState, withCardStateProvider } from '@/ui/elements/contexts'; import { Header } from '@/ui/elements/Header'; import { withRedirectToAfterSignUp } from '../../common'; -import { useCoreSignUp, useSignUpContext } from '../../contexts'; +import { useCoreSignUp } from '../../contexts'; import { Box, Button, @@ -22,8 +21,7 @@ import { import { useSpinDelay } from '../../hooks'; import { useNavigateToFlowStart } from '../../hooks/useNavigateToFlowStart'; import { useProtectCheckRunner } from '../../hooks/useProtectCheckRunner'; -import { useRouter } from '../../router'; -import { completeSignUpFlow } from './util'; +import { useCompleteSignUpFlow } from './useCompleteSignUpFlow'; /** * Continuation paths default to the standalone `/sign-up/protect-check` mount. When the card is @@ -48,10 +46,8 @@ function SignUpProtectCheckInternal({ const card = useCardState(); const { t } = useLocalizations(); const signUp = useCoreSignUp(); - const { navigate } = useRouter(); const { navigateToFlowStart } = useNavigateToFlowStart(); - const { setActive } = useClerk(); - const { afterSignUpUrl, navigateOnSetActive } = useSignUpContext(); + const completeSignUpFlow = useCompleteSignUpFlow(); // Latches that a protect check existed at some point, so the resolution race // (submitProtectCheck clearing protectCheck mid-navigation) isn't mistaken for // a stale visit. State adjusted during render (guarded) rather than a ref @@ -89,14 +85,6 @@ function SignUpProtectCheckInternal({ verifyPhonePath, protectCheckPath, // Defaults to '.' so a chained challenge re-runs this same route continuePath, - handleComplete: () => - setActive({ - session: updatedSignUp.createdSessionId, - navigate: async ({ session, decorateUrl }) => { - await navigateOnSetActive({ session, redirectUrl: afterSignUpUrl, decorateUrl }); - }, - }), - navigate, }); }, }); diff --git a/packages/ui/src/components/SignUp/SignUpStart.tsx b/packages/ui/src/components/SignUp/SignUpStart.tsx index 3b2e4a125c6..e146fd0341d 100644 --- a/packages/ui/src/components/SignUp/SignUpStart.tsx +++ b/packages/ui/src/components/SignUp/SignUpStart.tsx @@ -30,7 +30,7 @@ import { determineActiveFields, emailOrPhone, getInitialActiveIdentifier, showFo import { SignUpRestrictedAccess } from './SignUpRestrictedAccess'; import { SignUpSocialButtons } from './SignUpSocialButtons'; import { SignUpStartAlternativePhoneCodePhoneNumberCard } from './SignUpStartAlternativePhoneCodePhoneNumberCard'; -import { completeSignUpFlow } from './util'; +import { useCompleteSignUpFlow } from './useCompleteSignUpFlow'; function SignUpStartInternal(): JSX.Element { const card = useCardState(); @@ -41,10 +41,10 @@ function SignUpStartInternal(): JSX.Element { const { userSettings, authConfig } = useEnvironment(); const { navigate } = useRouter(); const { attributes } = userSettings; - const { setActive } = useClerk(); const ctx = useSignUpContext(); const isWithinSignInContext = !!React.useContext(SignInContext); - const { afterSignUpUrl, signInUrl, unsafeMetadata, navigateOnSetActive } = ctx; + const { signInUrl, unsafeMetadata } = ctx; + const completeSignUpFlow = useCompleteSignUpFlow(); const isCombinedFlow = !!(ctx.isCombinedFlow && !!isWithinSignInContext); const [activeCommIdentifierType, setActiveCommIdentifierType] = React.useState(() => getInitialActiveIdentifier(attributes, userSettings.signUp.progressive, { @@ -131,7 +131,6 @@ function SignUpStartInternal(): JSX.Element { const hasEmail = !!formState.emailAddress.value; const isProgressiveSignUp = userSettings.signUp.progressive; const isLegalConsentEnabled = userSettings.signUp.legal_consent_enabled; - const oidcPrompt = ctx.oidcPrompt; const fields = determineActiveFields({ attributes, @@ -158,27 +157,12 @@ function SignUpStartInternal(): JSX.Element { setMissingRequirementsWithTicket(true); } - const redirectUrl = ctx.ssoCallbackUrl; - const redirectUrlComplete = ctx.afterSignUpUrl || '/'; - return completeSignUpFlow({ signUp, - redirectUrl, - redirectUrlComplete, verifyEmailPath: 'verify-email-address', verifyPhonePath: 'verify-phone-number', protectCheckPath: 'protect-check', continuePath: 'continue', - handleComplete: () => { - return setActive({ - session: signUp.createdSessionId, - navigate: async ({ session, decorateUrl }) => { - await navigateOnSetActive({ session, redirectUrl: afterSignUpUrl, decorateUrl }); - }, - }); - }, - navigate, - oidcPrompt, }); }) .catch(err => { @@ -339,9 +323,6 @@ function SignUpStartInternal(): JSX.Element { card.setLoading(); card.setError(undefined); - const redirectUrl = ctx.ssoCallbackUrl; - const redirectUrlComplete = ctx.afterSignUpUrl || '/'; - let signUpAttempt: Promise; if (!fields.ticket && !hasExistingSignUpWithTicket) { signUpAttempt = signUp.create(buildRequest(fieldsToSubmit)); @@ -356,17 +337,6 @@ function SignUpStartInternal(): JSX.Element { verifyEmailPath: 'verify-email-address', verifyPhonePath: 'verify-phone-number', protectCheckPath: 'protect-check', - handleComplete: () => - setActive({ - session: res.createdSessionId, - navigate: async ({ session, decorateUrl }) => { - await navigateOnSetActive({ session, redirectUrl: afterSignUpUrl, decorateUrl }); - }, - }), - navigate, - redirectUrl, - redirectUrlComplete, - oidcPrompt, }), ) .catch(err => { diff --git a/packages/ui/src/components/SignUp/SignUpVerificationCodeForm.tsx b/packages/ui/src/components/SignUp/SignUpVerificationCodeForm.tsx index 5bf9893fd83..a91e89f6278 100644 --- a/packages/ui/src/components/SignUp/SignUpVerificationCodeForm.tsx +++ b/packages/ui/src/components/SignUp/SignUpVerificationCodeForm.tsx @@ -1,5 +1,4 @@ import { forwardClerkQueryParams } from '@clerk/shared/internal/clerk-js/queryParams'; -import { useClerk } from '@clerk/shared/react'; import type { SignUpResource } from '@clerk/shared/types'; import React from 'react'; @@ -9,7 +8,7 @@ import { VerificationCodeCard } from '@/ui/elements/VerificationCodeCard'; import { SignInContext, useSignUpContext } from '../../contexts'; import type { LocalizationKey } from '../../customizables'; import { useRouter } from '../../router'; -import { completeSignUpFlow } from './util'; +import { useCompleteSignUpFlow } from './useCompleteSignUpFlow'; type SignInFactorOneCodeFormProps = { cardTitle: LocalizationKey; @@ -26,9 +25,9 @@ type SignInFactorOneCodeFormProps = { }; export const SignUpVerificationCodeForm = (props: SignInFactorOneCodeFormProps) => { - const { afterSignUpUrl, navigateOnSetActive, isCombinedFlow: _isCombinedFlow } = useSignUpContext(); - const { setActive } = useClerk(); + const { isCombinedFlow: _isCombinedFlow } = useSignUpContext(); const { navigate } = useRouter(); + const completeSignUpFlow = useCompleteSignUpFlow(); const isWithinSignInContext = !!React.useContext(SignInContext); const isCombinedFlow = !!(isWithinSignInContext && _isCombinedFlow); @@ -49,14 +48,6 @@ export const SignUpVerificationCodeForm = (props: SignInFactorOneCodeFormProps) verifyPhonePath: '../verify-phone-number', protectCheckPath: '../protect-check', continuePath: '../continue', - handleComplete: () => - setActive({ - session: res.createdSessionId, - navigate: async ({ session, decorateUrl }) => { - await navigateOnSetActive({ session, redirectUrl: afterSignUpUrl, decorateUrl }); - }, - }), - navigate, }); }) .catch(err => { diff --git a/packages/ui/src/components/SignUp/__tests__/SignUpProtectCheck.test.tsx b/packages/ui/src/components/SignUp/__tests__/SignUpProtectCheck.test.tsx index 00e891c7853..c07bc6ce1c1 100644 --- a/packages/ui/src/components/SignUp/__tests__/SignUpProtectCheck.test.tsx +++ b/packages/ui/src/components/SignUp/__tests__/SignUpProtectCheck.test.tsx @@ -306,6 +306,32 @@ describe('SignUpProtectCheck', () => { }); }); + it('continues an enterprise SSO sign-up with populated redirect urls', async () => { + const { wrapper, fixtures } = await createFixtures(f => { + f.startSignUpWithProtectCheck(); + }); + mockExecute.mockResolvedValue('proof-abc'); + const authenticateWithRedirect = vi.fn().mockResolvedValue(undefined); + // Resolving the check is what first surfaces `enterprise_sso`: the identity is only known + // once the challenge is out of the way, so this route owns the SSO hand-off. + fixtures.signUp.submitProtectCheck.mockResolvedValue({ + status: 'missing_requirements', + missingFields: ['enterprise_sso'], + protectCheck: null, + authenticateWithRedirect, + } as unknown as SignUpResource); + + render(, { wrapper }); + + await waitFor(() => expect(authenticateWithRedirect).toHaveBeenCalled()); + + const params = authenticateWithRedirect.mock.calls[0][0]; + expect(params.strategy).toBe('enterprise_sso'); + expect(params.continueSignUp).toBe(true); + expect(params.redirectUrl).toBeTruthy(); + expect(params.redirectUrlComplete).toBeTruthy(); + }); + it('shows a retry control after a failure and re-runs the challenge when clicked', async () => { const { wrapper, fixtures } = await createFixtures(f => { f.startSignUpWithProtectCheck(); diff --git a/packages/ui/src/components/SignUp/index.tsx b/packages/ui/src/components/SignUp/index.tsx index a6c72b24b2c..ff68b5e1fe7 100644 --- a/packages/ui/src/components/SignUp/index.tsx +++ b/packages/ui/src/components/SignUp/index.tsx @@ -71,6 +71,7 @@ function SignUpRoutes(): JSX.Element { diff --git a/packages/ui/src/components/SignUp/useCompleteSignUpFlow.ts b/packages/ui/src/components/SignUp/useCompleteSignUpFlow.ts new file mode 100644 index 00000000000..55851d15b24 --- /dev/null +++ b/packages/ui/src/components/SignUp/useCompleteSignUpFlow.ts @@ -0,0 +1,52 @@ +import { useClerk } from '@clerk/shared/react'; +import type { SignUpResource } from '@clerk/shared/types'; +import { useCallback } from 'react'; + +import { useSignUpContext } from '../../contexts'; +import { useRouter } from '../../router'; +import { completeSignUpFlow } from './util'; + +type CompleteSignUpFlowParams = { + signUp: SignUpResource; + verifyEmailPath?: string; + verifyPhonePath?: string; + protectCheckPath?: string; + continuePath?: string; + handleComplete?: () => Promise; +}; + +/** + * Binds the sign-up context to `completeSignUpFlow` so callers only pass the routing paths. + * + * `redirectUrl`, `redirectUrlComplete` and `oidcPrompt` are needed by the `enterprise_sso` branch + * but are derived identically at every call site, so binding them here is what keeps a caller from + * silently omitting them. + */ +export const useCompleteSignUpFlow = () => { + const { navigate } = useRouter(); + const { setActive } = useClerk(); + const ctx = useSignUpContext(); + const { afterSignUpUrl, ssoCallbackUrl, oidcPrompt, navigateOnSetActive } = ctx; + + return useCallback( + ({ signUp, handleComplete, ...paths }: CompleteSignUpFlowParams) => + completeSignUpFlow({ + ...paths, + signUp, + navigate, + redirectUrl: ssoCallbackUrl, + redirectUrlComplete: afterSignUpUrl || '/', + oidcPrompt, + handleComplete: + handleComplete ?? + (() => + setActive({ + session: signUp.createdSessionId, + navigate: async ({ session, decorateUrl }) => { + await navigateOnSetActive({ session, redirectUrl: afterSignUpUrl, decorateUrl }); + }, + })), + }), + [navigate, setActive, afterSignUpUrl, ssoCallbackUrl, oidcPrompt, navigateOnSetActive], + ); +}; From 32465597e975839796fa76261a1b40ceed10f23f Mon Sep 17 00:00:00 2001 From: Theo Zourzouvillys Date: Thu, 13 Aug 2026 17:13:32 -0800 Subject: [PATCH 2/2] chore(ui): raise ui-common bundlewatch budget --- packages/ui/bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/bundlewatch.config.json b/packages/ui/bundlewatch.config.json index 3b818b7738c..c00fda564a6 100644 --- a/packages/ui/bundlewatch.config.json +++ b/packages/ui/bundlewatch.config.json @@ -5,7 +5,7 @@ { "path": "./dist/ui.shared.browser.js", "maxSize": "42KB" }, { "path": "./dist/framework*.js", "maxSize": "44KB" }, { "path": "./dist/vendors*.js", "maxSize": "73KB" }, - { "path": "./dist/ui-common*.js", "maxSize": "132KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "134KB" }, { "path": "./dist/signin*.js", "maxSize": "17KB" }, { "path": "./dist/signup*.js", "maxSize": "13KB" }, { "path": "./dist/userprofile*.js", "maxSize": "16KB" },