diff --git a/apps/web/src/components/DataLayerProvider.tsx b/apps/web/src/components/DataLayerProvider.tsx index 540516234..109e8f1b7 100644 --- a/apps/web/src/components/DataLayerProvider.tsx +++ b/apps/web/src/components/DataLayerProvider.tsx @@ -22,8 +22,6 @@ function AddUserData() { if (status === 'authenticated' && session?.user?.email) { const evt = { event: 'data_layer_update', - email: session.user.email, - name: session.user.name, is_new_user: session.isNewUser || false, }; window.dataLayer.push(evt); diff --git a/apps/web/src/lib/config.server.ts b/apps/web/src/lib/config.server.ts index 2f7c001a6..52de055a4 100644 --- a/apps/web/src/lib/config.server.ts +++ b/apps/web/src/lib/config.server.ts @@ -15,6 +15,8 @@ export const GITHUB_CLIENT_ID = getEnvVariable('GITHUB_CLIENT_ID'); export const GITHUB_CLIENT_SECRET = getEnvVariable('GITHUB_CLIENT_SECRET'); // Admin-only GitHub access (used for admin dashboards) export const GITHUB_ADMIN_STATS_TOKEN = getEnvVariable('GITHUB_ADMIN_STATS_TOKEN'); +export const CONTRIBUTOR_CHAMPION_TEAM_EMAILS = + getEnvVariable('CONTRIBUTOR_CHAMPION_TEAM_EMAILS') || ''; export const GITLAB_CLIENT_ID = getEnvVariable('GITLAB_CLIENT_ID'); export const GITLAB_CLIENT_SECRET = getEnvVariable('GITLAB_CLIENT_SECRET'); export const LINKEDIN_CLIENT_ID = getEnvVariable('LINKEDIN_CLIENT_ID'); diff --git a/apps/web/src/lib/contributor-champions/service.test.ts b/apps/web/src/lib/contributor-champions/service.test.ts index f5dd6738d..663b61ffa 100644 --- a/apps/web/src/lib/contributor-champions/service.test.ts +++ b/apps/web/src/lib/contributor-champions/service.test.ts @@ -16,6 +16,7 @@ const mockedFetchWithBackoff = jest.fn() as jest.MockedFunction; jest.mock('@/lib/config.server', () => ({ + CONTRIBUTOR_CHAMPION_TEAM_EMAILS: '', GITHUB_ADMIN_STATS_TOKEN: 'test-github-token', })); diff --git a/apps/web/src/lib/contributor-champions/service.ts b/apps/web/src/lib/contributor-champions/service.ts index be084903b..7cb91bb44 100644 --- a/apps/web/src/lib/contributor-champions/service.ts +++ b/apps/web/src/lib/contributor-champions/service.ts @@ -2,7 +2,7 @@ import 'server-only'; import { db } from '@/lib/drizzle'; import { fetchWithBackoff } from '@/lib/fetchWithBackoff'; -import { GITHUB_ADMIN_STATS_TOKEN } from '@/lib/config.server'; +import { CONTRIBUTOR_CHAMPION_TEAM_EMAILS, GITHUB_ADMIN_STATS_TOKEN } from '@/lib/config.server'; import teamLoginsJson from '@/data/contributor-champion-kilo-team.json'; import { contributor_champion_contributors, @@ -31,7 +31,11 @@ const TEAM_LOGIN_LIST = (() => { const TEAM_LOGIN_SET = new Set(TEAM_LOGIN_LIST.map(login => login.trim().toLowerCase())); const TEAM_EMAIL_DOMAINS = new Set(['kilocode.ai', 'kilo.ai']); -const TEAM_EMAILS = new Set(['vincesprints@gmail.com']); +const TEAM_EMAILS = new Set( + CONTRIBUTOR_CHAMPION_TEAM_EMAILS.split(',') + .map(email => email.trim().toLowerCase()) + .filter(Boolean) +); function isTeamEmail(email: string | null): boolean { if (!email) return false; diff --git a/apps/web/src/lib/email-literal-guardrail.test.ts b/apps/web/src/lib/email-literal-guardrail.test.ts new file mode 100644 index 000000000..45153fb27 --- /dev/null +++ b/apps/web/src/lib/email-literal-guardrail.test.ts @@ -0,0 +1,60 @@ +import { readdirSync, readFileSync, statSync } from 'fs'; +import { join, relative } from 'path'; + +const EMAIL_REGEX = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g; +const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.mdx']); +const ALLOWED_EMAILS = new Set([ + 'sales@kilocode.ai', + 'teams@kilocode.ai', + 'hi@kilocode.ai', + 'hi@app.kilocode.ai', + 'hi@kilo.ai', + 'admin@kilocode.ai', + 'git@github.com', + 'git@gitlab.com', +]); +const PLACEHOLDER_DOMAINS = ['example.com', 'example.test', 'test.local', 'admin.example.com']; +const EXCLUDED_PATH_PARTS = new Set(['tests', 'scripts']); + +function listProductionSourceFiles(dir: string): string[] { + const entries = readdirSync(dir); + const files: string[] = []; + + for (const entry of entries) { + const path = join(dir, entry); + const stats = statSync(path); + if (stats.isDirectory()) { + if (!EXCLUDED_PATH_PARTS.has(entry)) { + files.push(...listProductionSourceFiles(path)); + } + continue; + } + + if (!SOURCE_EXTENSIONS.has(path.slice(path.lastIndexOf('.')))) continue; + if (path.endsWith('.test.ts') || path.endsWith('.test.tsx')) continue; + files.push(path); + } + + return files; +} + +function isAllowedEmail(email: string): boolean { + const normalized = email.toLowerCase(); + if (ALLOWED_EMAILS.has(normalized)) return true; + return PLACEHOLDER_DOMAINS.some(domain => normalized.endsWith(`@${domain}`)); +} + +describe('email literal guardrail', () => { + it('keeps production source email literals limited to placeholders and approved aliases', () => { + const srcRoot = join(process.cwd(), 'src'); + const findings = listProductionSourceFiles(srcRoot).flatMap(file => { + const content = readFileSync(file, 'utf8'); + return Array.from(content.matchAll(EMAIL_REGEX)) + .map(match => match[0]) + .filter(email => !isAllowedEmail(email)) + .map(email => `${relative(process.cwd(), file)}: ${email}`); + }); + + expect(findings).toEqual([]); + }); +}); diff --git a/apps/web/src/types/datalayer.d.ts b/apps/web/src/types/datalayer.d.ts index d9dbef77f..8e76d60dc 100644 --- a/apps/web/src/types/datalayer.d.ts +++ b/apps/web/src/types/datalayer.d.ts @@ -1,6 +1,7 @@ -// Extend the Window interface to include dataLayer +export {}; + declare global { interface Window { - datalayer: object[]; + dataLayer?: Array>; } }