Skip to content

Add disposable email domain heuristics for sign-up risk scoring#1234

Closed
mantrakp04 wants to merge 6 commits intofraud-protection-country-codefrom
fraud-protection-temp-emails
Closed

Add disposable email domain heuristics for sign-up risk scoring#1234
mantrakp04 wants to merge 6 commits intofraud-protection-country-codefrom
fraud-protection-temp-emails

Conversation

@mantrakp04
Copy link
Copy Markdown
Collaborator

@mantrakp04 mantrakp04 commented Mar 10, 2026

Summary

  • Implements a weighted sign-up risk heuristic pipeline in risk-scores.tsx that detects disposable/temporary email domains via regex patterns and outputs bot + free-trial-abuse scores
  • Replaces the hardcoded test@example.com stub with real pattern-based detection for domains like tempmail, guerrillamail, mailinator, yopmail, etc.
  • Adds E2E tests validating that disposable-email heuristics correctly trigger sign-up rule blocking

Stacked on #1232

Test plan

  • Unit tests in risk-scores.tsx via import.meta.vitest for disposable email matching
  • E2E test for risk score conditions derived from disposable-email heuristics
  • Updated existing risk-scores E2E tests to use disposable domains instead of test@example.com

Made with Cursor

…. Introduced new risk score calculations based on disposable email patterns, enhancing fraud detection capabilities. Updated tests to validate the new heuristics and their integration into the sign-up process.
@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 10, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment Mar 12, 2026 5:35pm
stack-backend Ready Ready Preview, Comment Mar 12, 2026 5:35pm
stack-dashboard Ready Ready Preview, Comment Mar 12, 2026 5:35pm
stack-demo Ready Ready Preview, Comment Mar 12, 2026 5:35pm
stack-docs Ready Ready Preview, Comment Mar 12, 2026 5:35pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 10, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc275ce4-9de2-4af1-9455-29f1059339c7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fraud-protection-temp-emails
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

STACK_RISK_BOT_DISPOSABLE_EMAIL_WEIGHT and STACK_RISK_FTA_DISPOSABLE_EMAIL_WEIGHT
(both default to 100). Clamp final score to 0-100 instead of requiring sum=100.

Made-with: Cursor
…istics. Added new fields to the ProjectUser model for tracking sign-up IP and email normalization. Updated environment configurations for Emailable API keys and adjusted risk score calculations to incorporate new heuristics. Enhanced tests for email validation and sign-up processes to ensure accurate handling of new attributes.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 12, 2026

Greptile Summary

This PR introduces a weighted sign-up risk scoring pipeline backed by the Emailable API, replacing a hardcoded stub. It adds new database columns for storing heuristic facts (IP, normalised email) at sign-up time, a CEL-based rule evaluator, and an internal test endpoint — all of which are well-architected additions. However, there are several blocking issues that need to be addressed before merging:

  • riskScores.freeTrialAbuse naming mismatch in cel-evaluator.tscreateSignUpRuleContext returns the key as free_trial_abuse (snake_case) while every unit test assertion and every CEL rule condition (e.g. riskScores.freeTrialAbuse >= 80) uses camelCase freeTrialAbuse. This means all CEL rules that condition on the free_trial_abuse score silently never fire, and the in-file unit tests will fail.
  • Emailable API errors are treated as disposable emailsrisk-scores.tsx sets disposableEmailMatched = true when the status is "error", so any transient Emailable outage assigns bot: 100 / free_trial_abuse: 100 to every sign-up during the window, potentially blocking or flagging all legitimate users.
  • E2E tests depend on live Emailable API — several tests in sign-up-rules-test.test.ts and risk-scores.test.ts send domains like best-tempmail-service.com and tempmail.com and assert scores of 100. The no-key fallback path only flags the sentinel domain emailable-not-deliverable.example.com, so these tests will fail in any CI environment without a configured real API key.
  • Live API credential committed to .env.development — the Emailable test key should not be stored in source control; replace with a placeholder comment.

Confidence Score: 1/5

  • Not safe to merge — contains a runtime logic bug that silently disables free_trial_abuse CEL rules for all users, an API-error false-positive that can block legitimate users en masse, and unreliable E2E tests.
  • The riskScores.freeTrialAbuse naming mismatch is a confirmed logic bug that will cause unit tests to fail and make every CEL condition referencing freeTrialAbuse evaluate to false at runtime. The API-error-as-disposable logic can cause false-positive mass-blocking during any Emailable outage. Combined with test reliability issues and a committed credential, this PR requires fixes before it is safe to ship.
  • apps/backend/src/lib/cel-evaluator.ts (naming bug), apps/backend/src/lib/risk-scores.tsx (API error handling), E2E test files using real domain names

Important Files Changed

Filename Overview
apps/backend/src/lib/cel-evaluator.ts Critical naming mismatch: createSignUpRuleContext returns riskScores.free_trial_abuse (snake_case) but all unit tests and CEL expressions reference riskScores.freeTrialAbuse (camelCase), making free_trial_abuse-based rules silently non-functional.
apps/backend/src/lib/risk-scores.tsx Emailable API errors are treated identically to confirmed disposable emails, assigning max risk scores (100/100) during any transient API outage and causing false positives for legitimate users.
apps/backend/src/lib/emailable.tsx Well-structured Emailable client wrapper with retry logic, test-mode sentinel domain, and proper error capture; no functional issues found in isolation.
apps/backend/src/lib/sign-up-heuristics.tsx Clean IP/email normalisation logic with thorough in-file unit tests; no issues found.
apps/e2e/tests/backend/endpoints/api/v1/internal/sign-up-rules-test.test.ts New "derives risk score from disposable-email heuristics" test uses best-tempmail-service.com, which is only flagged by the live Emailable API and will produce score 0 in any CI environment without a real API key.
apps/e2e/tests/backend/endpoints/api/v1/risk-scores.test.ts Multiple tests use user@tempmail.com and expect bot: 100 / free_trial_abuse: 100, but the no-API-key code path only flags the sentinel domain emailable-not-deliverable.example.com, so these tests will fail without a live Emailable key.
apps/backend/.env.development A real Emailable test API key is committed directly to this file; should be replaced with a placeholder comment like the rest of .env.
apps/backend/prisma/migrations/20260310000000_add_signup_heuristic_facts/migration.sql Adds five nullable columns to ProjectUser with appropriate covering indexes; migration is safe for populated tables as all columns default to NULL.
apps/backend/src/app/api/latest/internal/sign-up-rules-test/route.tsx Uses SmartRouteHandler correctly; derives risk scores from the real end-user context and allows admin override. No issues found.

Sequence Diagram

sequenceDiagram
    participant Client
    participant SignUpRoute as auth/password/sign-up
    participant Users as users.tsx
    participant RiskScores as risk-scores.tsx
    participant Emailable as emailable.tsx
    participant Heuristics as sign-up-heuristics.tsx
    participant DB as Prisma / DB
    participant CEL as cel-evaluator.ts
    participant Rules as sign-up-rules.tsx

    Client->>SignUpRoute: POST /auth/password/sign-up
    SignUpRoute->>Users: createOrUpgradeAnonymousUserWithRules(...)
    Users->>RiskScores: calculateSignUpRiskAssessment(tenancy, context)
    RiskScores->>Heuristics: deriveSignUpHeuristicFacts(email, ip)
    Heuristics-->>RiskScores: normalised IP + email facts
    RiskScores->>DB: query recent same-IP / similar-email sign-ups
    DB-->>RiskScores: counts
    RiskScores->>Emailable: checkEmailWithEmailable(email)
    Emailable-->>RiskScores: status: "ok" | "not-deliverable" | "error"
    Note over RiskScores: Combines disposable + IP + email scores (clamped 0-100)
    RiskScores-->>Users: { scores, heuristicFacts }
    Users->>DB: persist risk scores + heuristic facts on ProjectUser
    Users->>CEL: createSignUpRuleContext(email, scores, ...)
    CEL->>Rules: evaluateSignUpRulesWithTrace(tenancy, context)
    Rules-->>Users: outcome (allow / reject / restrict)
    Users-->>SignUpRoute: created user or error
    SignUpRoute-->>Client: 200 tokens | 403 SIGN_UP_REJECTED
Loading

Comments Outside Diff (1)

  1. apps/backend/.env.development, line 21 (link)

    Real API credential committed to source control

    A live Emailable test API key is committed directly to .env.development. Even though it is scoped to a test plan, committing real credentials to source control creates a rotation burden and can leak access if the repository is ever forked or made public.

    Consider replacing it with a placeholder comment (matching the style of the main .env file) and documenting where developers can obtain the value out-of-band.

    Fix in Claude Code Fix in Cursor Fix in Codex

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Last reviewed commit: 441e35e

Comment on lines +192 to +195
riskScores: {
bot: params.riskScores.bot,
free_trial_abuse: params.riskScores.free_trial_abuse,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

riskScores.freeTrialAbuse naming mismatch breaks CEL evaluation

createSignUpRuleContext stores the score under the key free_trial_abuse (snake_case):

riskScores: {
  bot: params.riskScores.bot,
  free_trial_abuse: params.riskScores.free_trial_abuse,
},

But the unit tests at lines 218, 239, 263, 285, and 308 all assert the camelCase key freeTrialAbuse. Similarly, the E2E test in risk-scores.test.ts (line 838) uses the CEL condition riskScores.bot >= 80 && riskScores.freeTrialAbuse >= 80. Because the context object only contains free_trial_abuse, the identifier freeTrialAbuse resolves to undefined inside the CEL evaluator, so undefined >= 80 evaluates to false — the free_trial_abuse score is effectively invisible to every CEL rule that references it. The unit tests would also fail outright.

The return value should use camelCase to match the convention used in CEL expressions and tests:

Suggested change
riskScores: {
bot: params.riskScores.bot,
free_trial_abuse: params.riskScores.free_trial_abuse,
},
riskScores: {
bot: params.riskScores.bot,
freeTrialAbuse: params.riskScores.free_trial_abuse,
},

Fix in Claude Code Fix in Cursor Fix in Codex

? { status: "ok" } as const
: await checkEmailWithEmailable(context.primaryEmail);

const disposableEmailMatched = emailableResult.status === "not-deliverable" || emailableResult.status === "error";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API errors treated as disposable emails causes false positives

When the Emailable API call throws (network timeout, rate-limit, server error, etc.), checkEmailWithEmailable returns { status: "error" } (the default onError is "return-error"). Line 164 then treats this as evidence that the email is disposable:

const disposableEmailMatched = emailableResult.status === "not-deliverable" || emailableResult.status === "error";

This means any transient Emailable outage will silently assign bot: 100, free_trial_abuse: 100 to every sign-up during the outage window. Legitimate users would be wrongly flagged, potentially blocked by downstream sign-up rules, or permanently carry high-risk scores.

Consider treating API errors as "unknown" rather than "disposable":

Suggested change
const disposableEmailMatched = emailableResult.status === "not-deliverable" || emailableResult.status === "error";
const disposableEmailMatched = emailableResult.status === "not-deliverable";

Fix in Claude Code Fix in Cursor Fix in Codex

method: "POST",
accessType: "admin",
body: {
email: "user@best-tempmail-service.com",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E2E test relies on live Emailable API to flag best-tempmail-service.com

The test at line 159 sends user@best-tempmail-service.com and expects bot: 100 / free_trial_abuse: 100. However, the no-API-key shortcut in emailable.tsx only returns not-deliverable for the single sentinel domain emailable-not-deliverable.example.com. Any other domain — including best-tempmail-service.com — falls through to { status: "ok" }, producing zero risk scores. Without a real Emailable API key in the test environment, this test will fail.

Use the dedicated test-mode sentinel domain instead:

Suggested change
email: "user@best-tempmail-service.com",
email: `user@${EMAILABLE_NOT_DELIVERABLE_TEST_DOMAIN}`,

(Import EMAILABLE_NOT_DELIVERABLE_TEST_DOMAIN from @/lib/emailable, or export it from a shared test-helpers module.)

The same issue exists for every assertion in risk-scores.test.ts that uses user@tempmail.com and expects bot: 100 / free_trial_abuse: 100 (lines 762, 812, 843).

Fix in Claude Code Fix in Cursor Fix in Codex

method: "POST",
accessType: "admin",
body: {
email: "user@best-tempmail-service.com",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
email: "user@best-tempmail-service.com",
email: "user@emailable-not-deliverable.example.com",

Test uses wrong email domain that won't trigger disposable email detection in test mode, causing incorrect expected risk scores.

Fix on Vercel

? { status: "ok" } as const
: await checkEmailWithEmailable(context.primaryEmail);

const disposableEmailMatched = emailableResult.status === "not-deliverable" || emailableResult.status === "error";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const disposableEmailMatched = emailableResult.status === "not-deliverable" || emailableResult.status === "error";
const disposableEmailMatched = emailableResult.status === "not-deliverable";

API errors from Emailable service incorrectly penalize users with +100 bot and free_trial_abuse risk scores

Fix on Vercel

accessType: "client",
body: {
email: "test@example.com",
email: "user@tempmail.com",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests use tempmail.com expecting 100/100 risk scores but this domain doesn't trigger the test-mode undeliverable check

Fix on Vercel

@mantrakp04 mantrakp04 closed this Mar 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant