Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@N2D4
Copy link
Contributor

@N2D4 N2D4 commented Jan 28, 2026

Note

High Risk
Changes core sign-up and user creation/upgrade flows (password/OTP/OAuth) and introduces new DB constraints and user restriction semantics that can block registrations. Also adds CEL expression evaluation and ClickHouse dependencies for logging/stats, increasing the chance of misconfiguration or runtime failures impacting onboarding.

Overview
Adds a sign-up rules enforcement path across password, OTP, and OAuth sign-ups by evaluating CEL-based conditions and either allowing, rejecting (KnownErrors.SignUpRejected), or marking newly-created/upgraded users as admin-restricted.

Introduces admin restriction fields on ProjectUser (restrictedByAdmin, public reason, private details) with DB constraints, surfaces them through the Users CRUD APIs, and extends computeRestrictedStatus to treat admin-restricted users as restricted (with a new restricted_by_administrator reason).

Adds observability + admin UI for sign-up rules: a new $sign-up-rule-trigger event persisted to ClickHouse, an internal /internal/sign-up-rules-stats endpoint that returns 48h rule trigger counts for sparklines, and a new dashboard page to create/edit/reorder rules via a visual builder; also adds a user-page dialog/banner to view and manage manual restrictions.

Written by Cursor Bugbot for commit 7686d2b. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features
    • Sign-up Rules engine with visual rule builder, in-project CRUD, drag-reordering, and analytics.
    • Admin User Restriction management (set/clear public reason and private details) surfaced in UI and APIs.
    • Sign-up Rules analytics endpoint and dashboard sparklines.
  • APIs & Schema
    • User payloads and config schemas extended to include admin restriction fields and sign-up rules.
  • Tests
    • Comprehensive E2E and unit tests for sign-up rules, parser, and analytics.
  • Docs
    • Editorial guidance additions to AGENTS.md.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings January 28, 2026 06:20
@vercel
Copy link

vercel bot commented Jan 28, 2026

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

Project Deployment Actions Updated (UTC)
stack-backend Ready Ready Preview, Comment Jan 30, 2026 8:00pm
stack-dashboard Ready Ready Preview, Comment Jan 30, 2026 8:00pm
stack-demo Ready Ready Preview, Comment Jan 30, 2026 8:00pm
stack-docs Ready Ready Preview, Comment Jan 30, 2026 8:00pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

Adds a sign-up rules engine (CEL-based) with evaluation, async analytics, admin-imposed user restriction fields and UI, DB migrations, dashboard rule editor and stats, and updates auth flows to pass sign-up context.

Changes

Cohort / File(s) Summary
CEL evaluation & sign-up rules (backend)
apps/backend/src/lib/cel-evaluator.ts, apps/backend/src/lib/sign-up-rules.ts, apps/backend/src/lib/users.tsx
New CEL evaluator and sign-up rules evaluation, async rule trigger logging, and rules-driven user creation path (createOrUpgradeAnonymousUserWithRules).
Database schema & migrations
apps/backend/prisma/schema.prisma, apps/backend/prisma/migrations/*
Adds restrictedByAdmin, restrictedByAdminReason, restrictedByAdminPrivateDetails to ProjectUser and enforces consistency via CHECK constraint.
Auth flows: pass sign-up context
apps/backend/src/app/api/latest/auth/.../*, apps/backend/src/lib/oauth.tsx
Auth handlers (OAuth, Apple native, OTP, password) now pass signUpRuleOptions/authMethod/oauthProvider into user-creation calls.
User CRUD, types & schemas
apps/backend/src/app/api/latest/users/crud.tsx, packages/stack-shared/src/interface/crud/users.ts, packages/stack-shared/src/interface/crud/current-user.ts
Threaded admin-restriction fields through CRUD: computeRestrictedStatus update, read/update/create payloads, and public schemas include restricted_by_admin*.
Sign-up rules config & types
packages/stack-shared/src/config/schema.ts, packages/stack-shared/src/config/schema-fuzzer.test.ts, packages/stack-shared/src/interface/crud/sign-up-rules.ts
Adds signUpRules and signUpRulesDefaultAction to config schema, sorting by priority; introduces SignUpRule/SignUpRuleAction types.
Dashboard rule builder & parser (frontend)
apps/dashboard/src/components/rule-builder/condition-builder.tsx, apps/dashboard/src/components/rule-builder/index.tsx, apps/dashboard/src/lib/cel-visual-parser.ts, apps/dashboard/src/lib/cel-visual-parser.test.ts
New ConditionBuilder UI and CEL visual parser for bi-directional tree↔CEL editing with escaping and nested groups.
Sign-up rules dashboard page
apps/dashboard/src/app/.../sign-up-rules/page-client.tsx, .../page.tsx
Full CRUD UI with inline editors, drag reordering, per-rule analytics sparklines, default-action card, and persistence hooks.
Admin restriction UI (dashboard)
apps/dashboard/src/app/.../users/[userId]/page-client.tsx
RestrictionBanner, RestrictedStatusRow, and RestrictionDialog for setting/clearing admin restrictions (public reason + private details).
Analytics, events & ClickHouse integration
apps/backend/src/lib/events.tsx, apps/backend/src/lib/clickhouse.tsx, apps/backend/src/app/api/latest/internal/sign-up-rules-stats/route.tsx, apps/backend/src/app/api/latest/internal/clickhouse/migrate-events/route.tsx, apps/backend/src/app/api/latest/internal/analytics/query/route.ts
Adds SignUpRuleTrigger event type, stats endpoint aggregating hourly counts, logging path adjustments; removed isClickhouseConfigured() export/guards.
E2E & unit tests
apps/e2e/tests/backend/.../sign-up-rules.test.ts, apps/e2e/tests/backend/.../internal/sign-up-rules-stats.test.ts, apps/dashboard/src/lib/cel-visual-parser.test.ts, many updated snapshots
Large test additions: comprehensive sign-up rule engine tests, stats endpoint tests, CEL parser tests, and snapshot updates to include new admin fields.
Frontend wiring & symbols
apps/dashboard/src/lib/apps-frontend.tsx, apps/dashboard/src/lib/stack-app-internals.ts, various overview/user pages`
Adds "Sign-up Rules" nav item and consolidates stackAppInternalsSymbol into a shared lib; replaces local symbol declarations with import.
Type & template surface changes
packages/template/src/lib/stack-app/..., packages/stack-shared/src/schema-fields.ts, packages/stack-shared/src/known-errors.tsx, packages/stack-shared/src/utils/types.tsx
Extends restrictedReason unions and ServerUser types, adds SignUpRejected error, updates restrictedReasonTypes, and modifies KeyIntersect type semantics.
Dependencies & misc
apps/backend/package.json, apps/dashboard/package.json, docker/dev-postgres-with-extensions/Dockerfile, package.json, .vscode/settings.json, AGENTS.md, claude/CLAUDE-KNOWLEDGE.md
Adds cel-js and @dnd-kit/utilities deps, PostgreSQL statement_timeout, updates clean script, spell-check words, and agent guidance edits.
Snapshot serializer
apps/e2e/tests/snapshot-serializer.ts
Added hourly_counts to stripped fields for snapshots.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant AuthEndpoint as Auth Endpoint
    participant RuleEval as Sign-up Rule Evaluator
    participant CELEngine as CEL Engine
    participant UserCRUD as User CRUD
    participant Database as Database

    User->>AuthEndpoint: Sign-up request (email, method, provider?)
    AuthEndpoint->>RuleEval: createSignUpRuleContext(...)
    RuleEval->>CELEngine: evaluateCelExpression(condition, context)
    CELEngine->>CELEngine: Preprocess string methods and evaluate
    alt Rule matches
        RuleEval->>RuleEval: Apply action (allow/reject/restrict/log)
        RuleEval->>AuthEndpoint: Return decision / restricted flag
        RuleEval->>Database: Async log sign-up-rule-trigger event
    else No match
        RuleEval-->>AuthEndpoint: Default action (allow/reject)
    end
    AuthEndpoint->>UserCRUD: createOrUpgradeAnonymousUserWithRules(..., signUpRuleOptions)
    UserCRUD->>Database: Persist user with restricted_by_admin* fields
    Database-->>UserCRUD: Persisted user
    UserCRUD-->>AuthEndpoint: User object
    AuthEndpoint-->>User: Response (success/rejected/restricted)
Loading
sequenceDiagram
    actor Admin
    participant Dashboard as Dashboard UI
    participant AdminAPI as Admin API
    participant Database as Database

    Admin->>Dashboard: Open user → RestrictionDialog
    Admin->>Dashboard: Submit public reason + private details
    Dashboard->>AdminAPI: PATCH /users/{id} (restricted_by_admin=true, ...)
    AdminAPI->>Database: Update ProjectUser
    Database-->>AdminAPI: OK
    AdminAPI-->>Dashboard: Updated user
    Dashboard->>Dashboard: Show RestrictionBanner / RestrictedStatus
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • BilalG1

Poem

🐰 I hopped through CELs and parsed each rule,
I nudged the DB to keep secrets cool,
Admins can mark with reason and care,
Dashboards drag, charts sparkline in the air,
Hooray — sign-ups now hop through gates with style! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.06% 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 'Sign up rules' is vague and generic, using only a feature name without specifics about the change's scope, impact, or main contribution. Expand the title to be more descriptive, e.g., 'Add configurable sign-up rules system with CEL-based conditions and admin UI' to clarify the scope and intent.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive and provides clear context on changes, risk level, objectives, and implementation details. However, it lacks the required template structure from the repository guidelines.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sign-up-rules

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.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 28, 2026

Greptile Overview

Greptile Summary

This PR implements a comprehensive signup rules system using CEL (Common Expression Language) expressions to control user registration. The feature allows administrators to define conditions and actions for signup attempts.

Key Changes:

  • Backend: Added CEL expression evaluator (cel-evaluator.ts) with preprocessing for string methods, signup rule evaluation engine (signup-rules.ts), and integration into all auth flows (password, OAuth, OTP)
  • Database: New SignupRuleTrigger analytics table and restrictedByAdmin fields on ProjectUser with proper constraints
  • Frontend: Visual rule builder with drag-and-drop interface, condition editor, and real-time analytics sparklines
  • Config Schema: New signupRules config with priority-based ordering, CEL conditions, and 5 action types (allow, reject, restrict, log, add_metadata)
  • Tests: Comprehensive E2E test suite (2000+ lines) covering rule evaluation, priority ordering, all action types, and edge cases

Critical Issue Found:

The CEL string generation in cel-visual-parser.ts does not escape quotes in user-provided values, creating an injection vulnerability where malicious input can alter rule logic.

Confidence Score: 3/5

  • Not safe to merge due to injection vulnerability in CEL string generation
  • Score reflects critical security issue with quote escaping that must be fixed. The feature implementation is solid with comprehensive tests, proper database migrations, and good architecture. However, the injection vulnerability in the dashboard's CEL visual parser allows users to craft malicious inputs that can alter signup rule logic.
  • Pay close attention to apps/dashboard/src/lib/cel-visual-parser.ts - the quote escaping issue must be fixed before merging

Important Files Changed

Filename Overview
apps/dashboard/src/lib/cel-visual-parser.ts CEL string generation lacks quote escaping, allowing injection attacks
apps/backend/src/lib/cel-evaluator.ts CEL expression evaluator for signup rules with method preprocessing
apps/backend/src/lib/signup-rules.ts Main signup rule evaluation logic with proper action handling
apps/backend/src/lib/users.tsx Added user creation wrapper with signup rule evaluation
apps/backend/prisma/schema.prisma Added SignupRuleTrigger analytics table and restrictedByAdmin fields

Sequence Diagram

sequenceDiagram
    participant User
    participant Client
    participant API as Auth API
    participant Rules as Signup Rules Engine
    participant CEL as CEL Evaluator
    participant DB as Database
    participant Analytics as Analytics Logger

    User->>Client: Submit signup (email, password/OAuth)
    Client->>API: POST /auth/password/sign-up (or OAuth callback)
    
    API->>Rules: evaluateAndApplySignupRules(tenancy, context)
    
    Rules->>Rules: Get sorted rules (priority, then alphabetical)
    
    loop For each enabled rule
        Rules->>CEL: evaluateCelExpression(condition, context)
        CEL->>CEL: Preprocess method calls (contains, startsWith, etc.)
        CEL->>CEL: Evaluate transformed expression
        CEL-->>Rules: true/false
        
        alt Rule matches
            Rules->>Analytics: logRuleTrigger() (async, non-blocking)
            Analytics->>DB: INSERT INTO SignupRuleTrigger
            Rules-->>API: SignupRuleResult (action: reject/restrict/add_metadata/log/allow)
            Note over Rules: Stop evaluation after first match
        end
    end
    
    alt No rules matched
        Rules-->>API: Default action (allow/reject)
    end
    
    alt Action is "reject"
        API-->>Client: 403 SIGN_UP_REJECTED
        Client-->>User: Signup rejected
    else Action is "restrict"
        API->>DB: Create user with restrictedByAdmin=true
        DB-->>API: User created
        API-->>Client: 200 OK (restricted user)
        Client-->>User: Signup successful (restricted)
    else Action is "add_metadata"
        API->>DB: Create user with metadata in client/server/client_read_only
        DB-->>API: User created
        API-->>Client: 200 OK
        Client-->>User: Signup successful
    else Action is "allow" or "log"
        API->>DB: Create user normally
        DB-->>API: User created
        API-->>Client: 200 OK
        Client-->>User: Signup successful
    end
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link

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

This pull request implements a comprehensive "Sign up rules" feature that allows administrators to configure CEL (Common Expression Language) based rules to control who can sign up for the application. The feature includes rule evaluation, user restriction mechanisms, metadata injection, analytics tracking, and a visual rule builder in the dashboard.

Changes:

  • Added CEL-based signup rule evaluation system with support for conditions on email, domain, auth method, and OAuth provider
  • Introduced new user restriction fields (restrictedByAdmin, restrictedByAdminReason, restrictedByAdminPrivateDetails) to support rule-based user restrictions
  • Implemented analytics tracking for signup rule triggers with a dashboard interface showing rule activity
  • Created a visual rule builder UI for non-technical users to create and manage signup rules without writing CEL expressions

Reviewed changes

Copilot reviewed 36 out of 37 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
pnpm-lock.yaml Added dependencies: cel-js (CEL evaluator), @dnd-kit/utilities (drag-and-drop), updated Supabase packages
packages/stack-shared/src/config/schema.ts Added signup rules schema with action types, conditions, priorities, and default action configuration
packages/stack-shared/src/known-errors.tsx Added SignUpRejected error for rule-based signup rejection
packages/stack-shared/src/interface/crud/users.ts Extended user CRUD schemas with admin restriction fields and validation
packages/template/src/lib/stack-app/users/index.ts Added restrictedByAdmin fields to user types and updated restriction reason types
apps/backend/src/lib/cel-evaluator.ts Implemented CEL expression evaluation with method call preprocessing for string operations
apps/backend/src/lib/signup-rules.ts Core signup rule evaluation logic with priority-based rule matching
apps/backend/src/lib/users.tsx Added createOrUpgradeAnonymousUserWithRules wrapper for signup rule evaluation
apps/backend/prisma/schema.prisma Added restrictedByAdmin fields to ProjectUser and SignupRuleTrigger table for analytics
apps/backend/prisma/migrations/* Database migrations for admin restriction fields and signup rule triggers
apps/backend/src/app/api/latest/internal/signup-rules/route.tsx Analytics endpoint for fetching signup rule trigger statistics
apps/backend/src/app/api/latest/auth/*/route.tsx Integrated signup rule evaluation into all auth signup endpoints
apps/dashboard/src/lib/cel-visual-parser.ts Visual CEL expression parser for converting between tree structure and CEL strings
apps/dashboard/src/components/rule-builder/* React components for visual rule building interface
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/signup-rules/* Dashboard page for managing signup rules with drag-and-drop reordering
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx Added UI for viewing and managing user admin restrictions
apps/e2e/tests/backend/endpoints/api/v1/auth/signup-rules.test.ts Comprehensive test suite covering rule evaluation, priorities, actions, and edge cases
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Copy link
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: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/backend/src/app/api/latest/users/crud.tsx (1)

100-122: Filter out admin-restricted users when include_restricted=false.

computeRestrictedStatus treats restrictedByAdmin as a restricted condition (alongside isAnonymous and email verification), but the list endpoint only filters based on email verification. This means users marked restrictedByAdmin: true will appear in results when include_restricted=false, creating an inconsistency. The function itself notes: "when you implement this function, make sure to also update the filter in the list users endpoint."

Add restrictedByAdmin: false to the where clause when !includeRestricted:

+    const shouldFilterRestrictedByAdmin = !includeRestricted;
     const where = {
       tenancyId: auth.tenancy.id,
       ...
+      ...shouldFilterRestrictedByAdmin ? {
+        restrictedByAdmin: false,
+      } : {},
       ...shouldFilterRestrictedByEmail ? {
🤖 Fix all issues with AI agents
In `@apps/backend/src/app/api/latest/internal/signup-rules/route.tsx`:
- Around line 59-101: The hourly_counts output omits hours with zero activity,
which skews per-hour averages and sparklines; update the aggregation to produce
a complete 48-hour series per rule: after building ruleData.hourlyMap in the
loop that processes triggers (symbols: triggers, trigger.triggeredAt,
ruleTriggersMap, ruleData.hourlyMap), compute the 48 hourly keys (e.g., ending
at now or the latest triggeredAt) and map each key to
ruleData.hourlyMap.get(key) ?? 0, then use that full array when constructing
ruleTriggers (symbol: ruleTriggers) so hourly_counts always contains 48 hour
entries (hour + count) in sorted order. Ensure the hour key format matches the
existing ISO/truncated format used by hourKey.

In `@apps/backend/src/lib/cel-evaluator.ts`:
- Around line 76-83: The 'matches' branch in cel-evaluator.ts constructs a
RegExp from user input (arg) and runs test(varValue), which can trigger ReDoS;
change this to validate or sandbox regexes before executing: either (a) use a
safe-regex check on arg (e.g., safeRegex(arg)) and skip/deny unsafe patterns, or
(b) run the regex.test in a short-timeout sandbox (measure time around RegExp
execution or offload to a worker with a timeout) and treat over-time executions
as failures; update the case 'matches' handler to reference arg, varValue and
result and ensure exceptions/timeouts set result = false and log the offending
pattern.

In `@apps/backend/src/lib/signup-rules.ts`:
- Around line 64-83: The logRuleTrigger function is persisting raw PII
(context.email, emailDomain, authMethod, oauthProvider) into analytics; update
the metadata construction in logRuleTrigger so that email is pseudonymized
(e.g., hash the email using a secure one-way hash like SHA-256 with a salt or
redact it) and only store non-identifying fields needed for rules (e.g., store a
hashedEmail and/or emailDomainHash instead of plain email/emailDomain), keep
authMethod and oauthProvider if non-identifying or map them to enums, and
add/modify a retention/note field documenting that these fields are
pseudonymized and how long they are kept; locate changes around the
globalPrismaClient.signupRuleTrigger.create call and the metadata object in
logRuleTrigger to implement hashing/redaction and retention documentation.
- Around line 123-130: The sort can produce NaN when a rule's optional priority
is undefined; update the extraction inside the comparator used by
sortedRuleEntries to assert presence instead of silently falling back: for both
priorityA and priorityB (derived from rules entries in sortedRuleEntries)
replace direct property access with a nullish-coalescing assertion (e.g., use
priority = entry[1].priority ?? throwErr(...) or throw new Error(...)) so
missing priorities throw (reference SignupRuleConfig for the field) and keep the
tie-breaker using stringCompare(a[0], b[0]) unchanged.

In `@apps/backend/src/lib/users.tsx`:
- Around line 5-6: The code is silently falling back to an empty string for
primary_email when evaluating signup rules; instead, update the evaluation call
sites (e.g., where createSignupRuleContext and evaluateAndApplySignupRules /
SignupRuleMetadataEntry are used) to fail fast if primary_email is missing by
throwing a clear error (use a helper like throwErr or replace the ""/'' fallback
with `primary_email ?? throwErr("primary_email required for signup rule
evaluation")`), and adjust the input types or checks at the top-level so callers
must provide primary_email (also fix similar fallbacks around the other
occurrences at the block referenced by lines 45-47).

In `@apps/dashboard/package.json`:
- Around line 24-26: The review notes that while `@dnd-kit/core`,
`@dnd-kit/sortable`, and `@dnd-kit/utilities` are compatible, the package
react-resizable-panels should be bumped to avoid React 19 peer-dep warnings;
update the dependency entry for react-resizable-panels in package.json to ^2.1.7
(or later) so its peerDependencies explicitly declare React 19 support, then run
yarn/npm install and test to ensure no new warnings or breakages.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/signup-rules/page-client.tsx:
- Around line 112-115: avgPerHour is computed using data.length which only
counts active-hour buckets and inflates the per-hour rate; change the divisor to
the full fixed window (48) or use a provided totalHours field from the API if
available. Locate avgPerHour and rateLabel (where avgPerHour = totalCount /
Math.max(data.length, 1) and rateLabel is set) and replace Math.max(data.length,
1) with a constant 48 (or use totalHours) so avgPerHour reflects the full 48h
window before formatting into rateLabel.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/signup-rules/page.tsx:
- Around line 1-6: Remove the incorrect "use server" directive at the top of the
file — leave the file as an async Server Component by default; keep the import
of PageClient and the export default async function Page() that returns
<PageClient /> (so update the module to just import PageClient and export the
Page function without any "use server" or other directives).

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx:
- Around line 309-335: Both handlers (handleSaveAndRestrict and
handleRemoveRestriction) currently swallow update errors via try/finally; wrap
the await user.update(...) in a try/catch inside each function so errors are
caught, display a blocking error alert with the caught error message (e.g.,
using the app's alert/toast API or window.alert) and then rethrow or return
after alerting so the error is not silently swallowed; keep the existing finally
block to call setIsSaving(false) and only call onOpenChange(false) on success
(move it into the try after await).

In `@apps/dashboard/src/components/rule-builder/condition-builder.tsx`:
- Around line 146-155: The icon-only remove Button in condition-builder.tsx (the
Button rendering TrashIcon when showRemove is true) lacks an accessible name;
update the Button(s) that call onRemove to include an aria-label (e.g.,
aria-label="Remove condition" or similar) or add visually hidden text so screen
readers announce the action, and apply the same change to the other remove
Button instance in this file (the second TrashIcon/Button rendering that also
invokes onRemove).
- Around line 305-318: The current render mutates the incoming prop by aliasing
value to rootGroup and assigning rootGroup.children = [createEmptyCondition()],
which mutates parent state; instead, avoid mutation in render by keeping the
existing rootGroup logic pure (use spreading/cloning when building a non-group
root) and move the "ensure default condition" logic into a useEffect that runs
when value changes: inside useEffect, if value.type === 'group' and
value.children.length === 0, call onChange with a new GroupNode (copying value
but setting children: [createEmptyCondition()]); reference rootGroup, value,
createEmptyCondition, and onChange when implementing the effect to sync state
without direct mutation.

In `@apps/dashboard/src/lib/cel-visual-parser.ts`:
- Around line 66-99: The generated CEL strings in conditionToCel produce invalid
or unsafe output when value contains quotes or backslashes; add an escape helper
(e.g., escapeCelString) and use it wherever value is interpolated: escape double
quotes and backslashes and coerce non-string values to strings before escaping;
update the in_list branch to map through escapeCelString for each element and
join, and replace all direct `"${value}"` interpolations in conditionToCel with
`"${escapeCelString(value)}"` so CEL syntax remains valid and prevents
injection.

In `@packages/stack-shared/src/config/schema.ts`:
- Around line 138-147: The signupRuleSchema allows enabled rules with empty or
undefined condition; update signupRuleSchema so the condition field is required
and non-empty when enabled is true — use yup's conditional validation (yup.when
on 'enabled') on the condition string to call required() and enforce a non-empty
trimmed string (e.g., min(1) or test for non-blank) when enabled === true,
leaving the condition optional when enabled is false or undefined; modify the
existing signupRuleSchema's condition entry accordingly to reference the
'enabled' flag.
- Around line 126-129: The signupRuleMetadataEntrySchema currently uses
yupMixed<string | number | boolean>() for the value field which does not enforce
runtime scalar types; update the value validator in
signupRuleMetadataEntrySchema (the value key) to explicitly assert scalar types
at runtime — replace the generic mixed with a mixed().test (or a lazy/oneOf
schema) that checks typeof value is 'string' or 'number' or 'boolean' (and
rejects arrays/objects/null/undefined), and keep .defined() so downstream code
that expects scalar metadata cannot receive arrays/objects; reference the value
field and signupRuleMetadataEntrySchema when making the change.
🧹 Nitpick comments (14)
docker/dev-postgres-with-extensions/Dockerfile (1)

56-56: Consider removing backticks for clarity; use a Dockerfile comment instead.

While backticks with # comments are technically parsed by /bin/sh without error (the # creates a comment that expands to an empty string), this syntax is unconventional and confusing. Moving the note outside the command string makes the intent clearer:

Suggested change
-    -c statement_timeout=30s `# In production this is higher, but better safe than sorry during dev` \
+    -c statement_timeout=30s \
+    # In production this is higher, but better safe than sorry during dev
packages/stack-shared/src/config/schema.ts (1)

131-136: Make action-specific fields conditional.

metadata and message are currently allowed for any action type, which weakens validation. Consider requiring them only when relevant.

♻️ Suggested tightening
 const signupRuleActionSchema = yupObject({
   type: yupString().oneOf(['allow', 'reject', 'restrict', 'log', 'add_metadata']).defined(),
-  metadata: yupRecord(yupString(), signupRuleMetadataEntrySchema).optional(),
-  message: yupString().optional(), // for reject action custom message (internal use, not shown to user)
+  metadata: yupRecord(yupString(), signupRuleMetadataEntrySchema).when("type", {
+    is: "add_metadata",
+    then: s => s.defined(),
+    otherwise: s => s.optional(),
+  }),
+  message: yupString().when("type", {
+    is: "reject",
+    then: s => s.defined(),
+    otherwise: s => s.optional(),
+  }), // for reject action custom message (internal use, not shown to user)
 });
apps/backend/src/lib/cel-evaluator.ts (1)

178-192: Edge case: emails without @ symbol.

Line 184 handles emails without @ by returning an empty string for emailDomain, which is reasonable. However, consider whether this edge case should be validated earlier in the signup flow rather than silently producing an empty domain.

 export function createSignupRuleContext(params: {
   email: string,
   authMethod: 'password' | 'otp' | 'oauth' | 'passkey',
   oauthProvider?: string,
 }): SignupRuleContext {
   const email = params.email;
-  const emailDomain = email.includes('@') ? email.split('@').pop() ?? '' : '';
+  const atIndex = email.lastIndexOf('@');
+  const emailDomain = atIndex !== -1 ? email.slice(atIndex + 1) : '';

   return {
     email,
     emailDomain,
     authMethod: params.authMethod,
     oauthProvider: params.oauthProvider ?? '',
   };
 }
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)

442-455: Avoid unsound cast for admin-restriction fields.

The assertion bypasses UsersCrud typing and can silently propagate undefined if client/server versions drift. Prefer updating shared CRUD types and/or guarding the fields to fail fast when missing.

♻️ Suggested guard (fails fast on missing fields)
-    const crudWithAdminRestriction = crud as typeof crud & {
-      restricted_by_admin: boolean,
-      restricted_by_admin_reason: string | null,
-      restricted_by_admin_private_details: string | null,
-    };
+    const crudWithAdminRestriction = crud as typeof crud & {
+      restricted_by_admin?: boolean,
+      restricted_by_admin_reason?: string | null,
+      restricted_by_admin_private_details?: string | null,
+    };
@@
-      restrictedByAdmin: crudWithAdminRestriction.restricted_by_admin,
-      restrictedByAdminReason: crudWithAdminRestriction.restricted_by_admin_reason,
-      restrictedByAdminPrivateDetails: crudWithAdminRestriction.restricted_by_admin_private_details,
+      restrictedByAdmin: crudWithAdminRestriction.restricted_by_admin ?? throwErr("restricted_by_admin missing — update UsersCrud types/server response"),
+      restrictedByAdminReason: crudWithAdminRestriction.restricted_by_admin_reason ?? null,
+      restrictedByAdminPrivateDetails: crudWithAdminRestriction.restricted_by_admin_private_details ?? null,

As per coding guidelines: Never silently use fallback values when type errors occur. Update types or throw errors instead. Use ?? throwErr(...) over non-null assertions with clear error messages explaining the assumption.

apps/dashboard/src/components/rule-builder/condition-builder.tsx (1)

44-47: Prefer Map for PREDEFINED_VALUES.

This aligns with the project guideline and keeps lookups explicit.

♻️ Suggested refactor
-const PREDEFINED_VALUES: Partial<Record<ConditionField, string[]>> = {
-  authMethod: ['password', 'otp', 'oauth', 'passkey'],
-  oauthProvider: ['google', 'github', 'microsoft', 'facebook', 'discord', 'apple', 'linkedin', 'gitlab', 'bitbucket', 'spotify', 'twitch', 'x'],
-};
+const PREDEFINED_VALUES = new Map<ConditionField, string[]>([
+  ['authMethod', ['password', 'otp', 'oauth', 'passkey']],
+  ['oauthProvider', ['google', 'github', 'microsoft', 'facebook', 'discord', 'apple', 'linkedin', 'gitlab', 'bitbucket', 'spotify', 'twitch', 'x']],
+]);
@@
-  const predefinedValues = PREDEFINED_VALUES[condition.field];
+  const predefinedValues = PREDEFINED_VALUES.get(condition.field);

As per coding guidelines: Use ES6 maps instead of records wherever possible.

Also applies to: 62-63

apps/backend/src/app/api/latest/internal/signup-rules/route.tsx (1)

47-57: Consider DB-side aggregation for high-volume tenants.

findMany pulls all triggers for 48h and aggregates in JS. For large tenants this can be expensive. Consider selecting only needed columns and using Prisma groupBy or a raw date_trunc('hour', triggeredAt) query to aggregate by rule/action/hour to reduce memory and latency.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/signup-rules/page-client.tsx (2)

625-653: Avoid try/catch-all in analytics fetch.

The fetch uses a broad catch and console.debug. Prefer runAsynchronously/runAsynchronouslyWithAlert with an onError handler and keep finally for isLoading so errors are surfaced without a catch-all.

🔧 Example refactor
-    const fetchAnalytics = async () => {
-      try {
-        const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/signup-rules', {
-          method: 'GET',
-        });
-        if (cancelled) return;
-
-        const data = await response.json();
-        ...
-        setAnalytics(analyticsMap);
-      } catch (e) {
-        console.debug('Failed to fetch signup rules analytics:', e);
-      } finally {
-        if (!cancelled) {
-          setIsLoading(false);
-        }
-      }
-    };
-
-    runAsynchronously(fetchAnalytics());
+    runAsynchronously(async () => {
+      try {
+        const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/signup-rules', {
+          method: 'GET',
+        });
+        if (cancelled) return;
+        const data = await response.json();
+        ...
+        setAnalytics(analyticsMap);
+      } finally {
+        if (!cancelled) setIsLoading(false);
+      }
+    }, {
+      onError: (error) => {
+        // handle/report error here
+      },
+    });

As per coding guidelines, "Never try-catch-all, never void a promise, and never .catch(console.error) or similar. Use loading indicators instead when UI is involved. If async is necessary, use runAsynchronously or runAsynchronouslyWithAlert".


630-632: Avoid the any cast for StackApp internals.

Line 630: (stackAdminApp as any)[stackAppInternalsSymbol] drops type safety. Consider a typed helper for the internal request channel, or add a short comment explaining why any is unavoidable here.

As per coding guidelines, "Avoid the any type. When necessary, leave a comment explaining why it's being used and why the type system fails at that point".

apps/backend/src/lib/signup-rules.ts (1)

64-89: Avoid broad try/catch + console.error in rule logging/eval.

Both logRuleTrigger and the rule-eval loop catch-all and log to console. Since runAsynchronously already captures rejections and evaluateCelExpression returns false on error, prefer centralized error handling instead of catch-all blocks.

As per coding guidelines, "Never try-catch-all, never void a promise, and never .catch(console.error) or similar. Use loading indicators instead when UI is involved. If async is necessary, use runAsynchronously or runAsynchronouslyWithAlert".

Also applies to: 154-157

apps/e2e/tests/backend/endpoints/api/v1/auth/signup-rules.test.ts (2)

1524-1529: Encode interpolated path segments in URLs.

When building /api/v1/users/${userId}, use encodeURIComponent(userId) (or urlString) to keep URL handling consistent and safe.

🔧 Suggested tweak
-    const userResponse = await niceBackendFetch(`/api/v1/users/${userId}`, {
+    const userResponse = await niceBackendFetch(`/api/v1/users/${encodeURIComponent(userId)}`, {
       method: "GET",
       accessType: "admin",
     });

As per coding guidelines, "Use urlString template literals or encodeURIComponent() instead of normal string interpolation for URLs, for consistency".


81-84: Prefer inline snapshots for response bodies.

For structured error payloads, inline snapshots keep expectations compact and aligned with test conventions.

🔧 Example
-    expect(response.body).toMatchObject({
-      code: 'SIGN_UP_REJECTED',
-    });
+    expect(response.body).toMatchInlineSnapshot(`
+      {
+        "code": "SIGN_UP_REJECTED",
+      }
+    `);

As per coding guidelines, "Prefer .toMatchInlineSnapshot over other selectors when writing tests. Check snapshot-serializer.ts to understand how snapshots are formatted".

apps/dashboard/src/lib/cel-visual-parser.ts (3)

248-261: Consider validating the parsed field against known ConditionField values.

The regex matches any word character sequence for the field name, then casts it to ConditionField without validation. If the CEL contains an unrecognized field, it will be silently accepted with an invalid type.

♻️ Proposed validation helper
+const VALID_FIELDS: readonly ConditionField[] = ['email', 'emailDomain', 'authMethod', 'oauthProvider'];
+
+function isValidField(field: string): field is ConditionField {
+  return VALID_FIELDS.includes(field as ConditionField);
+}
+
 function parseCondition(expr: string): ConditionNode | null {
   const trimmed = expr.trim();
 
   // Match patterns like: field == "value"
   const equalsMatch = trimmed.match(/^(\w+)\s*==\s*"([^"]*)"$/);
-  if (equalsMatch) {
+  if (equalsMatch && isValidField(equalsMatch[1])) {
     return {
       type: 'condition',
       id: generateNodeId(),
-      field: equalsMatch[1] as ConditionField,
+      field: equalsMatch[1],
       operator: 'equals',
       value: equalsMatch[2],
     };
   }
   // Apply similar pattern to other matches...

323-343: In-list parsing does not handle values containing commas.

The simple comma split will incorrectly parse ["a,b", "c"] as three items instead of two. This is a known limitation that likely won't affect typical signup rule values (emails, domains, providers), but worth documenting.

Consider adding a comment noting this limitation, or implementing a proper tokenizer if comma-containing values become a requirement.


394-410: Type cast in updateNodeInTree could mask incompatible updates.

The Partial<RuleNode> type allows passing properties from either ConditionNode or GroupNode, and the as RuleNode cast will accept incompatible combinations (e.g., updating a condition node with children). Consider narrowing the type or adding runtime validation.

♻️ Type-safe alternative using overloads
export function updateNodeInTree(tree: RuleNode, nodeId: string, updates: Partial<ConditionNode>): RuleNode;
export function updateNodeInTree(tree: RuleNode, nodeId: string, updates: Partial<GroupNode>): RuleNode;
export function updateNodeInTree(tree: RuleNode, nodeId: string, updates: Partial<ConditionNode | GroupNode>): RuleNode {
  if (tree.id === nodeId) {
    // Optionally validate that updates.type matches tree.type if present
    return { ...tree, ...updates } as RuleNode;
  }
  // ... rest unchanged
}

Copy link
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

🤖 Fix all issues with AI agents
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx:
- Around line 144-223: The metadata editor currently stringifies all metadata on
load (see MetadataEditorEntry and initialMetadata) causing type loss; update the
MetadataEditorEntry shape to include the original type (e.g., { key, value,
target, valueType }) and change initialMetadata to store the raw value and its
type instead of String(entry.value); keep metadataEntries state as these richer
entries; then update handleSave to reconstruct SignUpRuleMetadataEntry values by
converting the string-edited value back to the original valueType (or reusing
the raw value if untouched) before building the metadata object so
numbers/booleans are preserved when saving.
- Around line 360-369: The onClick currently passes the async handler handleSave
directly which can void the returned promise and swallow rejections; update the
Button to call runAsynchronouslyWithAlert(() => handleSave()) (or wrap
handleSave in an inline async wrapper passed to runAsynchronouslyWithAlert) so
any rejection is caught and surfaced to the user; locate the Button using the
handleSave reference in page-client.tsx and replace onClick={handleSave} with
the wrapped invocation using runAsynchronouslyWithAlert.
- Around line 628-629: The fetchAnalytics function currently wraps its network
call in a try-catch that swallows errors; remove the internal catch so errors
propagate, and when invoking it use runAsynchronouslyWithAlert(fetchAnalytics())
so failures surface via the alert helper; keep the existing finally block in
fetchAnalytics to reset loading state (refer to the fetchAnalytics function and
the runAsynchronouslyWithAlert helper).
🧹 Nitpick comments (8)
apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts (1)

1472-1484: Consider documenting expected normalized behavior.

The test currently accepts both 200 and 403 for uppercase "ADMIN", which documents uncertainty about email normalization. If the system has a defined normalization behavior (lowercase), consider updating this test to assert the expected outcome once behavior is confirmed.

apps/backend/src/lib/cel-evaluator.ts (2)

17-21: Remove unused _email_lower from ExtendedContext.

The ExtendedContext type defines _email_lower but this field is never populated or used anywhere in the code. Either implement it for case-insensitive matching or remove the dead code.

♻️ Proposed fix
-// Extended context with helper functions for string operations
-type ExtendedContext = SignUpRuleContext & {
-  // Pre-computed helpers for common patterns
-  _email_lower: string,
-};

41-43: Regex pattern doesn't handle single quotes or escaped characters.

The pattern only matches double-quoted string arguments like email.contains("test"). Single-quoted strings email.contains('test') and escaped quotes email.contains("test\"value") won't be processed.

♻️ Enhanced pattern (optional)
-  const methodPattern = /(\w+)\.(contains|startsWith|endsWith|matches)\s*\(\s*"([^"]+)"\s*\)/g;
+  // Match both single and double quoted strings, handling basic escapes
+  const methodPattern = /(\w+)\.(contains|startsWith|endsWith|matches)\s*\(\s*(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)')\s*\)/g;

Note: If CEL expressions are admin-controlled and documented to use double quotes only, the current implementation is sufficient.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (1)

292-294: Multiple as any casts indicate incomplete type definitions.

The ServerUser type doesn't include restrictedByAdmin, restrictedByAdminReason, and restrictedByAdminPrivateDetails, requiring as any casts throughout the component. This bypasses type safety and could lead to runtime errors if the API changes.

Consider either:

  1. Extending the ServerUser type to include these fields (if they're part of the stable API)
  2. Creating a local extended type until the SDK types are updated
// Temporary type extension until SDK types are updated
type ServerUserWithRestriction = ServerUser & {
  restrictedByAdmin?: boolean;
  restrictedByAdminReason?: string | null;
  restrictedByAdminPrivateDetails?: string | null;
};

Also applies to: 435-437

apps/backend/src/app/api/latest/internal/sign-up-rules/route.tsx (1)

74-79: Action type safety relies on runtime behavior.

The cast trigger.action as keyof typeof actionCounts assumes the database always contains valid action values. If an unexpected action is stored, it would silently be ignored due to the if (action in actionCounts) check.

Consider logging or capturing unexpected action values for debugging:

       const action = trigger.action as keyof typeof actionCounts;
       if (action in actionCounts) {
         actionCounts[action]++;
+      } else {
+        // Log unexpected action for debugging
+        console.warn(`Unexpected signup rule action: ${trigger.action}`);
       }
apps/backend/src/lib/sign-up-rules.ts (2)

86-89: Avoid console.error in catch blocks per coding guidelines.

The coding guidelines state to never use .catch(console.error) or similar. Consider using a proper logging infrastructure that can be monitored/alerted on, or propagate the error if it should be visible. If truly swallowing errors intentionally, at minimum use a structured logger.

♻️ Suggested approach
   } catch (e) {
-    // Don't fail the signup if logging fails
-    console.error('Failed to log sign-up rule trigger:', e);
+    // Don't fail the signup if logging fails - use structured logging
+    // TODO: Replace with proper logging infrastructure (e.g., logger.error)
+    void e; // Intentionally swallowed - analytics logging is non-critical
   }

Or better, if a logging service exists:

} catch (e) {
  logger.error('Failed to log sign-up rule trigger', { error: e, tenancyId, ruleId });
}

Based on learnings: "Never try-catch-all, never void a promise, and never .catch(console.error) or similar."


154-157: Avoid console.error in catch blocks per coding guidelines.

Similar to the earlier catch block, console.error should be replaced with proper structured logging. CEL evaluation errors may indicate misconfigured rules that admins should be alerted about.

♻️ Suggested approach
     } catch (e) {
-      // Log CEL evaluation error but continue to next rule
-      console.error(`CEL evaluation error for rule ${ruleId}:`, e);
+      // Log CEL evaluation error but continue to next rule
+      // TODO: Use structured logging for monitoring/alerting on rule misconfigurations
+      // logger.warn('CEL evaluation error', { ruleId, error: e, tenancyId: tenancy.id });
     }

Based on learnings: "Never try-catch-all, never void a promise, and never .catch(console.error) or similar."

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)

630-632: Replace as any with a narrow internal type.

The stackAppInternalsSymbol pattern provides internal SDK access but requires as any to bypass type checking. This can be avoided by defining a type for the internals object, improving type safety and maintainability.

♻️ Suggested change
 const stackAppInternalsSymbol = Symbol.for("StackAuth--DO-NOT-USE-OR-YOU-WILL-BE-FIRED--StackAppInternals");
+type StackAppInternals = {
+  sendRequest: (path: string, init: { method: string }) => Promise<Response>;
+};

-        const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/sign-up-rules', {
+        const internals = (stackAdminApp as { [stackAppInternalsSymbol]: StackAppInternals })[stackAppInternalsSymbol];
+        const response = await internals.sendRequest('/internal/sign-up-rules', {
           method: 'GET',
         });

Copy link
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: 1

🤖 Fix all issues with AI agents
In `@apps/dashboard/src/lib/cel-visual-parser.ts`:
- Around line 259-351: The condition parsers (equalsMatch, notEqualsMatch,
matchesMatch, endsWithMatch, startsWithMatch, containsMatch, inListMatch)
currently use [^"]* so escaped quotes/backslashes (e.g. \" or \\) are not
unescaped and round‑trip is broken; add an unescapeCelString helper (e.g.
unescape sequences like \\ and \") and change each regex to capture escaped
sequences using ((?:\\.|[^"\\])*) instead of ([^"]*), then call
unescapeCelString(...) on the captured value when building the returned
condition objects and on each quoted item in the inListMatch items; ensure
generateNodeId usage remains the same.
🧹 Nitpick comments (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (1)

293-295: Avoid any for restriction fields; make the new properties explicit.

Casting user to any and defaulting restrictedByAdmin* values hides type gaps and can silently mask missing data. Please extend ServerUser (or use a narrowed local type) and access these fields without any, using an explicit guard if absence is unexpected.

🧩 Example approach (local narrowing)
+type ServerUserRestrictionFields = {
+  restrictedByAdmin?: boolean;
+  restrictedByAdminReason?: string | null;
+  restrictedByAdminPrivateDetails?: string | null;
+};
+const restrictionUser = user as ServerUser & ServerUserRestrictionFields;
-const restrictedByAdmin = (user as any).restrictedByAdmin ?? false;
-const restrictedByAdminReason = (user as any).restrictedByAdminReason ?? null;
-const restrictedByAdminPrivateDetails = (user as any).restrictedByAdminPrivateDetails ?? null;
+const restrictedByAdmin = restrictionUser.restrictedByAdmin ?? false;
+const restrictedByAdminReason = restrictionUser.restrictedByAdminReason ?? null;
+const restrictedByAdminPrivateDetails = restrictionUser.restrictedByAdminPrivateDetails ?? null;

As per coding guidelines: Avoid the any type. When necessary, leave a comment explaining why it's being used and why the type system fails at that point; Never silently use fallback values when type errors occur. Update types or throw errors instead. Use ?? throwErr(...) over non-null assertions with clear error messages explaining the assumption.

Also applies to: 440-442

apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts (1)

81-84: Consider inline snapshots for structured response assertions.

For response bodies like SIGN_UP_REJECTED, inline snapshots improve readability and make regressions more visible during test updates.

As per coding guidelines: Prefer .toMatchInlineSnapshot over other selectors when writing tests. Check snapshot-serializer.ts to understand how snapshots are formatted.

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)

617-633: Avoid any when accessing SDK internals.

Using (stackAdminApp as any) discards type safety. Consider a minimal internal interface (or a helper accessor) so the unsafe surface is localized and documented.

🧩 Minimal narrowing
+type StackAdminAppInternals = {
+  sendRequest: (path: string, init: RequestInit) => Promise<Response>;
+};
+const internals = (stackAdminApp as unknown as Record<typeof stackAppInternalsSymbol, StackAdminAppInternals>)[stackAppInternalsSymbol];
-const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/sign-up-rules', {
+const response = await internals.sendRequest('/internal/sign-up-rules', {
   method: 'GET',
 });

As per coding guidelines: Avoid the any type. When necessary, leave a comment explaining why it's being used and why the type system fails at that point.

Copy link
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

🤖 Fix all issues with AI agents
In `@apps/backend/package.json`:
- Line 117: Remove the redundant "@types/re2" dependency from package.json:
locate the dependency entry "@types/re2": "^1.10.8" and delete it, then run your
package manager (npm/yarn/pnpm) to update lockfile and reinstall so the project
uses the built-in types from "re2" (v1.23.1) instead of the deprecated `@types`
package.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx:
- Around line 516-531: The edit and delete icon-only Buttons (the Button
components wrapping PencilSimpleIcon and TrashIcon with onClick handlers onEdit
and onDelete) lack accessible names; add clear aria-label attributes (or a
visually hidden text node) to each Button such as aria-label="Edit rule" and
aria-label="Delete rule" (or include the rule name dynamically) so screen
readers can announce the action; update the Button instances that render
PencilSimpleIcon and TrashIcon to include these aria labels.
- Around line 337-345: The trash icon button used to remove metadata entries
(Button with onClick={() => removeMetadataEntry(index)} and
disabled={metadataEntries.length <= 1}) lacks an accessible name; update the
Button to include an accessible label (for example add aria-label={`Remove
metadata entry ${index + 1}`} or a title prop, or wrap a visually hidden text
inside the button) so screen readers can announce its purpose while preserving
the visual icon-only appearance.
🧹 Nitpick comments (5)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (2)

293-295: Add comments explaining the as any casts.

Per coding guidelines, when using any type, leave a comment explaining why it's being used. These casts suggest the restrictedByAdmin* fields aren't yet in the ServerUser type definition.

📝 Suggested documentation
-  const restrictedByAdmin = (user as any).restrictedByAdmin ?? false;
-  const restrictedByAdminReason = (user as any).restrictedByAdminReason ?? null;
-  const restrictedByAdminPrivateDetails = (user as any).restrictedByAdminPrivateDetails ?? null;
+  // TODO: Remove `as any` once ServerUser type includes admin restriction fields
+  const restrictedByAdmin = (user as any).restrictedByAdmin ?? false;
+  const restrictedByAdminReason = (user as any).restrictedByAdminReason ?? null;
+  const restrictedByAdminPrivateDetails = (user as any).restrictedByAdminPrivateDetails ?? null;

440-442: Consider extracting admin restriction field access to avoid duplication.

The same (user as any).restrictedByAdmin* pattern appears in both RestrictionDialog (lines 293-295) and RestrictionBanner (lines 440-442). Consider extracting a helper function to reduce duplication and centralize the type workaround.

♻️ Suggested helper
// Helper to access admin restriction fields until ServerUser type is updated
function getAdminRestrictionFields(user: ServerUser) {
  // TODO: Remove `as any` once ServerUser type includes these fields
  const u = user as any;
  return {
    restrictedByAdmin: u.restrictedByAdmin ?? false,
    restrictedByAdminReason: u.restrictedByAdminReason ?? null,
    restrictedByAdminPrivateDetails: u.restrictedByAdminPrivateDetails ?? null,
  };
}
apps/dashboard/src/lib/cel-visual-parser.test.ts (1)

131-151: Consider using inline snapshots and strengthen parsing test assertions.

The parsing tests use conditional checks that silently pass if the result type isn't 'condition'. If parsing returns an unexpected type, the inner assertions never run and the test still passes. Per coding guidelines, prefer .toMatchInlineSnapshot for test assertions.

♻️ Suggested refactor using inline snapshots
   describe('CEL to visual tree parsing', () => {
     it('should parse simple equality condition', () => {
       const result = parseCelToVisualTree('email == "[email protected]"');
-      expect(result).toBeDefined();
-      if (result?.type === 'condition') {
-        expect(result.field).toBe('email');
-        expect(result.operator).toBe('equals');
-        expect(result.value).toBe('[email protected]');
-      }
+      expect(result).toMatchInlineSnapshot(`
+        {
+          "field": "email",
+          "id": expect.any(String),
+          "operator": "equals",
+          "type": "condition",
+          "value": "[email protected]",
+        }
+      `);
     });
 
     it('should parse endsWith condition', () => {
       const result = parseCelToVisualTree('email.endsWith("@gmail.com")');
-      expect(result).toBeDefined();
-      if (result?.type === 'condition') {
-        expect(result.field).toBe('email');
-        expect(result.operator).toBe('ends_with');
-        expect(result.value).toBe('@gmail.com');
-      }
+      expect(result).toMatchInlineSnapshot(`
+        {
+          "field": "email",
+          "id": expect.any(String),
+          "operator": "ends_with",
+          "type": "condition",
+          "value": "@gmail.com",
+        }
+      `);
     });
   });
apps/dashboard/src/components/rule-builder/condition-builder.tsx (1)

114-124: The in_list input loses intermediate edits while typing.

Splitting on comma and filtering empty strings on every keystroke removes trailing commas immediately, making it awkward to type lists. Users can't type "value1, " before adding the second value.

♻️ Consider debouncing or only processing on blur
       {condition.operator === 'in_list' ? (
         <input
           type="text"
-          value={Array.isArray(condition.value) ? condition.value.join(', ') : condition.value}
-          onChange={(e) => {
-            const items = e.target.value.split(',').map(s => s.trim()).filter(Boolean);
-            handleValueChange(items);
-          }}
+          value={Array.isArray(condition.value) ? condition.value.join(', ') : condition.value}
+          onChange={(e) => {
+            // Store raw input while typing
+            const rawValue = e.target.value;
+            // Only split/filter on blur or when there's a complete item
+            const items = rawValue.split(',').map(s => s.trim());
+            handleValueChange(items.filter(Boolean));
+          }}
+          onBlur={(e) => {
+            // Clean up on blur
+            const items = e.target.value.split(',').map(s => s.trim()).filter(Boolean);
+            handleValueChange(items);
+          }}
           placeholder="value1, value2, ..."
           className="h-8 px-2 text-sm bg-background/60 border border-border/50 rounded-md flex-1"
         />
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)

629-632: Add comment explaining the any cast for SDK internals access.

Per coding guidelines, avoid the any type. When necessary, leave a comment explaining why it's being used.

♻️ Suggested fix
     const fetchAnalytics = async () => {
       try {
+        // Using `any` cast to access internal SDK method via Symbol.
+        // The public SDK API doesn't expose this endpoint, so we access internals directly.
+        // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
         const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/sign-up-rules', {
           method: 'GET',
         });

Copy link
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: 6

🤖 Fix all issues with AI agents
In `@apps/backend/src/app/api/latest/users/crud.tsx`:
- Around line 618-622: Add the same validation in the onCreate flow that
requires restricted_by_admin_reason and restricted_by_admin_private_details to
only be set when restricted_by_admin is true: before calling
tx.projectUser.create in the onCreate handler, check the incoming data fields
(restricted_by_admin, restricted_by_admin_reason,
restricted_by_admin_private_details) and throw/return a validation error if
reason or private_details are non-empty while restricted_by_admin is false;
mirror the logic used in onUpdate so tx.projectUser.create only receives
reason/details when restricted_by_admin === true.
- Around line 1089-1100: The current validation uses data.restricted_by_admin
!== true and ignores the existing oldUser.restrictedByAdmin, breaking PATCHes
that only update reason fields; change the check to compute an effective flag
(e.g., effectiveRestricted = data.restricted_by_admin ??
oldUser.restrictedByAdmin) and use that in the logic so that when
effectiveRestricted !== true you throw if restrictedByAdminReason or
restrictedByAdminPrivateDetails are provided, and when effectiveRestricted ===
false you clear restrictedByAdminReason and restrictedByAdminPrivateDetails;
update the branch that references data.restricted_by_admin,
oldUser.restrictedByAdmin, restrictedByAdminReason, and
restrictedByAdminPrivateDetails accordingly.

In `@apps/backend/src/lib/sign-up-rules.ts`:
- Around line 118-121: The current code only records rule triggers when
action.type === 'reject', so matching rules with action.type === 'log' are never
sent to analytics; update the branch around
runAsynchronously(logRuleTrigger(tenancy.id, ruleId, context, action)) to also
invoke logRuleTrigger for 'log' actions (e.g., change the condition to if
(action.type === 'reject' || action.type === 'log') or otherwise call
runAsynchronously(logRuleTrigger(...)) for both reject and log) while keeping
the async fire-and-forget behavior and using the existing identifiers
(runAsynchronously, logRuleTrigger, tenancy.id, ruleId, context, action).
- Around line 128-131: The catch-all around CEL evaluation (the block logging
`CEL evaluation error for rule ${ruleId}`) must be tightened: only swallow known
CEL evaluation errors and log them with the structured logger (replace
console.error with the project's logger, e.g., processLogger.error or
logger.error), and rethrow or propagate any unexpected errors. Specifically,
inside the try/catch that evaluates CEL for a rule (referencing ruleId), detect
CEL-specific failures (e.g., by checking error.name or instanceof
CELRuntimeError/EvaluationError) and log those with structured messages; for all
other exceptions, rethrow so they are not silently swallowed. Also ensure the
CEL evaluation is awaited (do not void a promise) so these errors are properly
caught.
- Around line 57-60: The catch-all in the logging path that currently does
console.error('Failed to log sign-up rule trigger:', e) must be removed or
narrowed so we don't swallow errors; update the code around the logging call
that emits "Failed to log sign-up rule trigger" to either remove the try/catch
and let the caller handle the promise rejection, or catch only expected error
types and rethrow after logging; replace console.error with the project's
structured logger (e.g., logger.error) and include the error object, ensuring
any unexpected errors are propagated instead of silently ignored.

In `@apps/dashboard/src/components/rule-builder/condition-builder.tsx`:
- Around line 65-72: The handler handleFieldChange currently resets value to an
empty string for all operator cases which corrupts the expected shape for
operators that expect arrays; change the reset logic so that after computing
operator (using getOperatorsForField and condition.operator) you set value to an
empty array when operator === 'in_list' and to an empty string otherwise, then
call onChange({...condition, field, operator, value}) so in_list values remain
arrays.
🧹 Nitpick comments (5)
packages/stack-shared/src/interface/crud/users.ts (2)

23-42: Consider extracting the validation test to a reusable helper.

The validation test for restricted_by_admin_consistency is duplicated verbatim in both the update schema (here) and the read schema (lines 87-103). Extract it into a shared function to follow DRY principles.

Also, per coding guidelines, the any type on line 29 should have a comment explaining why it's necessary.

♻️ Proposed refactor

Add a shared helper before the schemas:

function restrictedByAdminConsistencyTest(this: yup.TestContext<unknown>, value: unknown): boolean | yup.ValidationError {
  // yup test callbacks receive untyped values; we validate shape at runtime
  if (value == null || typeof value !== 'object') return true;
  const v = value as Record<string, unknown>;
  if (v.restricted_by_admin !== true) {
    if (v.restricted_by_admin_reason != null) {
      return this.createError({ message: "restricted_by_admin_reason must be null when restricted_by_admin is not true" });
    }
    if (v.restricted_by_admin_private_details != null) {
      return this.createError({ message: "restricted_by_admin_private_details must be null when restricted_by_admin is not true" });
    }
  }
  return true;
}

Then use it in both schemas:

-}).defined().test(
-  "restricted_by_admin_consistency",
-  "When restricted_by_admin is not true, reason and private_details must be null",
-  function(this: yup.TestContext<any>, value: any) {
-    if (value == null) return true;
-    // If restricted_by_admin is false or missing, both reason and private_details must be null
-    if (value.restricted_by_admin !== true) {
-      if (value.restricted_by_admin_reason != null) {
-        return this.createError({ message: "restricted_by_admin_reason must be null when restricted_by_admin is not true" });
-      }
-      if (value.restricted_by_admin_private_details != null) {
-        return this.createError({ message: "restricted_by_admin_private_details must be null when restricted_by_admin is not true" });
-      }
-    }
-    return true;
-  }
-);
+}).defined().test(
+  "restricted_by_admin_consistency",
+  "When restricted_by_admin is not true, reason and private_details must be null",
+  restrictedByAdminConsistencyTest
+);

87-103: Duplicate validation test — see earlier comment.

This test is identical to lines 26-42. Once the shared helper is extracted, apply it here as well.

apps/dashboard/src/components/rule-builder/condition-builder.tsx (1)

45-63: Prefer a Map for predefined field values.

Using a Record here conflicts with the Map preference in the guidelines and makes the lookup less explicit.

♻️ Suggested refactor
-const PREDEFINED_VALUES: Partial<Record<ConditionField, string[]>> = {
-  authMethod: ['password', 'otp', 'oauth', 'passkey'],
-  oauthProvider: Array.from(standardProviders),
-};
+const PREDEFINED_VALUES = new Map<ConditionField, string[]>([
+  ['authMethod', ['password', 'otp', 'oauth', 'passkey']],
+  ['oauthProvider', Array.from(standardProviders)],
+]);
-  const predefinedValues = PREDEFINED_VALUES[condition.field];
+  const predefinedValues = PREDEFINED_VALUES.get(condition.field);

As per coding guidelines "Use ES6 maps instead of records wherever possible".

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)

653-656: Add comment explaining the any cast.

The any cast is used to access internal methods via stackAppInternalsSymbol. Per coding guidelines, add a brief comment explaining why the type system fails here.

-      const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/sign-up-rules', {
+      // eslint-disable-next-line `@typescript-eslint/no-unsafe-member-access` -- stackAppInternalsSymbol is not exposed in public types
+      const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest('/internal/sign-up-rules', {
apps/backend/src/lib/users.tsx (1)

85-87: Consider adding a brief comment for the type assertion.

While the comment at line 86 explains the purpose, the as Record<string, unknown> assertion could benefit from a note about why it's safe here.

   // Merge sign-up rule data into createOrUpdate
-  // Use type assertion as we know the structure from UsersCrud
+  // Type assertion is safe: KeyIntersect guarantees these optional metadata fields exist on the intersection type
   const createOrUpdateWithMeta = createOrUpdate as Record<string, unknown>;

@N2D4 N2D4 requested a review from nams1570 January 29, 2026 22:13
Copy link
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: 1

🤖 Fix all issues with AI agents
In `@apps/backend/src/lib/users.tsx`:
- Around line 44-72: evaluateSignUpRules may return shouldAllow=false which is
not handled; before calling createOrUpgradeAnonymousUserWithoutRules you must
check ruleResult.shouldAllow and reject the operation when it's false (e.g.,
throw an appropriate error or return a structured rejection) using the sign-up
rule id for context (ruleResult.restrictedBecauseOfSignUpRuleId) so callers can
distinguish a rejection by rule; add this check immediately after obtaining
ruleResult and only proceed to createOrUpgradeAnonymousUserWithoutRules when
ruleResult.shouldAllow is true.
🧹 Nitpick comments (4)
apps/backend/src/lib/cel-evaluator.ts (1)

172-177: Consider defensive assertion for email domain extraction.

After the email.includes('@') check, split('@').pop() should always return a non-empty string for valid emails. However, edge cases like "@domain.com" would pass the check but produce an empty username.

The current fallback to '' is acceptable given that normalizeEmail (from emails.tsx) validates email format upstream, but a defensive assertion could make the assumption explicit.

Optional: More defensive version
   if (params.email) {
     // Normalize email to match how it's stored in the database
     email = normalizeEmail(params.email);
     // Extract domain from normalized email
-    emailDomain = email.includes('@') ? (email.split('@').pop() ?? '') : '';
+    const parts = email.split('@');
+    // normalizeEmail validates format, so @ must exist with non-empty parts
+    emailDomain = parts.length === 2 ? parts[1] : '';
   }
apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts (1)

1429-1438: Minor: Static email could cause test isolation issues.

The test uses a hardcoded email [email protected] without randomization. If tests run in parallel or leave data behind, this could cause flakiness.

💡 Suggested fix
     // Test spam domain
     const spamResponse = await niceBackendFetch("/api/v1/auth/password/sign-up", {
       method: "POST",
       accessType: "client",
       body: {
-        email: `[email protected]`,
+        email: `user-${generateSecureRandomString(4)}@spam.com`,
         password: generateSecureRandomString(),
       },
     });
apps/dashboard/src/lib/cel-visual-parser.ts (1)

343-347: Minor: Quote matching in list items could accept mismatched quotes.

The regex ^["']((?:\\.|[^"\\])*)["']$ would technically match strings with mismatched quotes like "test'. Since this parser consumes CEL generated by the same module (which consistently uses double quotes), this is unlikely to cause issues in practice.

💡 Stricter quote matching (optional)
       .map(s => {
-        // Remove surrounding quotes
-        const match = s.match(/^["']((?:\\.|[^"\\])*)["']$/);
-        return match ? unescapeCelString(match[1]) : s;
+        // Remove surrounding double quotes (CEL standard)
+        const doubleMatch = s.match(/^"((?:\\.|[^"\\])*)"$/);
+        if (doubleMatch) return unescapeCelString(doubleMatch[1]);
+        // Fallback for single quotes
+        const singleMatch = s.match(/^'((?:\\.|[^'\\])*)'$/);
+        if (singleMatch) return unescapeCelString(singleMatch[1]);
+        return s;
       });
packages/stack-shared/src/utils/types.tsx (1)

66-69: Add type-level assertions to lock in KeyIntersect's required/optional semantics.

The intersection of three mapped types makes shared keys required if required in either T or U, and can narrow incompatible property types to never. Add type assertions to document and prevent silent regressions of this behavior.

✅ Suggested type assertions
 /**
  * Returns a type whose keys are the intersection of the keys of T and U, deeply.
+ * Requiredness: a key is required if it is required in either T or U.
  */
 export type KeyIntersect<T, U> =
   & { [K in keyof T & keyof U]?: T[K] & U[K] }
   & { [K in RequiredKeys<T> & keyof U]: T[K] & U[K] }
   & { [K in RequiredKeys<U> & keyof T]: U[K] & T[K] }
+
+typeAssertIs<KeyIntersect<{ a: string }, { a: string }>, { a: string }>()();
+typeAssertIs<KeyIntersect<{ a?: string }, { a?: string }>, { a?: string }>()();

Copy link
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

🤖 Fix all issues with AI agents
In `@apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts`:
- Around line 311-362: The test’s priority values contradict the stated
ordering: either make the behavior consistent by swapping the priorities in the
Project.updateConfig entries so that 'first-allow' has a higher priority number
than 'second-reject' (set 'first-allow'.priority = 1 and
'second-reject'.priority = 0) if your system treats higher numbers as higher
priority, or if your system treats lower numbers as higher priority, update the
earlier comment that describes priority ordering to state "lower number = higher
priority" so the test expectations match the documented behavior.
- Around line 263-265: The inline snapshot in the test assertions
(expect(response).toMatchInlineSnapshot("TODO")) is a placeholder and must be
replaced with the real snapshot: run the test suite (or the single test in
apps/e2e/tests/backend/endpoints/api/v1/auth/sign-up-rules.test.ts) to
auto-generate the expected snapshot for the response object, then update the
expect(response).toMatchInlineSnapshot call with the generated inline snapshot
content so the assertion matches the actual response.
- Around line 223-265: The test's inline comment in the "should evaluate rules
by priority order (higher number = higher priority)" case is incorrect; update
the comment to read "lower number = higher priority" (or similar) so it matches
the backend implementation documented in sign-up-rules.ts and the test
expectation (priority 0 wins over priority 1) — locate the test case with the
description "should evaluate rules by priority order (higher number = higher
priority)" and edit the comment text accordingly.
🧹 Nitpick comments (2)
apps/backend/src/lib/cel-evaluator.ts (2)

49-51: Incomplete CEL string escape handling.

The unescapeCelString function only handles \\ and \" escapes. CEL string literals also support other escape sequences like \n, \t, \r, \b, \f, and Unicode escapes (\uXXXX). If rules use these in patterns (e.g., email.contains("\n")), they won't be unescaped correctly.

♻️ Suggested enhancement
 function unescapeCelString(escaped: string): string {
-  return escaped.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
+  return escaped
+    .replace(/\\n/g, '\n')
+    .replace(/\\t/g, '\t')
+    .replace(/\\r/g, '\r')
+    .replace(/\\b/g, '\b')
+    .replace(/\\f/g, '\f')
+    .replace(/\\"/g, '"')
+    .replace(/\\\\/g, '\\');
 }

Note: Order matters - \\\\ should be last to avoid double-unescaping.


184-253: Consider using toMatchInlineSnapshot for unit tests.

Per coding guidelines, toMatchInlineSnapshot is preferred over other selectors. While the current .toEqual() and .toBe() assertions are clear and correct, switching to inline snapshots would provide better consistency and easier test updates.

Copy link
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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/stack-shared/src/known-errors.tsx (1)

1077-1084: ⚠️ Potential issue | 🟡 Minor

Clarify messaging for admin-restricted users.

For restricted_by_administrator, the current text suggests completing onboarding, which doesn’t apply. Consider a conditional message (or more neutral wording) so admin-restricted users aren’t misdirected.

💡 Example adjustment
 const TeamInvitationRestrictedUserNotAllowed = createKnownErrorConstructor(
   KnownError,
   "TEAM_INVITATION_RESTRICTED_USER_NOT_ALLOWED",
   (restrictedReason: { type: "anonymous" | "email_not_verified" | "restricted_by_administrator" }) => [
     403,
-    `Restricted users cannot accept team invitations. Reason: ${restrictedReason.type}. Please complete the onboarding process before accepting team invitations.`,
+    restrictedReason.type === "restricted_by_administrator"
+      ? "Restricted users cannot accept team invitations because an administrator restricted this account. Please contact your administrator."
+      : `Restricted users cannot accept team invitations. Reason: ${restrictedReason.type}. Please complete the onboarding process before accepting team invitations.`,
     {
       restricted_reason: restrictedReason,
     },
   ] as const,
🧹 Nitpick comments (4)
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts (1)

957-979: Validate restricted_reason before casting.

The raw cast can hide unexpected API values and silently misclassify restrictions. Add a defensive guard and throw a clear error when the payload is missing or has an unknown type.

Suggested defensive guard
     return {
       affectedUsers: result.affected_users.map(u => ({
         id: u.id,
         displayName: u.display_name,
         primaryEmail: u.primary_email,
-        restrictedReason: u.restricted_reason as { type: "anonymous" | "email_not_verified" | "restricted_by_administrator" },
+        restrictedReason: (() => {
+          const restrictedReason = u.restricted_reason ?? throwErr("restricted_reason missing");
+          if (
+            restrictedReason.type !== "anonymous" &&
+            restrictedReason.type !== "email_not_verified" &&
+            restrictedReason.type !== "restricted_by_administrator"
+          ) {
+            throwErr(`Unexpected restricted_reason.type: ${restrictedReason.type}`);
+          }
+          return restrictedReason as {
+            type: "anonymous" | "email_not_verified" | "restricted_by_administrator"
+          };
+        })(),
       })),
       totalAffectedCount: result.total_affected_count,
     };

As per coding guidelines, "Never silently use fallback values when type errors occur. Update types or throw errors instead. Use ?? throwErr(...) over non-null assertions with clear error messages explaining the assumption".

apps/backend/prisma/schema.prisma (1)

1062-1076: Use a Prisma enum for SignupRuleTrigger.action to enforce valid values at the database layer.

Prisma enums with @map for PostgreSQL create native database enums that enforce constraints at the database level, preventing invalid values even if code bypasses the ORM. This improves analytics consistency and type safety.

♻️ Proposed refactor
+enum SignupRuleAction {
+  ALLOW        `@map`("allow")
+  REJECT       `@map`("reject")
+  RESTRICT     `@map`("restrict")
+  LOG          `@map`("log")
+  ADD_METADATA `@map`("add_metadata")
+}
+
 // Signup Rules Analytics
 model SignupRuleTrigger {
   id        String   `@default`(uuid()) `@db.Uuid`
   tenancyId String   `@db.Uuid`
   ruleId    String
   userId    String?  `@db.Uuid` // User ID if created, null if rejected
-  action    String   // allow, reject, restrict, log, add_metadata
+  action    SignupRuleAction
   metadata  Json?    // matched conditions, IP, country, etc.
packages/stack-shared/src/known-errors.tsx (1)

719-730: Avoid any and make missing details explicit for SIGN_UP_REJECTED.

constructorArgsFromJson currently accepts any and silently falls back to the default message when details.message is missing. Prefer a typed details object and ?? throwErr(...) so missing data is explicit and consistent with the guidelines.

♻️ Suggested change
 const SignUpRejected = createKnownErrorConstructor(
   KnownError,
   "SIGN_UP_REJECTED",
-  (message?: string) => [
-    403,
-    message ?? "Your sign up was rejected. Please contact us for more information.",
-    {
-      message: message ?? "Your sign up was rejected. Please contact us for more information.",
-    },
-  ] as const,
-  (json: any) => [json.message] as const,
+  (message: string = "Your sign up was rejected. Please contact us for more information.") => [
+    403,
+    message,
+    { message },
+  ] as const,
+  (json: { message?: string } | undefined) => [
+    json?.message ?? throwErr("message not found in SignUpRejected details"),
+  ] as const,
 );
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)

500-505: Avoid any when accessing stackAppInternalsSymbol.

Line 501: casting to any bypasses type safety; please add a narrow type (or document why any is unavoidable).

♻️ Suggested fix
+type StackAdminAppWithInternals = ReturnType<typeof useAdminApp> & {
+  [stackAppInternalsSymbol]: {
+    sendRequest: (path: string, init: RequestInit, auth: 'admin') => Promise<Response>;
+  };
+};
+
     const fetchAnalytics = async () => {
-      const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest(
+      const response = await (stackAdminApp as StackAdminAppWithInternals)[stackAppInternalsSymbol].sendRequest(
         '/internal/sign-up-rules',
         { method: 'GET' },
         'admin' // Required for internal endpoints
       );

Based on learnings: “Avoid the any type. When necessary, leave a comment explaining why it's being used and why the type system fails at that point.”

Copy link
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: 1

🤖 Fix all issues with AI agents
In `@apps/e2e/tests/backend/endpoints/api/v1/users.test.ts`:
- Around line 675-678: The failing snapshot exposes the private field
restricted_by_admin_private_details in a client access response; remove this
field from the client-side output in the test and ensure the server
serialization logic that produces client responses (the code path that handles
accessType "client" / the user response serializer) omits
restricted_by_admin_private_details for non-admin/client-facing responses;
update the snapshot/assertion to match the sanitized response (keep
restricted_by_admin, restricted_by_admin_reason, restricted_reason as needed)
and add or adjust a server/admin test to assert the private field is present
only for admin/server access if needed.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Copy link
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

🤖 Fix all issues with AI agents
In `@apps/backend/src/app/api/latest/internal/sign-up-rules-stats/route.tsx`:
- Around line 55-74: The ClickHouse call using client.query in route.tsx can
throw and currently lacks error handling; wrap the client.query invocation (the
block that assigns to result) in a try-catch or use the same Result.fromPromise
pattern used elsewhere (e.g., analytics query route) to capture failures and
return a structured error response; ensure you handle and log the error, and
convert failures into an appropriate HTTP response instead of letting the
exception bubble.

In `@apps/backend/src/lib/events.tsx`:
- Around line 132-145: The SignUpRuleTriggerEventType schema is missing the
emailDomain field which logRuleTrigger in sign-up-rules.ts includes in event
data; update SignUpRuleTriggerEventType.dataSchema to add an emailDomain entry
(same nullable string semantics as email, e.g., a nullable/defined string) so
the validator accepts the value and the event data isn't dropped or rejected.

In `@apps/backend/src/lib/sign-up-rules.ts`:
- Around line 63-64: The loop over typedEntries(config.auth.signUpRules) uses
object insertion order, not the documented priority order; change to collect
entries into an array and sort them by rule.priority (descending for highest
first) then by ruleId (alphabetical) before iterating, while still skipping
disabled or missing-condition rules (rule.enabled, rule.condition). Also replace
any non-null assertions when reading required fields with a defensive nullish
coalescing that throws a clear error (use your throwErr(...) helper or throw new
Error(...)) so missing priority/condition values fail with a descriptive
message.
🧹 Nitpick comments (3)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)

489-494: Add a comment explaining the any type cast.

Per coding guidelines, when using any, leave a comment explaining why it's necessary and why the type system fails at that point.

Suggested fix
+      // Type assertion required: stackAppInternalsSymbol is a private symbol not exposed in public types
       const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest(
         '/internal/sign-up-rules-stats',
         { method: 'GET' },
         'admin' // Required for internal endpoints
       );
apps/backend/src/lib/sign-up-rules.ts (1)

20-35: Error handling improved but still swallows errors.

The try-catch now uses captureError instead of console.error, which is an improvement for observability. However, swallowing all errors in the logging path means signup analytics could silently fail without the caller knowing. Consider whether this is the desired behavior or if certain error types should propagate.

apps/e2e/tests/backend/endpoints/api/v1/internal/sign-up-rules-stats.test.ts (1)

164-170: Potential time calculation fragility in hour verification.

The expected hour calculation new Date(new Date().getTime() - (hourlyCounts.length - 1 - i) * 60 * 60 * 1000) creates a new Date() at assertion time, which could differ from the backend's now if the test takes more than a few seconds. Consider capturing the timestamp once before the assertion loop.

🧩 Suggested improvement
     const hourlyCounts = response.body.rule_triggers[0].hourly_counts;
     expect(hourlyCounts.length).toBe(48);
+    const assertionTime = new Date();
+    assertionTime.setUTCMinutes(0, 0, 0);
     for (let i = 0; i < hourlyCounts.length - 1; i++) {
-      expect(hourlyCounts[i].hour).toEqual(new Date(new Date().getTime() - (hourlyCounts.length - 1 - i) * 60 * 60 * 1000).toISOString().slice(0, 13) + ':00:00.000Z');
+      const expectedHour = new Date(assertionTime.getTime() - (hourlyCounts.length - 1 - i) * 60 * 60 * 1000);
+      expect(hourlyCounts[i].hour).toEqual(expectedHour.toISOString().slice(0, 13) + ':00:00.000Z');
       expect(hourlyCounts[i].count).toBe(0);
     }
     const lastHourlyCount = hourlyCounts[hourlyCounts.length - 1];
-    expect(lastHourlyCount.hour).toEqual(new Date().toISOString().slice(0, 13) + ':00:00.000Z');
+    expect(lastHourlyCount.hour).toEqual(assertionTime.toISOString().slice(0, 13) + ':00:00.000Z');

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

Copy link
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: 1

🤖 Fix all issues with AI agents
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx:
- Around line 563-567: serverRules calculation can throw when
configWithRules.auth.signUpRules is undefined and doesn't reflect evaluation
order; guard against undefined and sort by priority before mapping. Update the
useMemo that computes serverRules to pass a safe object (e.g., default to {}
when signUpRules is falsy) into typedEntries, then sort the resulting array by
rule.priority (use a numeric fallback like 0) in the correct evaluation order
before mapping to { id, rule }, keeping the same dependency on
configWithRules.auth.signUpRules; reference symbols: serverRules, typedEntries,
configWithRules.auth.signUpRules, signUpRules.
🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)

508-513: Avoid as any when accessing internals.
Please add a narrow type (or at least a short rationale comment) instead of as any to keep type safety intact.

♻️ Possible typing
+type StackAdminAppInternals = {
+  [stackAppInternalsSymbol]: {
+    sendRequest: (path: string, init: RequestInit, scope: 'admin') => Promise<Response>;
+  };
+};
+
 const fetchAnalytics = async () => {
-  const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest(
+  const response = await (stackAdminApp as StackAdminAppInternals)[stackAppInternalsSymbol].sendRequest(
     '/internal/sign-up-rules-stats',
     { method: 'GET' },
     'admin'
   );

Based on learnings: Applies to **/*.{ts,tsx} : Avoid the any type. When necessary, leave a comment explaining why it's being used and why the type system fails at that point.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

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