diff --git a/src/lib/schemas/email.test.ts b/src/lib/schemas/email.test.ts index 4979a76a3e..d1268a9fd1 100644 --- a/src/lib/schemas/email.test.ts +++ b/src/lib/schemas/email.test.ts @@ -16,16 +16,33 @@ describe('validateMagicLinkSignupEmail', () => { expect(result).toEqual({ valid: false, error: MAGIC_LINK_EMAIL_ERRORS.LOWERCASE }); }); - it('should reject email with + character', () => { + it('should reject email with + character for non-kilocode domains', () => { const result = validateMagicLinkSignupEmail('user+tag@example.com'); expect(result).toEqual({ valid: false, error: MAGIC_LINK_EMAIL_ERRORS.NO_PLUS }); }); + it('should allow email with + character for @kilocode.ai domain', () => { + const result = validateMagicLinkSignupEmail('user+tag@kilocode.ai'); + expect(result).toEqual({ valid: true, error: null }); + }); + + it('should reject email with + character for lookalike domains ending in kilocode.ai', () => { + // @henkkilocode.ai ends with "kilocode.ai" but is not the @kilocode.ai domain + const result = validateMagicLinkSignupEmail('mark+klaas@henkkilocode.ai'); + expect(result).toEqual({ valid: false, error: MAGIC_LINK_EMAIL_ERRORS.NO_PLUS }); + }); + it('should reject email with both uppercase and +', () => { // Uppercase check happens first const result = validateMagicLinkSignupEmail('User+tag@Example.com'); expect(result).toEqual({ valid: false, error: MAGIC_LINK_EMAIL_ERRORS.LOWERCASE }); }); + + it('should reject uppercase @kilocode.ai email even with +', () => { + // Uppercase check happens first, even for kilocode.ai + const result = validateMagicLinkSignupEmail('User+tag@kilocode.ai'); + expect(result).toEqual({ valid: false, error: MAGIC_LINK_EMAIL_ERRORS.LOWERCASE }); + }); }); describe('magicLinkSignupEmailSchema', () => { @@ -47,11 +64,25 @@ describe('magicLinkSignupEmailSchema', () => { } }); - it('should reject email with + character', () => { + it('should reject email with + character for non-kilocode domains', () => { const result = magicLinkSignupEmailSchema.safeParse('user+tag@example.com'); expect(result.success).toBe(false); if (!result.success) { expect(result.error.issues[0].message).toBe('Email address cannot contain a + character'); } }); + + it('should allow email with + character for @kilocode.ai domain', () => { + const result = magicLinkSignupEmailSchema.safeParse('user+tag@kilocode.ai'); + expect(result.success).toBe(true); + }); + + it('should reject email with + character for lookalike domains ending in kilocode.ai', () => { + // @henkkilocode.ai ends with "kilocode.ai" but is not the @kilocode.ai domain + const result = magicLinkSignupEmailSchema.safeParse('mark+klaas@henkkilocode.ai'); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe('Email address cannot contain a + character'); + } + }); }); diff --git a/src/lib/schemas/email.ts b/src/lib/schemas/email.ts index 41441ee485..a6f2dc32f0 100644 --- a/src/lib/schemas/email.ts +++ b/src/lib/schemas/email.ts @@ -13,10 +13,27 @@ export const MAGIC_LINK_EMAIL_ERRORS = { NO_PLUS: 'EMAIL-CANNOT-CONTAIN-PLUS', } as const; +/** + * Domain that is allowed to use + in email addresses for internal testing. + */ +const KILOCODE_DOMAIN = '@kilocode.ai'; + +/** + * Checks if an email is from the kilocode.ai domain. + * Uses strict matching to ensure the domain is exactly @kilocode.ai, + * not a subdomain or lookalike (e.g., @henkkilocode.ai). + */ +function isKilocodeDomain(email: string): boolean { + const atIndex = email.lastIndexOf('@'); + if (atIndex === -1) return false; + const domain = email.slice(atIndex).toLowerCase(); + return domain === KILOCODE_DOMAIN; +} + /** * Validates that an email is suitable for magic link signup: * - Must be lowercase - * - Must not contain a + character + * - Must not contain a + character (except for @kilocode.ai emails) * * This is NOT enforced during sign-in to existing accounts. * Returns error codes that can be displayed via AuthErrorNotification. @@ -28,7 +45,7 @@ export function validateMagicLinkSignupEmail(email: string): { if (email !== email.toLowerCase()) { return { valid: false, error: MAGIC_LINK_EMAIL_ERRORS.LOWERCASE }; } - if (email.includes('+')) { + if (email.includes('+') && !isKilocodeDomain(email)) { return { valid: false, error: MAGIC_LINK_EMAIL_ERRORS.NO_PLUS }; } return { valid: true, error: null }; @@ -39,6 +56,6 @@ export const magicLinkSignupEmailSchema = z .refine(email => email === email.toLowerCase(), { message: 'Email address must be lowercase', }) - .refine(email => !email.includes('+'), { + .refine(email => !email.includes('+') || isKilocodeDomain(email), { message: 'Email address cannot contain a + character', });