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
13 changes: 12 additions & 1 deletion .github/workflows/migrations.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,18 @@ jobs:

if [ "${ENVIRONMENT}" = "dev" ]; then
echo "Dev environment — pushing schema directly (db:push)"
bun run db:push --force
# `--force` only suppresses the data-loss confirm, not drizzle's
# rename-vs-drop prompt, which fires (and crashes, no TTY) when a
# diff both adds and drops tables/columns at once. Turn that opaque
# crash into an actionable failure instead of a bare stack trace.
push_output="$(bun run db:push --force 2>&1)" && push_status=0 || push_status=$?
echo "$push_output"
if [ "$push_status" -ne 0 ]; then
if printf '%s' "$push_output" | grep -q 'Interactive prompts require a TTY'; then
echo "::error title=Dev schema push needs manual reconciliation::drizzle-kit push hit an interactive rename/drop prompt that CI cannot answer. The dev DB has drifted from schema.ts: it still holds table(s)/column(s) the schema no longer declares while the schema also adds new ones, so drizzle cannot tell a rename from a drop+create. Fix: drop the stale objects on the dev DB to match schema.ts — the same DROPs the latest versioned migration already applied to staging/prod (grep packages/db/migrations for the most recent DROP TABLE / DROP COLUMN) — then re-run this workflow. --force cannot bypass this prompt."
fi
exit "$push_status"
fi
else
echo "Applying versioned migrations (db:migrate)"
bun run ./scripts/migrate.ts
Expand Down
11 changes: 10 additions & 1 deletion apps/sim/lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
organization,
} from 'better-auth/plugins'
import { emailHarmony } from 'better-auth-harmony'
import { validateEmail as validateEmailWithMailchecker } from 'better-auth-harmony/email'
import { and, count, eq, inArray, sql } from 'drizzle-orm'
import { headers } from 'next/headers'
import Stripe from 'stripe'
Expand Down Expand Up @@ -78,6 +79,7 @@ import {
import { PlatformEvents } from '@/lib/core/telemetry'
import { getBaseUrl, isLocalhostUrl, parseOriginList } from '@/lib/core/utils/urls'
import { processCredentialDraft } from '@/lib/credentials/draft-processor'
import { isDisposableEmailDomain } from '@/lib/messaging/email/disposable-domains.server'
Comment thread
cursor[bot] marked this conversation as resolved.
import { sendEmail } from '@/lib/messaging/email/mailer'
import { getFromEmailAddress, getPersonalEmailFrom } from '@/lib/messaging/email/utils'
import { quickValidateEmail } from '@/lib/messaging/email/validation'
Expand Down Expand Up @@ -930,7 +932,14 @@ export const auth = betterAuth({
}),
},
plugins: [
...(isSignupEmailValidationEnabled ? [emailHarmony()] : []),
...(isSignupEmailValidationEnabled
? [
emailHarmony({
validator: async (email) =>
validateEmailWithMailchecker(email) && !(await isDisposableEmailDomain(email)),
}),
]
: []),
...(env.TURNSTILE_SECRET_KEY
? [
captcha({
Expand Down
10 changes: 10 additions & 0 deletions apps/sim/lib/messaging/email/disposable-domains.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/** Ambient types for `disposable-email-domains` — ships JSON arrays with no bundled types. */
declare module 'disposable-email-domains' {
const domains: string[]
export default domains
}

declare module 'disposable-email-domains/wildcard.json' {
const baseDomains: string[]
export default baseDomains
}
35 changes: 35 additions & 0 deletions apps/sim/lib/messaging/email/disposable-domains.server.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* @vitest-environment node
*/
import { describe, expect, it } from 'vitest'
import { isDisposableEmailDomain } from '@/lib/messaging/email/disposable-domains.server'

describe('isDisposableEmailDomain', () => {
it('flags a known disposable domain', async () => {
expect(await isDisposableEmailDomain('someone@mailinator.com')).toBe(true)
})

it('flags a subdomain of a wildcard base domain', async () => {
expect(await isDisposableEmailDomain('someone@inbox.10mail.org')).toBe(true)
})

it('flags the bare wildcard base domain itself', async () => {
expect(await isDisposableEmailDomain('someone@10mail.org')).toBe(true)
})

it('is case-insensitive on the domain', async () => {
expect(await isDisposableEmailDomain('Someone@MailInator.com')).toBe(true)
})

it('allows a normal provider domain', async () => {
expect(await isDisposableEmailDomain('someone@gmail.com')).toBe(false)
})

it('allows a custom catch-all domain that is not on the list', async () => {
expect(await isDisposableEmailDomain('sim6dc088f506@lordfortescue.org.uk')).toBe(false)
})

it('returns false for malformed input with no domain', async () => {
expect(await isDisposableEmailDomain('not-an-email')).toBe(false)
})
})
32 changes: 32 additions & 0 deletions apps/sim/lib/messaging/email/disposable-domains.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
let cache: { exact: Set<string>; wildcards: string[] } | undefined

/**
* Lazily loads the `disposable-email-domains` dataset (~120K exact domains plus
* wildcard base domains) on first use and memoizes it. Deferred behind a dynamic
* import so deployments with signup email validation disabled never load it.
*/
async function loadDisposableData(): Promise<{ exact: Set<string>; wildcards: string[] }> {
if (!cache) {
const [{ default: exactList }, { default: wildcards }] = await Promise.all([
import('disposable-email-domains'),
import('disposable-email-domains/wildcard.json'),
])
cache = { exact: new Set(exactList), wildcards }
}
return cache
}

/**
* Server-only disposable-email-domain check. Layered alongside better-auth-harmony's
* bundled Mailchecker list at the signup gate. Matches exact domains and any subdomain
* of (or the bare) wildcard base domain.
*
* Never import from client code — the dataset would bloat the browser bundle.
*/
export async function isDisposableEmailDomain(email: string): Promise<boolean> {
const domain = email.split('@')[1]?.toLowerCase()
if (!domain) return false
const { exact, wildcards } = await loadDisposableData()
if (exact.has(domain)) return true
return wildcards.some((base) => domain === base || domain.endsWith(`.${base}`))
}
1 change: 1 addition & 0 deletions apps/sim/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
"csv-parse": "6.1.0",
"date-fns": "4.1.0",
"decimal.js": "10.6.0",
"disposable-email-domains": "1.0.62",
"docx": "^9.6.1",
"docx-preview": "^0.3.7",
"drizzle-orm": "^0.45.2",
Expand Down
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading