Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type {
OrganizationCreationAdvisorySeverity,
OrganizationCreationAdvisoryType,
OrganizationCreationDefaultsJSON,
OrganizationCreationDefaultsJSONSnapshot,
OrganizationCreationDefaultsResource,
} from '@clerk/shared/types';

import { BaseResource } from './internal';

export class OrganizationCreationDefaults extends BaseResource implements OrganizationCreationDefaultsResource {
advisory: {
code: OrganizationCreationAdvisoryType;
severity: OrganizationCreationAdvisorySeverity;
meta: Record<string, string>;
} | null = null;
form: {
name: string;
slug: string;
logo: string | null;
} = {
name: '',
slug: '',
logo: null,
};

public constructor(data: OrganizationCreationDefaultsJSON | OrganizationCreationDefaultsJSONSnapshot | null = null) {
super();
this.fromJSON(data);
}

protected fromJSON(data: OrganizationCreationDefaultsJSON | OrganizationCreationDefaultsJSONSnapshot | null): this {
if (!data) {
return this;
}

if (data.advisory) {
this.advisory = this.withDefault(data.advisory, this.advisory ?? null);
}

if (data.form) {
this.form.name = this.withDefault(data.form.name, this.form.name);
this.form.slug = this.withDefault(data.form.slug, this.form.slug);
this.form.logo = this.withDefault(data.form.logo, this.form.logo);
}

return this;
}

static async retrieve(): Promise<OrganizationCreationDefaultsResource> {
return await BaseResource._fetch({
path: '/me/organization_creation_defaults',
method: 'GET',
}).then(res => {
const data = res?.response as unknown as OrganizationCreationDefaultsJSON;
return new OrganizationCreationDefaults(data);
});
}
Comment on lines +50 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unsafe type casting bypasses validation and could cause runtime errors.

Line 55 uses as unknown as OrganizationCreationDefaultsJSON to force-cast the API response without any validation. If the API returns malformed data or the response structure changes, this will bypass type safety and could cause runtime errors when the resource properties are accessed.

🔎 Proposed fix with runtime validation
   static async retrieve(): Promise<OrganizationCreationDefaultsResource> {
     return await BaseResource._fetch({
       path: '/me/organization_creation_defaults',
       method: 'GET',
     }).then(res => {
-      const data = res?.response as unknown as OrganizationCreationDefaultsJSON;
+      const data = res?.response;
+      // Basic runtime validation
+      if (!data || typeof data !== 'object') {
+        throw new Error('Invalid organization creation defaults response');
+      }
       return new OrganizationCreationDefaults(data);
     });
   }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/OrganizationCreationDefaults.ts around
lines 50 to 58, the code force-casts the API response using "as unknown as
OrganizationCreationDefaultsJSON" which bypasses runtime validation; replace the
cast with a proper runtime check: extract res.response, validate required fields
and types (either with a small type-guard function or an existing JSON/schema
validator), throw or return a clear error if validation fails, and only then
construct and return new OrganizationCreationDefaults(validatedData); do not use
"as unknown as" to silence type checking.


public __internal_toSnapshot(): OrganizationCreationDefaultsJSONSnapshot {
return {
advisory: this.advisory
? {
code: this.advisory.code,
meta: this.advisory.meta,
severity: this.advisory.severity,
}
: null,
} as unknown as OrganizationCreationDefaultsJSONSnapshot;
}
}
12 changes: 12 additions & 0 deletions packages/clerk-js/src/core/resources/OrganizationSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe
} = {
disabled: false,
};
organizationCreationDefaults: {
enabled: boolean;
} = {
enabled: false,
};
enabled: boolean = false;
maxAllowedMemberships: number = 1;
forceOrganizationSelection!: boolean;
Expand Down Expand Up @@ -51,6 +56,13 @@ export class OrganizationSettings extends BaseResource implements OrganizationSe
this.slug.disabled = this.withDefault(data.slug.disabled, this.slug.disabled);
}

if (data.organization_creation_defaults) {
this.organizationCreationDefaults.enabled = this.withDefault(
data.organization_creation_defaults.enabled,
this.organizationCreationDefaults.enabled,
);
}

this.enabled = this.withDefault(data.enabled, this.enabled);
this.maxAllowedMemberships = this.withDefault(data.max_allowed_memberships, this.maxAllowedMemberships);
this.forceOrganizationSelection = this.withDefault(
Expand Down
3 changes: 3 additions & 0 deletions packages/clerk-js/src/core/resources/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
UserOrganizationInvitation,
Web3Wallet,
} from './internal';
import { OrganizationCreationDefaults } from './OrganizationCreationDefaults';

export class User extends BaseResource implements UserResource {
pathRoot = '/me';
Expand Down Expand Up @@ -275,6 +276,8 @@ export class User extends BaseResource implements UserResource {
getOrganizationMemberships: GetOrganizationMemberships = retrieveMembership =>
OrganizationMembership.retrieve(retrieveMembership);

getOrganizationCreationDefaults = () => OrganizationCreationDefaults.retrieve();

leaveOrganization = async (organizationId: string): Promise<DeletedObjectResource> => {
const json = (
await BaseResource._fetch<DeletedObjectJSON>({
Expand Down
4 changes: 4 additions & 0 deletions packages/clerk-js/src/test/fixture-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,9 @@ const createOrganizationSettingsFixtureHelpers = (environment: EnvironmentJSON)
const withOrganizationSlug = (enabled = false) => {
os.slug.disabled = !enabled;
};
const withOrganizationCreationDefaults = (enabled = false) => {
os.organization_creation_defaults.enabled = enabled;
};

const withOrganizationDomains = (modes?: OrganizationEnrollmentMode[], defaultRole?: string) => {
os.domains.enabled = true;
Expand All @@ -356,6 +359,7 @@ const createOrganizationSettingsFixtureHelpers = (environment: EnvironmentJSON)
withOrganizationDomains,
withForceOrganizationSelection,
withOrganizationSlug,
withOrganizationCreationDefaults,
};
};

Expand Down
4 changes: 4 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,10 @@ export const enUS: LocalizationResource = {
actionLink: 'Sign out',
actionText: 'Signed in as {{identifier}}',
},
alerts: {
existingOrgWithDomain:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will generate the other locale files once I get a first PR review, in case the localization key changes

'An organization already exists for the detected company name and {{email}}. Join by invitation.',
},
},
taskResetPassword: {
formButtonPrimary: 'Reset Password',
Expand Down
3 changes: 2 additions & 1 deletion packages/shared/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type * from './customPages';
export type * from './deletedObject';
export type * from './devtools';
export type * from './displayConfig';
export type * from './elementIds';
export type * from './emailAddress';
export type * from './enterpriseAccount';
export type * from './environment';
Expand All @@ -32,6 +33,7 @@ export type * from './localization';
export type * from './multiDomain';
export type * from './oauth';
export type * from './organization';
export type * from './organizationCreationDefaults';
export type * from './organizationDomain';
export type * from './organizationInvitation';
export type * from './organizationMembership';
Expand All @@ -49,7 +51,6 @@ export type * from './protectConfig';
export type * from './redirects';
export type * from './resource';
export type * from './role';
export type * from './elementIds';
export type * from './router';
/**
* TODO @revamp-hooks: Drop this in the next major release.
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1307,6 +1307,9 @@ export type __internal_LocalizationResource = {
title: LocalizationValue;
subtitle: LocalizationValue;
};
alerts: {
existingOrgWithDomain: LocalizationValue<'email'>;
};
};
taskResetPassword: {
title: LocalizationValue;
Expand Down
32 changes: 32 additions & 0 deletions packages/shared/src/types/organizationCreationDefaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { ClerkResourceJSON } from './json';
import type { ClerkResource } from './resource';

export type OrganizationCreationAdvisoryType = 'existing_org_with_domain';

export type OrganizationCreationAdvisorySeverity = 'warning';

export interface OrganizationCreationDefaultsJSON extends ClerkResourceJSON {
advisory: {
code: OrganizationCreationAdvisoryType;
severity: OrganizationCreationAdvisorySeverity;
meta: Record<string, string>;
} | null;
form: {
name: string;
slug: string;
logo: string | null;
};
}

export interface OrganizationCreationDefaultsResource extends ClerkResource {
advisory: {
code: OrganizationCreationAdvisoryType;
severity: OrganizationCreationAdvisorySeverity;
meta: Record<string, string>;
} | null;
form: {
name: string;
slug: string;
logo: string | null;
};
}
6 changes: 6 additions & 0 deletions packages/shared/src/types/organizationSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export interface OrganizationSettingsJSON extends ClerkResourceJSON {
slug: {
disabled: boolean;
};
organization_creation_defaults: {
enabled: boolean;
};
}

export interface OrganizationSettingsResource extends ClerkResource {
Expand All @@ -37,5 +40,8 @@ export interface OrganizationSettingsResource extends ClerkResource {
slug: {
disabled: boolean;
};
organizationCreationDefaults: {
enabled: boolean;
};
__internal_toSnapshot: () => OrganizationSettingsJSONSnapshot;
}
3 changes: 3 additions & 0 deletions packages/shared/src/types/snapshots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
VerificationJSON,
Web3WalletJSON,
} from './json';
import type { OrganizationCreationDefaultsJSON } from './organizationCreationDefaults';
import type { OrganizationSettingsJSON } from './organizationSettings';
import type { ProtectConfigJSON } from './protectConfig';
import type { SignInJSON } from './signIn';
Expand Down Expand Up @@ -143,6 +144,8 @@ export type OrganizationMembershipJSONSnapshot = OrganizationMembershipJSON;

export type OrganizationSettingsJSONSnapshot = OrganizationSettingsJSON;

export type OrganizationCreationDefaultsJSONSnapshot = OrganizationCreationDefaultsJSON;

export type PasskeyJSONSnapshot = Override<PasskeyJSON, { verification: VerificationJSONSnapshot | null }>;

export type PhoneNumberJSONSnapshot = Override<
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/types/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { ExternalAccountResource } from './externalAccount';
import type { ImageResource } from './image';
import type { UserJSON } from './json';
import type { OAuthScope } from './oauth';
import type { OrganizationCreationDefaultsResource } from './organizationCreationDefaults';
import type { OrganizationInvitationStatus } from './organizationInvitation';
import type { OrganizationMembershipResource } from './organizationMembership';
import type { OrganizationSuggestionResource, OrganizationSuggestionStatus } from './organizationSuggestion';
Expand Down Expand Up @@ -115,6 +116,7 @@ export interface UserResource extends ClerkResource, BillingPayerMethods {
getOrganizationSuggestions: (
params?: GetUserOrganizationSuggestionsParams,
) => Promise<ClerkPaginatedResponse<OrganizationSuggestionResource>>;
getOrganizationCreationDefaults: () => Promise<OrganizationCreationDefaultsResource>;
leaveOrganization: (organizationId: string) => Promise<DeletedObjectResource>;
createTOTP: () => Promise<TOTPResource>;
verifyTOTP: (params: VerifyTOTPParams) => Promise<TOTPResource>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
import { useOrganizationList } from '@clerk/shared/react';
import type { CreateOrganizationParams } from '@clerk/shared/types';
import type { CreateOrganizationParams, OrganizationCreationDefaultsResource } from '@clerk/shared/types';
import { useState } from 'react';

import { useEnvironment } from '@/ui/contexts';
import { useSessionTasksContext, useTaskChooseOrganizationContext } from '@/ui/contexts/components/SessionTasks';
import { localizationKeys } from '@/ui/customizables';
import { Icon, localizationKeys } from '@/ui/customizables';
import { useCardState } from '@/ui/elements/contexts';
import { Form } from '@/ui/elements/Form';
import { FormButtonContainer } from '@/ui/elements/FormButtons';
import { FormContainer } from '@/ui/elements/FormContainer';
import { Header } from '@/ui/elements/Header';
import { IconButton } from '@/ui/elements/IconButton';
import { Upload } from '@/ui/icons';
import { createSlug } from '@/ui/utils/createSlug';
import { handleError } from '@/ui/utils/errorHandler';
import { useFormControl } from '@/ui/utils/useFormControl';

import { OrganizationProfileAvatarUploader } from '../../../OrganizationProfile/OrganizationProfileAvatarUploader';
import { organizationListParams } from '../../../OrganizationSwitcher/utils';
import { OrganizationCreationDefaultsAlert } from './OrganizationCreationDefaultsAlert';

type CreateOrganizationScreenProps = {
onCancel?: () => void;
organizationCreationDefaults?: OrganizationCreationDefaultsResource;
};

export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) => {
Expand All @@ -27,13 +33,14 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
userMemberships: organizationListParams.userMemberships,
});
const { organizationSettings } = useEnvironment();
const [file, setFile] = useState<File | null>();

const nameField = useFormControl('name', '', {
const nameField = useFormControl('name', props.organizationCreationDefaults?.form.name ?? '', {
type: 'text',
label: localizationKeys('taskChooseOrganization.createOrganization.formFieldLabel__name'),
placeholder: localizationKeys('taskChooseOrganization.createOrganization.formFieldInputPlaceholder__name'),
});
const slugField = useFormControl('slug', '', {
const slugField = useFormControl('slug', props.organizationCreationDefaults?.form.slug ?? '', {
type: 'text',
label: localizationKeys('taskChooseOrganization.createOrganization.formFieldLabel__slug'),
placeholder: localizationKeys('taskChooseOrganization.createOrganization.formFieldInputPlaceholder__slug'),
Expand All @@ -57,6 +64,15 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =

const organization = await createOrganization(createOrgParams);

if (file) {
await organization.setLogo({ file });
} else if (defaultLogoUrl) {
const response = await fetch(defaultLogoUrl);
const blob = await response.blob();
const logoFile = new File([blob], 'logo', { type: blob.type });
await organization.setLogo({ file: logoFile });
}
Comment on lines +67 to +74
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing error handling for logo fetch could break organization creation.

The fetch and blob conversion for defaultLogoUrl (lines 69-74) has no error handling. If the network request fails, the response is not a valid image, or blob conversion fails, it will throw an unhandled exception and prevent the organization from being created.

🔎 Proposed fix with error handling
       const organization = await createOrganization(createOrgParams);

       if (file) {
         await organization.setLogo({ file });
       } else if (defaultLogoUrl) {
-        const response = await fetch(defaultLogoUrl);
-        const blob = await response.blob();
-        const logoFile = new File([blob], 'logo', { type: blob.type });
-        await organization.setLogo({ file: logoFile });
+        try {
+          const response = await fetch(defaultLogoUrl);
+          if (!response.ok) {
+            throw new Error('Failed to fetch logo');
+          }
+          const blob = await response.blob();
+          const logoFile = new File([blob], 'logo', { type: blob.type });
+          await organization.setLogo({ file: logoFile });
+        } catch (err) {
+          // Log error but don't block organization creation
+          console.error('Failed to set default logo:', err);
+        }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (file) {
await organization.setLogo({ file });
} else if (defaultLogoUrl) {
const response = await fetch(defaultLogoUrl);
const blob = await response.blob();
const logoFile = new File([blob], 'logo', { type: blob.type });
await organization.setLogo({ file: logoFile });
}
if (file) {
await organization.setLogo({ file });
} else if (defaultLogoUrl) {
try {
const response = await fetch(defaultLogoUrl);
if (!response.ok) {
throw new Error('Failed to fetch logo');
}
const blob = await response.blob();
const logoFile = new File([blob], 'logo', { type: blob.type });
await organization.setLogo({ file: logoFile });
} catch (err) {
// Log error but don't block organization creation
console.error('Failed to set default logo:', err);
}
}
🤖 Prompt for AI Agents
In
packages/ui/src/components/SessionTasks/tasks/TaskChooseOrganization/CreateOrganizationScreen.tsx
around lines 67 to 74, the code fetches and converts defaultLogoUrl to a File
without any error handling which can throw and abort organization creation; wrap
the fetch/blob/file creation in a try/catch, check response.ok before calling
response.blob(), and only call organization.setLogo when the blob->File
succeeds; on error, log the error (or surface a non-blocking UI message) and
continue creating the organization without a logo so a failed image fetch does
not stop the flow.


await setActive({
organization,
navigate: async ({ session }) => {
Expand All @@ -77,7 +93,13 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
slugField.setValue(val);
};

const onAvatarRemove = () => {
card.setIdle();
return setFile(null);
};

const isSubmitButtonDisabled = !nameField.value || !isLoaded;
const defaultLogoUrl = file === undefined ? props.organizationCreationDefaults?.form.logo : undefined;

return (
<>
Expand All @@ -88,8 +110,46 @@ export const CreateOrganizationScreen = (props: CreateOrganizationScreenProps) =
<Header.Title localizationKey={localizationKeys('taskChooseOrganization.createOrganization.title')} />
<Header.Subtitle localizationKey={localizationKeys('taskChooseOrganization.createOrganization.subtitle')} />
</Header.Root>

<FormContainer sx={t => ({ padding: `${t.space.$none} ${t.space.$10} ${t.space.$8}` })}>
<Form.Root onSubmit={onSubmit}>
<OrganizationCreationDefaultsAlert organizationCreationDefaults={props.organizationCreationDefaults} />
<OrganizationProfileAvatarUploader
organization={{ name: nameField.value, imageUrl: defaultLogoUrl ?? undefined }}
onAvatarChange={async file => await setFile(file)}
onAvatarRemove={file || defaultLogoUrl ? onAvatarRemove : null}
avatarPreviewPlaceholder={
<IconButton
variant='ghost'
aria-label='Upload organization logo'
icon={
<Icon
size='md'
icon={Upload}
sx={t => ({
color: t.colors.$colorMutedForeground,
transitionDuration: t.transitionDuration.$controls,
})}
/>
}
sx={t => ({
width: t.sizes.$16,
height: t.sizes.$16,
borderRadius: t.radii.$md,
borderWidth: t.borderWidths.$normal,
borderStyle: t.borderStyles.$dashed,
borderColor: t.colors.$borderAlpha200,
backgroundColor: t.colors.$neutralAlpha50,
':hover': {
backgroundColor: t.colors.$neutralAlpha50,
svg: {
transform: 'scale(1.2)',
},
},
})}
/>
}
/>
<Form.ControlRow elementId={nameField.id}>
<Form.PlainInput
{...nameField.props}
Expand Down
Loading