Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions src/lib/schemas/email.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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');
}
});
});
23 changes: 20 additions & 3 deletions src/lib/schemas/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 };
Expand All @@ -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',
});