Skip to content

bot protection#1231

Closed
mantrakp04 wants to merge 6 commits into
devfrom
fraud-protection
Closed

bot protection#1231
mantrakp04 wants to merge 6 commits into
devfrom
fraud-protection

Conversation

@mantrakp04
Copy link
Copy Markdown
Collaborator

@mantrakp04 mantrakp04 commented Mar 9, 2026

Summary by CodeRabbit

  • New Features

    • Added sign-up risk scoring system to detect bot activity and free trial abuse patterns
    • Risk scores can be tested and configured within sign-up rules
    • Risk score data is now visible and manageable in user details
    • Sign-up rules support numeric comparisons on risk scores for conditional blocking or allowing
  • Tests

    • Added comprehensive end-to-end test coverage for risk scoring across multiple sign-up methods and user flows

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 9, 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:32pm
stack-backend Error Error Mar 12, 2026 5:32pm
stack-dashboard Ready Ready Preview, Comment Mar 12, 2026 5:32pm
stack-demo Ready Ready Preview, Comment Mar 12, 2026 5:32pm
stack-docs Error Error Mar 12, 2026 5:32pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

A sign-up risk scoring system is introduced, adding risk evaluation capabilities for bot activity and free trial abuse. The implementation spans database schema changes, a new risk calculation module, integration into sign-up flows and rule evaluation, and corresponding UI components for rule testing and user management.

Changes

Cohort / File(s) Summary
Database Schema
apps/backend/prisma/migrations/.../migration.sql, apps/backend/prisma/schema.prisma, apps/backend/prisma/seed.ts
Adds signUpRiskScoreBot and signUpRiskScoreFreeTrialAbuse columns to ProjectUser table with migration and schema updates; seed data includes default risk scores for internal users.
Risk Scoring Module
apps/backend/src/lib/risk-scores.tsx
New module defining SignUpRiskScores and SignUpRiskScoreContext types; calculateSignUpRiskScores function returns high risk (100) for test@example.com, zero otherwise.
Sign-up & User Integration
apps/backend/src/lib/users.tsx, apps/backend/src/app/api/latest/users/crud.tsx
Integrates calculateSignUpRiskScores into anonymous user signup; propagates risk_scores through user creation/update flows and CRUD operations with optional ipAddress parameter.
API & CEL Evaluation
apps/backend/src/app/api/latest/internal/sign-up-rules-test/route.tsx, apps/backend/src/lib/cel-evaluator.ts
Extends sign-up rules test endpoint to accept and return risk_scores; updates SignUpRuleContext and createSignUpRuleContext to include risk score data for CEL expression evaluation.
Shared Type Definitions
packages/stack-shared/src/interface/crud/users.ts, packages/template/src/lib/stack-app/users/index.ts, packages/template/src/lib/stack-app/apps/.../server-app-impl.ts
Adds risk_scores field to server user schemas (read/create/update) with nested sign_up structure; extends ServerBaseUser and ServerUser types to include riskScores property.
Dashboard UI - Sign-up Rules
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
Adds numeric input fields (0-100) for risk score bot and free trial abuse in test form; displays risk_scores in test results context section.
Dashboard UI - User Details
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
Displays bot and free trial abuse risk scores as read-only rows in user details page.
Rule Builder & CEL
apps/dashboard/src/components/rule-builder/condition-builder.tsx, apps/dashboard/src/lib/cel-visual-parser.ts, apps/dashboard/src/lib/cel-visual-parser.test.ts
Adds riskScores.bot and riskScores.freeTrialAbuse as numeric fields with operators greater_than, greater_or_equal, less_than, less_or_equal; updates CEL parsing and serialization to handle numeric comparisons.
Testing
apps/e2e/tests/backend/endpoints/api/v1/risk-scores.test.ts, apps/e2e/tests/backend/endpoints/api/v1/internal/sign-up-rules-test.test.ts
Comprehensive e2e test suite validating risk score initialization, persistence, API visibility, rule evaluation, and update behaviors across signup methods; sign-up-rules-test validates risk score condition blocking.

Sequence Diagram

sequenceDiagram
    actor User
    participant Client
    participant SignUpAPI as Sign-up API
    participant RiskModule as Risk Scoring
    participant RuleEngine as Rule Engine
    participant Database
    participant Dashboard

    User->>Client: Initiate sign-up (email, auth method)
    Client->>SignUpAPI: POST /sign-up with auth details
    SignUpAPI->>RiskModule: calculateSignUpRiskScores(email, authMethod, ipAddress)
    RiskModule->>RiskModule: Evaluate bot & free-trial-abuse risk
    RiskModule-->>SignUpAPI: Return riskScores {bot, freeTrialAbuse}
    SignUpAPI->>RuleEngine: evaluateCelExpression(context + riskScores)
    RuleEngine->>RuleEngine: Check conditions (e.g., riskScores.bot >= 80)
    RuleEngine-->>SignUpAPI: Rule decision (allow/reject/restrict)
    SignUpAPI->>Database: Store ProjectUser with risk_scores columns
    Database-->>SignUpAPI: User created
    SignUpAPI-->>Client: Sign-up result + context
    User->>Dashboard: View user or test sign-up rules
    Dashboard->>Dashboard: Display risk_scores in UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Sign up rules #1138: Modifies the same ProjectUser risk score fields, sign-up evaluation context, and CRUD flows for risk score propagation.

Suggested reviewers

  • nams1570
  • N2D4

Poem

🐰 Hop along the signup trail,
Risk scores bloom without fail,
Bots and abuse, we now can measure,
Rules bend and flex with data's treasure!
CEL expressions dance so bright,
Risk awareness, shining light!

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is incomplete, containing only the repository contribution reminder template with no substantive explanation of changes. Add a detailed description explaining the risk scoring feature implementation, including what risk scores are added, how they are calculated, and which systems are affected.
Docstring Coverage ⚠️ Warning Docstring coverage is 41.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'bot protection' is vague and generic, using non-descriptive terms that do not clearly convey the actual changeset scope. Provide a more specific title that reflects the main changes, such as 'Add sign-up risk scoring fields and evaluation' or 'Implement risk score calculation for bot and free trial abuse detection'.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fraud-protection
📝 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.

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Mar 9, 2026

CLA assistant check
All committers have signed the CLA.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 12, 2026

Greptile Summary

This PR introduces a bot-protection framework: two new SMALLINT columns (signUpRiskScoreBot, signUpRiskScoreFreeTrialAbuse) are added to ProjectUser, risk scores are computed at sign-up time and stored on the user record, and they are exposed to sign-up rule CEL conditions so admins can block or restrict high-risk signups. The dashboard rule builder gains numeric comparison operators and a test panel for risk scores, and the server SDK surfaces riskScores.signUp on server user objects.

Key issues found:

  • Stub implementation in productioncalculateSignUpRiskScores in risk-scores.tsx contains only a // TODO stub that hardcodes test@example.com as high-risk (score 100) and returns 0 for every other user. This is the core of the bot-protection feature and ships non-functional.
  • TypeScript compilation errorSignUpRuleOptions.ipAddress is added as a required (non-optional) string | null field, but two existing callers — the password sign-up route and the OTP verification handler — were not updated to pass it. The OTP handler even has an explicit // TODO: Pass request context comment acknowledging this. This will fail the TypeScript build.
  • Unvalidated field names in CEL parserparseCondition now uses [\w.]+ to capture field names in numeric comparisons and casts results directly as ConditionField, meaning a handcrafted CEL expression with any arbitrary dotted path (e.g. someField.nested >= 50) will parse successfully without validation.
  • Unsafe property access in update handlerdata.risk_scores?.sign_up.bot uses optional chaining on risk_scores but not on sign_up, which could throw at runtime if sign_up is absent.

Confidence Score: 1/5

  • Not safe to merge — the core feature is a non-functional stub and there is a TypeScript compilation error from missing required fields in existing callers.
  • The calculateSignUpRiskScores function is explicitly a TODO placeholder, meaning the bot-protection feature does nothing real in production. Additionally, adding a required ipAddress field to SignUpRuleOptions without updating all callers breaks the TypeScript build. These two issues together make the PR not production-ready.
  • apps/backend/src/lib/risk-scores.tsx (stub implementation), apps/backend/src/lib/users.tsx (required field breaks callers), apps/dashboard/src/lib/cel-visual-parser.ts (unvalidated field casting)

Important Files Changed

Filename Overview
apps/backend/src/lib/risk-scores.tsx New file implementing risk score calculation — currently a TODO stub with hardcoded test@example.com logic; no real bot-detection is performed in production.
apps/backend/src/lib/users.tsx Adds ipAddress as a required field to SignUpRuleOptions and integrates risk score calculation into sign-up flow, but existing callers (password and OTP sign-up routes) were not updated to pass ipAddress, causing TypeScript compilation errors.
apps/dashboard/src/lib/cel-visual-parser.ts Extends CEL parser with numeric comparison operators and dotted field support for riskScores.bot/riskScores.freeTrialAbuse; parsed field names are cast as ConditionField without validation, allowing arbitrary field paths.
apps/backend/src/app/api/latest/users/crud.tsx Maps risk score fields to/from Prisma and CRUD schemas; uses unsafe property access data.risk_scores?.sign_up.bot in the update handler.
apps/e2e/tests/backend/endpoints/api/v1/risk-scores.test.ts Comprehensive e2e tests for risk score persistence, API visibility, update validation, and sign-up rule interaction; all tests rely on the test@example.com stub behavior.

Sequence Diagram

sequenceDiagram
    participant Client
    participant SignUpRoute as Sign-Up Route
    participant Users as users.tsx
    participant RiskScores as risk-scores.tsx
    participant CELEval as cel-evaluator.ts
    participant SignUpRules as sign-up-rules.ts
    participant DB as Database (ProjectUser)

    Client->>SignUpRoute: POST /auth/password/sign-up
    SignUpRoute->>Users: createOrUpgradeAnonymousUserWithRules(...)
    Users->>RiskScores: calculateSignUpRiskScores(tenancy, {email, authMethod, ipAddress, ...})
    RiskScores-->>Users: SignUpRiskScores {bot, freeTrialAbuse}
    Users->>CELEval: createSignUpRuleContext({email, authMethod, riskScores})
    CELEval-->>Users: SignUpRuleContext
    Users->>SignUpRules: evaluateSignUpRules(tenancy, context)
    SignUpRules-->>Users: {shouldAllow, action}
    alt rejected
        Users-->>SignUpRoute: throw SIGN_UP_REJECTED
        SignUpRoute-->>Client: 403 SIGN_UP_REJECTED
    else allowed / restricted
        Users->>DB: CREATE ProjectUser (signUpRiskScoreBot, signUpRiskScoreFreeTrialAbuse)
        DB-->>Users: created user
        Users-->>SignUpRoute: UsersCrud["Admin"]["Read"]
        SignUpRoute-->>Client: 200 {user_id, ...}
    end
Loading

Comments Outside Diff (1)

  1. apps/backend/src/lib/risk-scores.tsx, line 21-34 (link)

    Stub implementation shipped as production code

    calculateSignUpRiskScores is a placeholder with hardcoded logic that will be shipped as-is. Any user signing up with test@example.com always gets bot: 100, freeTrialAbuse: 100, and every other user always gets bot: 0, freeTrialAbuse: 0 regardless of actual bot signals. This means the entire bot-protection feature is effectively non-functional in production — all legitimate signups get a clean score of 0 and only the hardcoded test address triggers risk scoring.

    The // TODO marker and the hardcoded email guard should not land in production code until real risk-score computation (using the ipAddress, authMethod, email reputation data, etc.) is implemented.

    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: b9290d5

export type SignUpRuleOptions = {
authMethod: 'password' | 'otp' | 'oauth' | 'passkey',
oauthProvider?: string,
ipAddress: string | null,
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.

Required ipAddress field breaks existing callers

ipAddress: string | null is added as a required (non-optional) property to SignUpRuleOptions. However, two existing callers that were not updated in this PR pass only { authMethod: '...' } without ipAddress, which would be a TypeScript compilation error:

  • apps/backend/src/app/api/latest/auth/password/sign-up/route.tsx calls with { authMethod: 'password' }
  • apps/backend/src/app/api/latest/auth/otp/sign-in/verification-code-handler.tsx calls with { authMethod: 'otp' } (and even has a TODO comment acknowledging this)

Either make ipAddress optional (ipAddress?: string | null) or update all callers to explicitly pass null.

Suggested change
ipAddress: string | null,
ipAddress?: string | null,

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines 291 to +303
function parseCondition(expr: string): ConditionNode | null {
const trimmed = expr.trim();

// Match patterns like: field >= 42
const greaterOrEqualNumberMatch = trimmed.match(/^([\w.]+)\s*>=\s*(-?\d+(?:\.\d+)?)$/);
if (greaterOrEqualNumberMatch) {
return {
type: 'condition',
id: generateNodeId(),
field: greaterOrEqualNumberMatch[1] as ConditionField,
operator: 'greater_or_equal',
value: Number(greaterOrEqualNumberMatch[2]),
};
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.

Unvalidated field names accepted as ConditionField

The new number-comparison regex patterns use [\w.]+ to capture the field name and then cast the result directly as ConditionField without validating that the matched name is actually a member of the union. For example, a handcrafted CEL expression like someArbitraryField.nested >= 50 would be parsed successfully and produce a ConditionNode with an invalid field value.

This is inconsistent with how the rest of the parser works — string-comparison patterns also now use [\w.]+ but only riskScores.bot and riskScores.freeTrialAbuse are valid dotted fields. An invalid field passed to conditionToCel would silently generate a CEL expression referencing a non-existent context property, which would evaluate as false and give users no indication that their rule is broken.

Consider adding a guard after parsing, e.g.:

const VALID_FIELDS = new Set<string>(['email', 'emailDomain', 'authMethod', 'oauthProvider', 'riskScores.bot', 'riskScores.freeTrialAbuse']);
if (!VALID_FIELDS.has(parsedField)) return null;

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines 1159 to 1160
restrictedByAdminReason: restrictedByAdminReason,
restrictedByAdminPrivateDetails: restrictedByAdminPrivateDetails,
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.

Unsafe property access on optional nested object

data.risk_scores?.sign_up.bot uses optional chaining on risk_scores but not on sign_up. If risk_scores is present but sign_up is somehow absent at runtime (schema coercion edge case, or a future schema change), this would throw TypeError: Cannot read properties of undefined (reading 'bot').

Use consistent optional chaining throughout:

Suggested change
restrictedByAdminReason: restrictedByAdminReason,
restrictedByAdminPrivateDetails: restrictedByAdminPrivateDetails,
signUpRiskScoreBot: data.risk_scores?.sign_up?.bot,
signUpRiskScoreFreeTrialAbuse: data.risk_scores?.sign_up?.free_trial_abuse,

Fix in Claude Code Fix in Cursor Fix in Codex

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds “sign-up risk scores” (bot + free-trial-abuse) as server-only user fields, persists them in the database, exposes them via server APIs/SDK, and enables rule-building + CEL evaluation against these scores for bot protection workflows.

Changes:

  • Persist sign-up risk scores on ProjectUser (Prisma + migration + seed) and include them in server CRUD read/create/update schemas.
  • Compute/store risk scores during signup flows and make them available to sign-up rule evaluation (CEL context).
  • Update Dashboard rule builder + CEL visual parser to support numeric comparisons on riskScores.*, and add e2e coverage for persistence/visibility.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/template/src/lib/stack-app/users/index.ts Adds riskScores to server user types (SDK surface).
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts Maps CRUD risk_scores into ServerUser.riskScores.
packages/stack-shared/src/interface/crud/users.ts Extends server CRUD schemas to read/create/update risk_scores.
apps/e2e/tests/backend/endpoints/api/v1/risk-scores.test.ts Adds end-to-end tests for risk score persistence, visibility, updates, and rule interaction.
apps/e2e/tests/backend/endpoints/api/v1/internal/sign-up-rules-test.test.ts Verifies internal sign-up rules test endpoint supports risk score conditions.
apps/dashboard/src/lib/cel-visual-parser.ts Adds numeric operators + parsing/serialization for riskScores.* conditions.
apps/dashboard/src/lib/cel-visual-parser.test.ts Tests numeric risk score CEL parsing/serialization.
apps/dashboard/src/components/rule-builder/condition-builder.tsx Adds risk score fields + numeric operators + numeric input UI.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx Displays user risk scores in the dashboard user details view.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx Adds risk score inputs to the sign-up rules “test rules” UI and displays returned context.
apps/backend/src/lib/users.tsx Computes risk scores during signup/upgrade and injects them into rule context + persisted user data.
apps/backend/src/lib/risk-scores.tsx Introduces risk score calculation module (currently stubbed).
apps/backend/src/lib/cel-evaluator.ts Extends sign-up rule CEL context to include riskScores.
apps/backend/src/app/api/latest/users/crud.tsx Persists risk scores in Prisma and returns them in server user CRUD responses.
apps/backend/src/app/api/latest/internal/sign-up-rules-test/route.tsx Accepts risk scores in test input and returns them in evaluation context.
apps/backend/prisma/seed.ts Ensures seeded users set required risk score columns.
apps/backend/prisma/schema.prisma Adds signUpRiskScoreBot / signUpRiskScoreFreeTrialAbuse columns to ProjectUser.
apps/backend/prisma/migrations/20260308000000_add_signup_risk_scores/migration.sql Migration to add new required SMALLINT risk score columns.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +81 to +89
function isNumericField(field: ConditionField): boolean {
return field === 'riskScores.bot' || field === 'riskScores.freeTrialAbuse';
}

// Get available operators for a field
function getOperatorsForField(field: ConditionField): ConditionOperator[] {
if (isNumericField(field)) {
return ['equals', 'not_equals', 'greater_than', 'greater_or_equal', 'less_than', 'less_or_equal'];
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

isNumericField(...) is duplicated here and in cel-visual-parser.ts. Since both places must stay in sync for correct operator/value handling, consider exporting a shared helper (or a shared Set of numeric fields) from the parser module to avoid divergence over time.

Copilot uses AI. Check for mistakes.
export type SignUpRuleOptions = {
authMethod: 'password' | 'otp' | 'oauth' | 'passkey',
oauthProvider?: string,
ipAddress: string | null,
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

SignUpRuleOptions now requires ipAddress, but several call sites still pass only { authMethod, oauthProvider } (e.g. password sign-up, OTP verification-code handler). This makes the change a breaking compile-time error. Either make ipAddress optional (defaulting to null) or update all callers to pass a value (or null when unavailable).

Suggested change
ipAddress: string | null,
ipAddress?: string | null,

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +28
// TODO
if (context.primaryEmail === "test@example.com") {
return {
bot: 100,
freeTrialAbuse: 100,
};
} else {
return {
bot: 0,
freeTrialAbuse: 0,
};
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

calculateSignUpRiskScores is currently a hard-coded stub that flags only test@example.com as high risk. Shipping this as-is will produce incorrect risk scores in production and makes sign-up rules unreliable. Please either implement the real scoring logic, or gate this stub to test-only (e.g., behind an env/config flag) and default to neutral scores otherwise.

Suggested change
// TODO
if (context.primaryEmail === "test@example.com") {
return {
bot: 100,
freeTrialAbuse: 100,
};
} else {
return {
bot: 0,
freeTrialAbuse: 0,
};
}
// TODO: Implement real scoring logic. For now, only apply the stub behavior in tests
if (process.env.NODE_ENV === "test" && context.primaryEmail === "test@example.com") {
return {
bot: 100,
freeTrialAbuse: 100,
};
}
// Default to neutral risk scores in non-test environments
return {
bot: 0,
freeTrialAbuse: 0,
};

Copilot uses AI. Check for mistakes.
Comment on lines +108 to +117
return `${field} > ${useNumericValue ? valueAsNumber : 0}`;
}
case 'greater_or_equal': {
return `${field} >= ${useNumericValue ? valueAsNumber : 0}`;
}
case 'less_than': {
return `${field} < ${useNumericValue ? valueAsNumber : 0}`;
}
case 'less_or_equal': {
return `${field} <= ${useNumericValue ? valueAsNumber : 0}`;
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

For numeric operators (>, >=, <, <=), conditionToCel falls back to 0 when the field/value isn't treated as numeric. This can silently change rule semantics if a parsed/edited condition ends up with a non-numeric field or non-numeric value (e.g., it will serialize to field > 0). Consider returning null/throwing for invalid numeric comparisons, or ensuring these operators are only ever used with numeric fields at the type/runtime level.

Suggested change
return `${field} > ${useNumericValue ? valueAsNumber : 0}`;
}
case 'greater_or_equal': {
return `${field} >= ${useNumericValue ? valueAsNumber : 0}`;
}
case 'less_than': {
return `${field} < ${useNumericValue ? valueAsNumber : 0}`;
}
case 'less_or_equal': {
return `${field} <= ${useNumericValue ? valueAsNumber : 0}`;
if (!useNumericValue) {
throw new Error(
`Invalid non-numeric comparison: field "${field}" with operator "greater_than" and value "${String(
value,
)}"`,
);
}
return `${field} > ${valueAsNumber}`;
}
case 'greater_or_equal': {
if (!useNumericValue) {
throw new Error(
`Invalid non-numeric comparison: field "${field}" with operator "greater_or_equal" and value "${String(
value,
)}"`,
);
}
return `${field} >= ${valueAsNumber}`;
}
case 'less_than': {
if (!useNumericValue) {
throw new Error(
`Invalid non-numeric comparison: field "${field}" with operator "less_than" and value "${String(
value,
)}"`,
);
}
return `${field} < ${valueAsNumber}`;
}
case 'less_or_equal': {
if (!useNumericValue) {
throw new Error(
`Invalid non-numeric comparison: field "${field}" with operator "less_or_equal" and value "${String(
value,
)}"`,
);
}
return `${field} <= ${valueAsNumber}`;

Copilot uses AI. Check for mistakes.
Comment on lines +294 to +304
// Match patterns like: field >= 42
const greaterOrEqualNumberMatch = trimmed.match(/^([\w.]+)\s*>=\s*(-?\d+(?:\.\d+)?)$/);
if (greaterOrEqualNumberMatch) {
return {
type: 'condition',
id: generateNodeId(),
field: greaterOrEqualNumberMatch[1] as ConditionField,
operator: 'greater_or_equal',
value: Number(greaterOrEqualNumberMatch[2]),
};
}
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

parseCondition casts any ([\w.]+) match to ConditionField without validating that the field is actually supported. This can yield a ConditionNode with an invalid field value, and also accepts numeric comparisons for non-numeric fields; on round-trip, conditionToCel may serialize these as ... > 0, changing the original expression. Recommend validating the matched field against the allowed set and rejecting numeric operators unless isNumericField(field) is true (otherwise return null so the UI can fall back to raw CEL).

Copilot uses AI. Check for mistakes.
Comment on lines +122 to +127
risk_scores: fieldSchema.yupObject({
sign_up: fieldSchema.yupObject({
bot: fieldSchema.yupNumber().integer().min(0).max(100).defined(),
free_trial_abuse: fieldSchema.yupNumber().integer().min(0).max(100).defined(),
}).defined(),
}).optional(),
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

usersCrudServerCreateSchema is built by concatenating usersCrudServerUpdateSchema, which already defines risk_scores, but it then re-defines risk_scores again in the concat object. This duplication is easy to accidentally let drift and makes the schema harder to maintain; consider removing the duplicate definition and relying on the field inherited from the update schema (or explicitly overriding with a comment explaining why).

Suggested change
risk_scores: fieldSchema.yupObject({
sign_up: fieldSchema.yupObject({
bot: fieldSchema.yupNumber().integer().min(0).max(100).defined(),
free_trial_abuse: fieldSchema.yupNumber().integer().min(0).max(100).defined(),
}).defined(),
}).optional(),

Copilot uses AI. Check for mistakes.
Comment on lines +363 to +369
/** Server-only risk scores used during sign-up risk evaluation. */
readonly riskScores: {
readonly signUp: {
readonly bot: number,
readonly freeTrialAbuse: number,
},
},
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

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

riskScores is now exposed on ServerBaseUser, but ServerUserUpdateOptions (and the corresponding update(...) plumbing) doesn’t provide any way to set/update these scores even though the server API supports risk_scores updates. If risk scores are intended to be server-adjustable (as the API allows), consider adding an update option for them (or intentionally disallowing updates and removing/locking down the server PATCH field).

Suggested change
/** Server-only risk scores used during sign-up risk evaluation. */
readonly riskScores: {
readonly signUp: {
readonly bot: number,
readonly freeTrialAbuse: number,
},
},

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
apps/backend/prisma/migrations/20260308000000_add_signup_risk_scores/migration.sql (1)

2-5: Back the 0–100 invariant with database checks.

SMALLINT still allows values outside the documented range for signUpRiskScoreBot and signUpRiskScoreFreeTrialAbuse, while the new read schema now assumes both are integers in [0, 100]. An out-of-band write would let invalid rows persist and then break later reads. Please add CHECK constraints for both columns in a NOT VALID migration and validate them in a separate follow-up migration.

As per coding guidelines, "When adding CHECK constraints, use NOT VALID in one migration, then VALIDATE CONSTRAINT in a separate migration file."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/backend/prisma/migrations/20260308000000_add_signup_risk_scores/migration.sql`
around lines 2 - 5, Add CHECK constraints on ProjectUser.signUpRiskScoreBot and
ProjectUser.signUpRiskScoreFreeTrialAbuse to enforce values are between 0 and
100 using CONSTRAINT ... CHECK (...) NOT VALID in this migration (referencing
the column names signUpRiskScoreBot and signUpRiskScoreFreeTrialAbuse and table
ProjectUser), and create a separate follow-up migration that runs VALIDATE
CONSTRAINT for each new constraint; ensure you add descriptive constraint names
(e.g. chk_projectuser_signupriskscorebot_range and
chk_projectuser_signupriskscorefreetrialabuse_range) in the NOT VALID step so
the subsequent migration can target them for validation.
apps/backend/src/lib/users.tsx (1)

129-133: Type cast is documented but consider a safer alternative.

The comment explains why the cast is needed (create-only fields like risk_scores are stripped by KeyIntersect). While the cast is justified, an alternative would be to define a more precise type for the create path that includes these fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/lib/users.tsx` around lines 129 - 133, The current cast of
createOrUpdate to UsersCrud["Admin"]["Create"] when calling
usersCrudHandlers.adminCreate is unsafe; instead define an explicit create
payload typed as UsersCrud["Admin"]["Create"] (or a small builder/helper that
maps createOrUpdate into that type) so you only include/transform the
create-only fields (e.g., risk_scores) required for creation; update the call to
pass that strongly-typed createPayload to usersCrudHandlers.adminCreate,
referencing the existing createOrUpdate variable, the
usersCrudHandlers.adminCreate call site, and the UsersCrud["Admin"]["Create"]
type to locate where to change.
apps/backend/src/lib/risk-scores.tsx (1)

16-29: Placeholder implementation with unused parameter.

The tenancy parameter is declared but not used, and the implementation contains a TODO marker with test-only logic. This is acceptable for initial scaffolding, but the hardcoded test@example.com check should be replaced with actual risk scoring logic before production use.

Would you like me to open an issue to track implementing the actual risk scoring logic, or is this intentionally deferred to a follow-up PR?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/lib/risk-scores.tsx` around lines 16 - 29,
calculateSignUpRiskScores currently contains a test-only hardcoded email check
and never uses the tenancy parameter; replace the placeholder logic so the
function no longer special-cases "test@example.com" and instead either
implements real scoring using the provided tenancy and context (e.g., consult
tenancy.id or tenancy.features and context.primaryEmail, ip, device info) or, if
real logic is deferred, return a neutral/default SignUpRiskScores and explicitly
reference tenancy (e.g., include tenancy.id in a debug/log statement or use it
in a simple conditional) to avoid an unused parameter; also remove the TODO
marker or convert it into a clear follow-up comment and open a tracked issue to
implement proper risk logic later.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/backend/src/lib/users.tsx`:
- Around line 13-17: SignUpRuleOptions now requires ipAddress but three callers
pass objects without it; either make ipAddress optional on SignUpRuleOptions or
(preferred) update each caller to supply a value (real IP from the request or
null) when constructing the options. Locate the type SignUpRuleOptions and add
ipAddress: string | null where needed, then update the password sign-up handler
(password sign-up), the OTP verification-code handler (otp sign-in
verification), and the OAuth code path that forwards params.signUpRuleOptions to
include an ipAddress field (use request IP extraction or null if unavailable).
Ensure all call sites create a complete SignUpRuleOptions object to satisfy the
type.

In `@apps/e2e/tests/backend/endpoints/api/v1/internal/sign-up-rules-test.test.ts`:
- Around line 85-128: Two earlier tests still call
niceBackendFetch("/api/v1/internal/sign-up-rules-test") without the now-required
risk_scores body and will 400; update those POST bodies to include risk_scores:
{ bot: 0, free_trial_abuse: 0 } so the endpoint receives the required object
before rule evaluation, mirroring the new test that uses risk_scores with
nonzero values.

In `@apps/e2e/tests/backend/endpoints/api/v1/risk-scores.test.ts`:
- Around line 526-568: The two tests "should default risk scores to 0 for
server-created users" and "should allow setting risk scores when creating users
via server API" are missing a dedicated project context and must call
Project.createAndSwitch() before performing requests; update each test to create
and switch to a fresh project (via Project.createAndSwitch()) at the start of
the test so the subsequent niceBackendFetch calls operate in an isolated project
context and avoid cross-test leakage.

---

Nitpick comments:
In
`@apps/backend/prisma/migrations/20260308000000_add_signup_risk_scores/migration.sql`:
- Around line 2-5: Add CHECK constraints on ProjectUser.signUpRiskScoreBot and
ProjectUser.signUpRiskScoreFreeTrialAbuse to enforce values are between 0 and
100 using CONSTRAINT ... CHECK (...) NOT VALID in this migration (referencing
the column names signUpRiskScoreBot and signUpRiskScoreFreeTrialAbuse and table
ProjectUser), and create a separate follow-up migration that runs VALIDATE
CONSTRAINT for each new constraint; ensure you add descriptive constraint names
(e.g. chk_projectuser_signupriskscorebot_range and
chk_projectuser_signupriskscorefreetrialabuse_range) in the NOT VALID step so
the subsequent migration can target them for validation.

In `@apps/backend/src/lib/risk-scores.tsx`:
- Around line 16-29: calculateSignUpRiskScores currently contains a test-only
hardcoded email check and never uses the tenancy parameter; replace the
placeholder logic so the function no longer special-cases "test@example.com" and
instead either implements real scoring using the provided tenancy and context
(e.g., consult tenancy.id or tenancy.features and context.primaryEmail, ip,
device info) or, if real logic is deferred, return a neutral/default
SignUpRiskScores and explicitly reference tenancy (e.g., include tenancy.id in a
debug/log statement or use it in a simple conditional) to avoid an unused
parameter; also remove the TODO marker or convert it into a clear follow-up
comment and open a tracked issue to implement proper risk logic later.

In `@apps/backend/src/lib/users.tsx`:
- Around line 129-133: The current cast of createOrUpdate to
UsersCrud["Admin"]["Create"] when calling usersCrudHandlers.adminCreate is
unsafe; instead define an explicit create payload typed as
UsersCrud["Admin"]["Create"] (or a small builder/helper that maps createOrUpdate
into that type) so you only include/transform the create-only fields (e.g.,
risk_scores) required for creation; update the call to pass that strongly-typed
createPayload to usersCrudHandlers.adminCreate, referencing the existing
createOrUpdate variable, the usersCrudHandlers.adminCreate call site, and the
UsersCrud["Admin"]["Create"] type to locate where to change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2a3ed090-6fd1-4398-98c3-1e3c0481ddc4

📥 Commits

Reviewing files that changed from the base of the PR and between 485fa9d and b9290d5.

📒 Files selected for processing (18)
  • apps/backend/prisma/migrations/20260308000000_add_signup_risk_scores/migration.sql
  • apps/backend/prisma/schema.prisma
  • apps/backend/prisma/seed.ts
  • apps/backend/src/app/api/latest/internal/sign-up-rules-test/route.tsx
  • apps/backend/src/app/api/latest/users/crud.tsx
  • apps/backend/src/lib/cel-evaluator.ts
  • apps/backend/src/lib/risk-scores.tsx
  • apps/backend/src/lib/users.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx
  • apps/dashboard/src/components/rule-builder/condition-builder.tsx
  • apps/dashboard/src/lib/cel-visual-parser.test.ts
  • apps/dashboard/src/lib/cel-visual-parser.ts
  • apps/e2e/tests/backend/endpoints/api/v1/internal/sign-up-rules-test.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/risk-scores.test.ts
  • packages/stack-shared/src/interface/crud/users.ts
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
  • packages/template/src/lib/stack-app/users/index.ts

Comment on lines 13 to 17
export type SignUpRuleOptions = {
authMethod: 'password' | 'otp' | 'oauth' | 'passkey',
oauthProvider?: string,
ipAddress: string | null,
};
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all callers of createOrUpgradeAnonymousUserWithRules and check if they pass ipAddress
rg -n -A 10 'createOrUpgradeAnonymousUserWithRules\(' --type ts | head -100

Repository: stack-auth/stack-auth

Length of output: 4091


🏁 Script executed:

# Get the complete call at password sign-up route
sed -n '57,75p' apps/backend/src/app/api/latest/auth/password/sign-up/route.tsx

Repository: stack-auth/stack-auth

Length of output: 593


🏁 Script executed:

# Get the complete call at OTP handler
sed -n '110,130p' apps/backend/src/app/api/latest/auth/otp/sign-in/verification-code-handler.tsx

Repository: stack-auth/stack-auth

Length of output: 635


🏁 Script executed:

# Get the complete call at OAuth
sed -n '207,225p' apps/backend/src/lib/oauth.tsx

Repository: stack-auth/stack-auth

Length of output: 623


🏁 Script executed:

# View the SignUpRuleOptions type definition in users.tsx
sed -n '13,17p' apps/backend/src/lib/users.tsx

Repository: stack-auth/stack-auth

Length of output: 211


🏁 Script executed:

# Check if there are any other callers we missed
rg -c 'createOrUpgradeAnonymousUserWithRules\(' --type ts

Repository: stack-auth/stack-auth

Length of output: 277


Critical: ipAddress is now required in SignUpRuleOptions but three callers don't provide it.

The SignUpRuleOptions type defines ipAddress: string | null as a required field, but the following callers pass incomplete objects:

  • apps/backend/src/app/api/latest/auth/password/sign-up/route.tsx:67 — passes only { authMethod: 'password' }
  • apps/backend/src/app/api/latest/auth/otp/sign-in/verification-code-handler.tsx:120 — passes only { authMethod: 'otp' }
  • apps/backend/src/lib/oauth.tsx:217 — passes params.signUpRuleOptions which lacks ipAddress

This will cause TypeScript compilation errors at all three sites.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/lib/users.tsx` around lines 13 - 17, SignUpRuleOptions now
requires ipAddress but three callers pass objects without it; either make
ipAddress optional on SignUpRuleOptions or (preferred) update each caller to
supply a value (real IP from the request or null) when constructing the options.
Locate the type SignUpRuleOptions and add ipAddress: string | null where needed,
then update the password sign-up handler (password sign-up), the OTP
verification-code handler (otp sign-in verification), and the OAuth code path
that forwards params.signUpRuleOptions to include an ipAddress field (use
request IP extraction or null if unavailable). Ensure all call sites create a
complete SignUpRuleOptions object to satisfy the type.

Comment on lines +85 to +128
it("evaluates risk score conditions", async ({ expect }) => {
await Project.createAndSwitch();
await Project.updateConfig({
"auth.signUpRules.block-high-bot-score": {
enabled: true,
displayName: "Block high bot score",
priority: 1,
condition: "riskScores.bot >= 80",
action: {
type: "reject",
message: "High bot risk",
},
},
"auth.signUpRulesDefaultAction": "allow",
});

const response = await niceBackendFetch("/api/v1/internal/sign-up-rules-test", {
method: "POST",
accessType: "admin",
body: {
email: "risk@example.com",
auth_method: "password",
risk_scores: {
bot: 90,
free_trial_abuse: 10,
},
},
});

expect(response.status).toBe(200);
expect(response.body).toMatchObject({
context: {
risk_scores: {
bot: 90,
free_trial_abuse: 10,
},
},
outcome: {
should_allow: false,
decision: "reject",
decision_rule_id: "block-high-bot-score",
},
});
});
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.

⚠️ Potential issue | 🟠 Major

Update the older request fixtures for the now-required risk_scores body.

This new case is correct, but the two existing niceBackendFetch("/api/v1/internal/sign-up-rules-test") calls earlier in this file still omit risk_scores. The endpoint contract now requires that object, so those tests will 400 before the rule logic runs. Add a zeroed { bot: 0, free_trial_abuse: 0 } payload to the older request bodies as well.

🧪 Minimal follow-up
       body: {
         email: "user@example.com",
         auth_method: "password",
+        risk_scores: {
+          bot: 0,
+          free_trial_abuse: 0,
+        },
       },

Apply the same change to the other existing POST body in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/backend/endpoints/api/v1/internal/sign-up-rules-test.test.ts`
around lines 85 - 128, Two earlier tests still call
niceBackendFetch("/api/v1/internal/sign-up-rules-test") without the now-required
risk_scores body and will 400; update those POST bodies to include risk_scores:
{ bot: 0, free_trial_abuse: 0 } so the endpoint receives the required object
before rule evaluation, mirroring the new test that uses risk_scores with
nonzero values.

Comment on lines +526 to +568
describe("server-created users", () => {
it("should default risk scores to 0 for server-created users", async ({ expect }) => {
const createResponse = await niceBackendFetch("/api/v1/users", {
method: "POST",
accessType: "server",
body: {
primary_email: `server-created-${generateSecureRandomString(8)}@example.com`,
},
});

expect(createResponse.status).toBe(201);
expect(createResponse.body.risk_scores).toEqual({
sign_up: {
bot: 0,
free_trial_abuse: 0,
},
});
});

it("should allow setting risk scores when creating users via server API", async ({ expect }) => {
const createResponse = await niceBackendFetch("/api/v1/users", {
method: "POST",
accessType: "server",
body: {
primary_email: `risky-${generateSecureRandomString(8)}@example.com`,
risk_scores: {
sign_up: {
bot: 55,
free_trial_abuse: 42,
},
},
},
});

expect(createResponse.status).toBe(201);
expect(createResponse.body.risk_scores).toEqual({
sign_up: {
bot: 55,
free_trial_abuse: 42,
},
});
});
});
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.

⚠️ Potential issue | 🟡 Minor

Missing Project.createAndSwitch() in server-created users tests.

Unlike all other test blocks in this file, these two tests don't establish their own project context. They'll use whatever project context remains from previously executed tests, which could lead to flaky behavior or unexpected test isolation issues.

🧪 Proposed fix to add project context
 describe("server-created users", () => {
   it("should default risk scores to 0 for server-created users", async ({ expect }) => {
+    await Project.createAndSwitch({
+      config: { credential_enabled: true },
+    });
+
     const createResponse = await niceBackendFetch("/api/v1/users", {
       method: "POST",
       accessType: "server",
   it("should allow setting risk scores when creating users via server API", async ({ expect }) => {
+    await Project.createAndSwitch({
+      config: { credential_enabled: true },
+    });
+
     const createResponse = await niceBackendFetch("/api/v1/users", {
       method: "POST",
       accessType: "server",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/backend/endpoints/api/v1/risk-scores.test.ts` around lines 526
- 568, The two tests "should default risk scores to 0 for server-created users"
and "should allow setting risk scores when creating users via server API" are
missing a dedicated project context and must call Project.createAndSwitch()
before performing requests; update each test to create and switch to a fresh
project (via Project.createAndSwitch()) at the start of the test so the
subsequent niceBackendFetch calls operate in an isolated project context and
avoid cross-test leakage.

Copy link
Copy Markdown

@vercel vercel Bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestions:

  1. Missing required ipAddress property in signUpRuleOptions object for OAuth callback routes causes TypeScript build failure.
  1. TypeScript error: SignUpRuleOptions type requires ipAddress property but multiple auth handlers don't provide it
  1. Missing required ipAddress property in signUpRuleOptions objects causes TypeScript compilation errors in multiple sign-up routes

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.

4 participants