Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

88 lines
3.0 KiB
TypeScript

import type { MxRecord } from 'dns'
import dns from 'dns/promises'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
const logger = createLogger('EmailValidationServer')
const MX_LOOKUP_TIMEOUT_MS = 3000
export interface SignupEmailCheck {
/** Whether the email may proceed to signup. */
allowed: boolean
/** Machine-readable block reason, present only when `allowed` is false. */
reason?: 'no_mx' | 'blocked_mx_backend'
}
/**
* Server-side signup email validation backed by an MX lookup.
*
* Rejects domains that resolve to no mail server (`no_mx`) or to a denylisted
* catch-all backend (`blocked_mx_backend`). Designed to be fail-open: any DNS
* timeout or transient resolver error allows the signup through so legitimate
* users are never blocked by an infrastructure blip. Only a definitive
* "domain has no MX" answer (`ENOTFOUND` / `ENODATA`) blocks.
*
* `blockedMxHosts` are case-insensitive substrings matched against each resolved
* MX exchange — signup-spam botnets rotate throwaway domains but funnel them
* through a few shared catch-all backends, so the MX host is a more stable signal
* than the domain. Sourced from access-control config (AppConfig or env fallback).
*
* Server-only — imports `dns/promises`. Never import from client code. Gated by the caller
* behind `isSignupMxValidationEnabled`; this function performs the check unconditionally.
*/
export async function validateSignupEmailMx(
email: string,
blockedMxHosts: string[]
): Promise<SignupEmailCheck> {
const domain = email.split('@')[1]?.toLowerCase()
if (!domain) return { allowed: true }
let records: MxRecord[]
let timeoutHandle: ReturnType<typeof setTimeout> | undefined
try {
records = await Promise.race([
dns.resolveMx(domain),
new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(
() => reject(new Error('mx_lookup_timeout')),
MX_LOOKUP_TIMEOUT_MS
)
}),
])
} catch (error) {
const code = (error as NodeJS.ErrnoException).code
if (code === 'ENOTFOUND' || code === 'ENODATA') {
logger.info('Blocked signup: domain has no MX record', { domain })
return { allowed: false, reason: 'no_mx' }
}
logger.warn('MX lookup failed; allowing signup (fail-open)', {
domain,
error: getErrorMessage(error),
})
return { allowed: true }
} finally {
if (timeoutHandle) clearTimeout(timeoutHandle)
}
if (!records || records.length === 0) {
logger.info('Blocked signup: domain has no MX record', { domain })
return { allowed: false, reason: 'no_mx' }
}
const match = records.find((record) => {
const exchange = record.exchange.toLowerCase()
return blockedMxHosts.some((host) => exchange.includes(host))
})
if (match) {
logger.info('Blocked signup: denylisted MX backend', {
domain,
exchange: match.exchange,
})
return { allowed: false, reason: 'blocked_mx_backend' }
}
return { allowed: true }
}