Skip to content

Enhance sign-up rules with derived country code and risk score handling#1232

Open
mantrakp04 wants to merge 18 commits intofraud-protectionfrom
fraud-protection-country-code
Open

Enhance sign-up rules with derived country code and risk score handling#1232
mantrakp04 wants to merge 18 commits intofraud-protectionfrom
fraud-protection-country-code

Conversation

@mantrakp04
Copy link
Collaborator

Stacked on fraud-protection.

  • Introduced new functions for calculating derived country codes and risk scores
  • Updated API request handling to support optional risk scores
  • Improved validation for user input in the dashboard
  • Updated tests to cover new functionality and ensure proper handling of country codes and risk scores in user creation and updates.

Made with Cursor

…ng. Introduced new functions for calculating derived country codes and risk scores, updated API request handling to support optional risk scores, and improved validation for user input in the dashboard. Updated tests to cover new functionality and ensure proper handling of country codes and risk scores in user creation and updates.
@vercel
Copy link

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

@CLAassistant
Copy link

CLAassistant commented Mar 9, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 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: 1928cd4f-148d-4a5f-a2d4-bfb96981e8ee

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-country-code
📝 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.

@mantrakp04
Copy link
Collaborator Author

@greptile-ai review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR extends the sign-up rules system with derived country code support and improved risk score handling. A countryCode field is added to the ProjectUser table, propagated through the CRUD layer and ServerUser SDK type, and surfaced in the sign-up rule CEL evaluation context. The dashboard rule builder gains a CountryCodeSelect combobox for countryCode conditions, and the test simulator supports country code and risk score overrides.

Key changes and observations:

  • Country code derivation pipeline (users.tsx, end-users.tsx): getSpoofableEndUserLocation now returns user-controlled header data for all requests (previously null for spoofable requests). The country used in CEL evaluation is therefore always spoofable — this is acknowledged in CLAUDE-KNOWLEDGE.md as intentional but should be surfaced clearly in project documentation so admins understand geo rules provide soft filtering, not hard enforcement.

  • countryCodeToPersist logic gap (users.tsx line 96): The existing-country-code preservation guard only applies when the current user is_anonymous. For non-anonymous currentUser paths (e.g. OAuth account linking), the derived country code from the current request overwrites whatever was previously stored — contrary to the field's "captured at sign-up time" semantics.

  • normalizeConditionValue numeric path (cel-visual-parser.ts lines 90–103): If a countryCode condition node somehow carries a numeric value, the function returns it unchanged, causing conditionToCel to generate countryCode == 42 — a CEL expression that silently never matches instead of failing validation.

  • Test simulator cannot force empty country code (sign-up-rules-test/route.tsx line 97): req.body.country_code ?? derivedCountryCode means explicitly passing null falls through to the server-derived value. There is no way to pin countryCode = "" in the test context when the server has geo headers available.

  • Several issues raised in the previous review round have been addressed: StackAssertionError has been replaced with plain Error for user-input validation, the render-phase throw in condition-builder.tsx is replaced with a graceful [] fallback, empty in_list arrays now fail validation for countryCode, and CountryCodeField no longer coerces null to "".

  • The test stub in getDerivedSignUpCountryCode that maps [email protected] to a country code (flagged previously) remains in production code.

Confidence Score: 3/5

  • Mergeable with caution — the country-code persistence logic has a gap for non-anonymous users, and the test simulator cannot force an empty country code context.
  • The core infrastructure (migration, schema, CEL evaluator, SDK type) is solid and well-tested. Three new logic issues were found not covered by previous review threads: (1) countryCodeToPersist only guards anonymous users, potentially overwriting a non-anonymous user's stored country code on subsequent auth events; (2) the test simulator cannot force countryCode = "" via explicit null due to the ?? operator; (3) a numeric countryCode value in normalizeConditionValue produces silently-invalid CEL instead of a validation error. Additionally, several items from prior review rounds remain open (test stub in production, index-based list keys, lack of API-level format validation for country_code updates).
  • apps/backend/src/lib/users.tsx (countryCodeToPersist guard), apps/backend/src/app/api/latest/internal/sign-up-rules-test/route.tsx (null override semantics), apps/dashboard/src/lib/cel-visual-parser.ts (numeric countryCode path)

Important Files Changed

Filename Overview
apps/backend/src/lib/users.tsx Introduces getDerivedSignUpCountryCode (with production-embedded test stub) and countryCodeToPersist logic in createOrUpgradeAnonymousUserWithRules; the preservation guard only protects anonymous users' stored code — non-anonymous users' country code is overwritten on every auth event.
apps/backend/src/app/api/latest/internal/sign-up-rules-test/route.tsx Adds country code and optional risk score overrides to the test simulation endpoint; null ?? derivedCountryCode prevents testers from forcing an empty-country-code context when the server has geo headers, limiting test coverage of "no country" edge cases.
apps/dashboard/src/lib/cel-visual-parser.ts Adds countryCode as a parseable CEL field and introduces normalizeConditionValue for automatic uppercase normalization; numeric countryCode values are returned unchanged, which would produce invalid never-matching CEL expressions rather than a validation error.
apps/backend/src/lib/cel-evaluator.ts Extends SignUpRuleContext and createSignUpRuleContext with countryCode; null maps to "" for safe CEL evaluation; well-tested with in-file vitest cases.
apps/dashboard/src/components/rule-builder/condition-builder.tsx Adds dedicated country code UI with CountryCodeSelect dropdowns and per-item removal; several issues from the previous review round (render-phase throw, stuck single-item list, index-based keys) remain partially addressed — the throw is replaced but index keys are still used.
packages/stack-shared/src/interface/crud/users.ts Adds country_code field to read/update/create schemas; still uses plain countryCodeSchema.nullable() without a regex constraint at the API schema level for updates, leaving server-side callers able to write arbitrary strings (previously flagged); also userWebhookEvent factory loses type specificity on the type literal field.
apps/backend/src/lib/end-users.tsx Changed getSpoofableEndUserLocation to return spoofed headers (previously returned null for spoofable requests); makes geo-based sign-up rules trivially bypassable via request header manipulation — previously flagged and acknowledged as intentional in CLAUDE-KNOWLEDGE.md.
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx Adds country code and risk score override fields to the test simulator; validation now correctly uses throw new Error(...) instead of StackAssertionError for user-input validation, addressing a previously flagged issue.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Sign-up Request] --> B{signUpRuleOptions\ncountryCode !== null?}
    B -- Yes: explicit override --> C[Use provided countryCode]
    B -- No: derive --> D[getSpoofableEndUserLocation\nfrom request geo headers]
    D --> E[getDerivedSignUpCountryCode]
    E --> F{email matches\[email protected]?}
    F -- Yes --> G[Use email-embedded CC\ntest stub priority]
    F -- No --> H{requestLocation\ncountryCode valid?}
    H -- Yes --> I[Use geo-derived CC]
    H -- No --> J[countryCode = null]
    C --> K[createSignUpRuleContext\nnull → empty string]
    G --> K
    I --> K
    J --> K
    K --> L[evaluateSignUpRules\nCEL evaluation with countryCode]
    L --> M{Rule result}
    M -- Rejected --> N[throw SignUpRejected]
    M -- Allowed/Restricted --> O{currentUser is anonymous\nAND has country_code?}
    O -- Yes --> P[Preserve anonymous user's\nexisting country_code]
    O -- No --> Q[Use derived countryCode]
    P --> R[Persist to DB via adminUpdate/adminCreate]
    Q --> R
Loading

Comments Outside Diff (1)

  1. apps/backend/src/app/api/latest/internal/sign-up-rules-test/route.tsx, line 97-98 (link)

    null country_code override cannot force empty context in test simulator

    req.body.country_code ?? derivedCountryCode evaluates to derivedCountryCode whenever the body contains null. This means that when a tester explicitly passes country_code: null (meaning "don't override, derive it") and the request arrives from an IP with valid geo headers, there is no way to simulate a sign-up where countryCode == "" (the "no country" case). Any test that passes null from a geo-equipped machine will always exercise the derived-country path — it can't pin countryCode to empty.

    This matters when testing rules that explicitly match the empty string (e.g., a catch-all fallback) or when verifying that a geo rule does not match for unknown origins.

    Consider using undefined as the sentinel for "use derived" and null for "force empty", or add a dedicated force_empty_country_code: boolean parameter:

    const countryCode = req.body.country_code !== undefined
      ? req.body.country_code           // null → force ""  via createSignUpRuleContext
      : derivedCountryCode;             // undefined → use server-derived

    And update the body schema so country_code is .optional() (omittable) instead of .defined() (required-but-nullable).

    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: 5e4da43

…entralized `countryCodeSchema` for validation and normalization, updated relevant components and APIs to utilize this schema, and replaced ad-hoc country code handling with a new `CountryCodeSelect` component. Enhanced tests to ensure proper validation of country codes in various contexts, including sign-up rules and user dialogs.
@mantrakp04
Copy link
Collaborator Author

@greptile-ai review

…o, removing email-based inference. Update related functions and tests to reflect this change, ensuring proper handling of null values. Preserve existing country codes during anonymous user upgrades when geo is unavailable.
@mantrakp04
Copy link
Collaborator Author

@greptile-ai review

…ountryCodeSet` for improved consistency across the application. Update relevant imports and functions to utilize the new set, ensuring proper handling of country codes in user sign-up and validation processes.
@mantrakp04
Copy link
Collaborator Author

@greptile-ai review

… country code inference. Update related API and tests to accommodate the new email parameter, ensuring accurate country code derivation from both request geo and email tags. Maintain existing functionality for handling null values.
…d country code handling. Introduced new validation logic for `restricted_by_admin` fields and updated risk score schema. Enhanced consistency and maintainability of user-related data structures.
…e user schema to include risk scores. Enhanced tests to validate new fields and ensure proper handling of country codes and risk scores across various endpoints.
…. Enhanced test coverage for user data structures to ensure proper handling of new attributes across various scenarios.
…res fields. Improved test coverage for user data structures, ensuring proper handling of new attributes across various scenarios.
@mantrakp04
Copy link
Collaborator Author

@greptile-ai review

@mantrakp04
Copy link
Collaborator Author

@greptile-ai The risk-score stub in calculateSignUpRiskScores is intentional. This PR is focused on laying the groundwork — wiring up country-code derivation, the rule evaluation pipeline, CEL expression support, and the dashboard condition builder UI. The actual risk-scoring pipeline (bot detection, free trial abuse scoring) is being implemented in a follow-up PR that will be stacked on top of this one.

The { bot: 0, freeTrialAbuse: 0 } return for non-test users is a deliberate placeholder, not a missed implementation. The [email protected] case exists solely to validate that the rule evaluation machinery works end-to-end in tests.

Please re-review with this context in mind.

…structures. Adjusted status codes and error messages for team membership actions, and removed unnecessary fields from user responses. Enhanced test coverage for user data structures, ensuring proper handling of new attributes across various scenarios.
…ded tests to verify behavior of 'in_list' and 'equals' operations for country codes. Updated user dialog and country code select components to handle null values and improve validation logic. Refactored condition builder to ensure proper handling of single values and arrays for country code conditions.
…ttributes and country code. Updated conditions for setting `restricted_by_admin` and added `country_code` to the user response structure. Enhanced test snapshots to reflect these changes, ensuring accurate representation of user data.
Comment on lines 90 to +103
return value.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
}

function normalizeConditionValue(condition: ConditionNode): ConditionNode['value'] {
if (condition.field !== 'countryCode') {
return condition.value;
}

if (Array.isArray(condition.value)) {
return condition.value.map(normalizeCountryCode);
}

if (typeof condition.value === 'number') {
return condition.value;
Copy link
Contributor

Choose a reason for hiding this comment

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

normalizeConditionValue silently passes numeric values for countryCode field

When condition.field === 'countryCode' and typeof condition.value === 'number', the function returns the number unchanged (falls through the array and string guards). conditionToCel then uses this number to build the CEL expression. Depending on the operator, it would generate:

  • countryCode == 42 (number literal, not a quoted string)
  • countryCode in [42] (list of numbers, not strings)

Both would silently evaluate to false in the CEL runtime because the context variable countryCode is always a string — there's no type error, just a rule that never matches. The current UI never produces a numeric country-code value, but this is reachable through parseCelToVisualTree if it parses an existing expression and returns a ConditionNode with a numeric value, or after a future refactor.

// current — number passes through unchanged
if (typeof condition.value === 'number') {
  return condition.value;   // ← will produce countryCode == 42
}

Consider throwing or coercing to an empty string so the condition renders as invalid rather than silently never-matching:

if (typeof condition.value === 'number') {
  return '';  // treated as invalid by validateCountryCodeValue
}

Fix in Claude Code Fix in Cursor Fix in Codex

Comment on lines +96 to +98
const countryCodeToPersist = currentUser?.is_anonymous && currentUser.country_code != null
? currentUser.country_code
: countryCode;
Copy link
Contributor

Choose a reason for hiding this comment

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

countryCodeToPersist preserves only anonymous users' existing country code, not non-anonymous users'

The guard on line 96 reads:

const countryCodeToPersist = currentUser?.is_anonymous && currentUser.country_code != null
  ? currentUser.country_code
  : countryCode;

This correctly preserves the stored code when upgrading an anonymous user that already has a country_code set. However, for non-anonymous currentUser instances (e.g. an OAuth sign-in where the account already exists in the oauth.tsx caller's params.currentUser), currentUser?.is_anonymous is false, so countryCodeToPersist becomes the freshly-derived countryCode from the current request. If this is non-null, line 135 (...(countryCodeToPersist !== null ? { country_code: countryCodeToPersist } : {})) will overwrite the user's previously-stored country code.

The field is documented as "captured at sign-up time" but could be updated on every sign-in if the user's geo changes. Apply the same preservation logic to all users that already have a non-null country_code:

const countryCodeToPersist = (currentUser?.country_code != null)
  ? currentUser.country_code
  : countryCode;

Fix in Claude Code Fix in Cursor Fix in Codex

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.

2 participants