Skip to content

Sign up rules#1138

Merged
N2D4 merged 27 commits intodevfrom
sign-up-rules
Feb 3, 2026
Merged

Sign up rules#1138
N2D4 merged 27 commits intodevfrom
sign-up-rules

Conversation

@N2D4
Copy link
Contributor

@N2D4 N2D4 commented Jan 28, 2026

Note

High Risk
Touches core sign-up/auth flows and user restriction semantics (including new DB constraints) and introduces dynamic rule evaluation/logging; misconfiguration or CEL/parser bugs could block sign-ups or incorrectly restrict users.

Overview
Introduces CEL-based sign-up rules (config-driven) that are evaluated during password/OTP/OAuth sign-ups and anonymous upgrades; matching rules can reject sign-ups or mark users as admin-restricted, and triggers are logged for analytics.

Extends ProjectUser with restrictedByAdmin plus public/private restriction details, updates restriction computation/filtering, and exposes these fields via user CRUD (including validation + DB constraint enforcing consistency when unrestricted).

Adds a new dashboard Sign-up Rules page with a visual condition builder (CEL <-> visual tree), drag-reorder by priority, per-rule 48h sparkline analytics via a new hidden internal endpoint, and adds user-page UI to view/edit manual restrictions. Also refactors ClickHouse client initialization to require env vars (removing isClickhouseConfigured checks) and adjusts CI container startup wait time.

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

Summary by CodeRabbit

  • New Features

    • Sign-up Rules: visual rule builder, in-project CRUD with drag-reorder, per-rule analytics, backend evaluation, and admin UI.
    • Admin user restrictions: dashboard controls, banners/status, public/private admin details surfaced in user views.
  • APIs & Schema

    • Config and user schemas extended; new SignUpRejected error and sign-up rule types added.
  • Tests

    • Extensive unit and E2E coverage for rules, parser, evaluator, analytics, and restricted-user flows.
  • Docs

    • Editorial guidance added to AGENTS.md.
  • Chores

    • DB statement timeout, updated clean script, minor dependency additions.

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 Feb 3, 2026 6:41pm
stack-dashboard Ready Ready Preview, Comment Feb 3, 2026 6:41pm
stack-demo Ready Ready Preview, Comment Feb 3, 2026 6:41pm
stack-docs Ready Ready Preview, Comment Feb 3, 2026 6:41pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

Adds a CEL-based sign-up rules engine (evaluator, parser, UI), async rule-trigger logging and stats endpoint, admin-imposed user restriction fields with DB migrations and CRUD plumbing, threads sign-up context through auth flows, dashboard UIs for rule management and user restrictions, ClickHouse/events adjustments, and extensive tests and snapshots.

Changes

Cohort / File(s) Summary
CEL evaluator & sign-up rules (backend)
apps/backend/src/lib/cel-evaluator.ts, apps/backend/src/lib/sign-up-rules.ts
New CEL evaluation utilities, SignUpRuleContext, evaluateCelExpression, evaluateSignUpRules, async rule-trigger logging, and CelEvaluationError wrapper.
User creation & rules integration
apps/backend/src/lib/users.tsx, apps/backend/src/lib/oauth.tsx, apps/backend/src/app/api/latest/auth/.../*
Adds SignUpRuleOptions, createOrUpgradeAnonymousUserWithRules, and updates auth flows to pass signUpRuleOptions (authMethod/oauthProvider) to rule-enabled creation.
Database schema & migrations
apps/backend/prisma/schema.prisma, apps/backend/prisma/migrations/*
Adds restrictedByAdmin, restrictedByAdminReason, restrictedByAdminPrivateDetails to ProjectUser and a CHECK constraint enforcing consistency.
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
Threads admin-restriction fields through computeRestrictedStatus, prisma→crud mapping, get/list/create/update flows; adds restricted_by_admin* to server read/update and current-user selection with validation.
Sign-up rules config & types
packages/stack-shared/src/config/schema.ts, .../schema-fuzzer.test.ts, packages/stack-shared/src/interface/crud/sign-up-rules.ts
Adds signUpRules and signUpRulesDefaultAction to config schema and fuzzer, default values, sorting by priority, and TS types for rules/actions.
Dashboard rule builder & parser (frontend)
apps/dashboard/src/components/rule-builder/condition-builder.tsx, apps/dashboard/src/lib/cel-visual-parser.ts, apps/dashboard/src/lib/cel-visual-parser.test.ts, apps/dashboard/src/components/rule-builder/index.tsx
Adds ConditionBuilder UI, CEL visual parser (visual ↔ CEL), escaping/parsing logic, tests, and re-exports.
Sign-up rules dashboard page
apps/dashboard/src/app/.../sign-up-rules/page-client.tsx, .../page.tsx
New Sign-up Rules management UI: inline editor, drag-and-drop reorder, per-rule analytics sparklines, default action config, and persistence.
Admin restriction UI (dashboard)
apps/dashboard/src/app/.../users/[userId]/page-client.tsx
Adds RestrictionBanner, RestrictedStatusRow, and RestrictionDialog to set/clear admin restrictions (public reason + private details) with server PATCH handling.
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, sign-up-rule-stats endpoint (hourly aggregates), logging updates; removes early ClickHouse gating and uses runtime env for client creation.
E2E & unit tests & snapshots
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 suites for sign-up rules engine, stats endpoint, parser; many snapshots updated to include admin-restriction fields.
Frontend wiring & shared symbol
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; centralizes stackAppInternalsSymbol into shared lib and replaces local Symbol.for usages with imports.
Type & template surface changes
packages/template/..., 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 KnownError, updates restrictedReasonTypes, and changes KeyIntersect from a union to an intersection (type semantics change).
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, sets Postgres statement_timeout, updates top-level clean script, VSCode cSpell entries, and editorial AGENTS guidance.
Snapshot serializer
apps/e2e/tests/snapshot-serializer.ts
Adds hourly_counts to fields stripped from snapshots.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User as User
    participant Auth as "Auth Endpoint"
    participant RuleEval as "Sign-up Rule Evaluator"
    participant CELEngine as "CEL Engine"
    participant Events as "Events Logger"
    participant Users as "Users CRUD"
    participant DB as "Database"

    User->>Auth: Sign-up request (email, method, provider)
    Auth->>RuleEval: createSignUpRuleContext(context)
    RuleEval->>CELEngine: evaluateCelExpression(condition, context)
    CELEngine->>CELEngine: preprocess string methods & evaluate
    alt rule matches
        RuleEval->>Auth: return action (allow/reject/restrict/log)
        RuleEval->>Events: async log sign-up-rule-trigger
    else no match
        RuleEval-->>Auth: default action (allow/reject)
    end
    Auth->>Users: createOrUpgradeAnonymousUserWithRules(..., signUpRuleOptions)
    Users->>DB: persist user (restricted_by_admin* fields)
    DB-->>Users: OK
    Users-->>Auth: user object
    Auth-->>User: response
Loading
sequenceDiagram
    autonumber
    actor Admin as Admin
    participant UI as "Dashboard UI"
    participant API as "Admin API"
    participant DB as "Database"

    Admin->>UI: Open user → RestrictionDialog
    UI->>API: PATCH /users/{id} (restricted_by_admin, reason, private_details)
    API->>DB: Update ProjectUser fields
    DB-->>API: OK
    API-->>UI: Updated user
    UI->>UI: Show RestrictionBanner / RestrictedStatus
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I nibbled CEL strings and hopped through each rule,
I tucked admin reasons in a tiny data pool,
Dashboards now drag rules with sparklines that gleam,
Sign-ups get checked, logged, and sometimes reamed,
Hooray — the rabbit approves this tidy new rule!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Sign up rules' directly and clearly references the main feature introduced in the changeset, which adds CEL-based sign-up rules functionality throughout the codebase.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering the high-risk designation, key features (CEL rules, user restrictions, dashboard UI, analytics), database changes, validation logic, and infrastructure updates with clear context about the scope and impact.

✏️ 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 == "test@example.com"');
-      expect(result).toBeDefined();
-      if (result?.type === 'condition') {
-        expect(result.field).toBe('email');
-        expect(result.operator).toBe('equals');
-        expect(result.value).toBe('test@example.com');
-      }
+      expect(result).toMatchInlineSnapshot(`
+        {
+          "field": "email",
+          "id": expect.any(String),
+          "operator": "equals",
+          "type": "condition",
+          "value": "test@example.com",
+        }
+      `);
     });
 
     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>;

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.

Copy link
Collaborator

@nams1570 nams1570 left a comment

Choose a reason for hiding this comment

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

Overall logic is pretty solid, but there are some gaps in behavior I'm curious about. Test coverage looks good.

@N2D4 N2D4 enabled auto-merge (squash) February 3, 2026 02:48
@N2D4 N2D4 requested a review from nams1570 February 3, 2026 02:48
Copy link
Collaborator

@nams1570 nams1570 left a comment

Choose a reason for hiding this comment

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

LGTM!

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.

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/lib/sign-up-rules.ts`:
- Around line 84-85: The log call is being wrapped in
runAsynchronouslyAndWaitUntil which can detach request-bound dynamic data;
instead remove that wrapper and directly await the logging function
(logRuleTrigger) so it runs in the request context and preserves end-user
info—find the call site where
runAsynchronouslyAndWaitUntil(logRuleTrigger(tenancy, ruleId, context, action))
is used and replace it with a direct awaited call to logRuleTrigger(tenancy,
ruleId, context, action).

In `@apps/backend/src/lib/users.tsx`:
- Around line 61-67: The enrichment object `enrichedCreateOrUpdate` sets
`restricted_by_admin` and `restricted_by_admin_private_details` when
`ruleResult.restrictedBecauseOfSignUpRuleId` is truthy but omits the user-facing
`restricted_by_admin_reason`; update the ternary branch that merges into
`enrichedCreateOrUpdate` to also set `restricted_by_admin_reason` (use a
sensible default like the sign-up rule's public message or a generic "Restricted
by site policy") alongside `restricted_by_admin_private_details`, keeping the
existing logic that composes `existingRestrictionPrivateDetails` and
`restrictionPrivateDetails` and leaving `createOrUpdate` unchanged.

In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx:
- Around line 319-323: The catch block for the save-and-restrict operation
currently only logs the error via captureError/StackAssertionError and leaves
the user uninformed; update the catch in the async handler that calls
setIsSaving and captureError (the block referencing
captureError(`user-restriction-save-and-restrict-error`, new
StackAssertionError(...)) ) to also show a blocking user alert (e.g., call
alert(...) with a clear failure message mentioning the user or action) so the
user is notified of the failure before or after calling captureError; keep the
existing finally that calls setIsSaving(false).
🧹 Nitpick comments (7)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/users/[userId]/page-client.tsx (1)

292-294: Add comments explaining as any type casts.

Multiple as any casts are used to access restrictedByAdmin fields (here and at lines 317, 333, 440-442). Per coding guidelines, when using any is necessary, leave a comment explaining why the type system fails at that point.

♻️ Proposed fix: Add explanatory comment
+  // Type assertion needed: restrictedByAdmin fields are not yet exposed on ServerUser type
+  // TODO: Update ServerUser type definition to include these fields
   const restrictedByAdmin = (user as any).restrictedByAdmin ?? false;
   const restrictedByAdminReason = (user as any).restrictedByAdminReason ?? null;
   const restrictedByAdminPrivateDetails = (user as any).restrictedByAdminPrivateDetails ?? null;

Based on 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/e2e/tests/backend/endpoints/api/v1/restricted-users.test.ts (1)

320-321: Avoid any in list item mapping.
Use a typed helper (e.g., { id: string }) or a narrow cast for listResponse.body.items so the tests stay type-safe.

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.

Also applies to: 361-362, 392-392, 429-430, 495-496, 542-543

packages/stack-shared/src/known-errors.tsx (1)

719-730: Type SignUpRejected details instead of any.
A narrow { message?: string } keeps deserialization typed without changing behavior.

♻️ Suggested fix
-  (json: any) => [json.message] as const,
+  (json: { message?: string }) => [json.message] as const,
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.

Also applies to: 1812-1812

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

159-169: Empty string filtering may produce unexpected results for in_list.

When the user types just whitespace or commas (e.g., , , ,), the .filter(Boolean) removes all empty strings, resulting in an empty array []. However, if the user intentionally clears the input (backspaces everything), the value becomes [] immediately. This is fine, but the UX feedback could be confusing since typing "a, " then deleting "a" leaves [] while typing ", " also leaves [].

More importantly, if a user types a single space, split(',') returns ['']map(trim) returns ['']filter(Boolean) returns []. This is correct behavior, but consider whether you want to preserve user input display (e.g., show what they typed) separately from the parsed value.

apps/backend/src/lib/users.tsx (2)

61-67: Consider simplifying the conditional spread syntax.

The !!ruleResult.restrictedBecauseOfSignUpRuleId with spread is functional but could be clearer.

♻️ Suggested simplification
   const enrichedCreateOrUpdate = {
     ...createOrUpdate,
-    ...!!ruleResult.restrictedBecauseOfSignUpRuleId ? {
+    ...(ruleResult.restrictedBecauseOfSignUpRuleId ? {
       restricted_by_admin: true,
       restricted_by_admin_private_details: existingRestrictionPrivateDetails ? `${existingRestrictionPrivateDetails}\n\n${restrictionPrivateDetails}` : restrictionPrivateDetails,
-    } : {},
+    } : {}),
   };

The !! is unnecessary since the ternary already handles truthiness, and adding parentheses improves readability.


70-75: Type assertion after enrichment may mask type errors.

The as KeyIntersect<...> assertion on line 73 bypasses TypeScript's ability to verify that enrichedCreateOrUpdate actually matches the expected type. If the enrichment adds fields not in the original type, this could hide issues.

Consider whether the type could be adjusted upstream to accept the restriction fields, avoiding the need for the assertion.

apps/backend/src/app/api/latest/users/crud.tsx (1)

638-643: Consider extracting shared validation logic.

The admin restriction validation logic in onCreate (lines 619-626) and onUpdate (lines 1113-1124) is similar but not identical. The onUpdate version is more sophisticated (handles effective state). Consider extracting a helper function to ensure consistency and reduce duplication.

♻️ Suggested refactor
function validateRestrictedByAdminFields(
  restrictedByAdmin: boolean,
  reason: string | null | undefined,
  privateDetails: string | null | undefined,
): void {
  if (!restrictedByAdmin) {
    if (reason != null) {
      throw new StatusError(StatusError.BadRequest, "restricted_by_admin_reason requires restricted_by_admin=true");
    }
    if (privateDetails != null) {
      throw new StatusError(StatusError.BadRequest, "restricted_by_admin_private_details requires restricted_by_admin=true");
    }
  }
}

Then use in both onCreate and onUpdate (computing effective values as needed).

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/clickhouse.tsx`:
- Around line 5-11: The module eagerly calls getEnvVariable for clickhouseUrl,
clickhouseAdminPassword, clickhouseExternalPassword (and sets HAS_CLICKHOUSE) at
import time which causes throws even when ClickHouse is optional; move or defer
these env reads into the createClickhouseClient factory (or provide non-throwing
defaults) and compute HAS_CLICKHOUSE there so missing vars only error when
createClickhouseClient is invoked; update references to clickhouseUrl,
clickhouseAdminPassword, clickhouseExternalPassword, HAS_CLICKHOUSE, and keep
getEnvVariable usage inside the function that actually needs the values.

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

@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.

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/clickhouse.tsx`:
- Around line 5-11: The createClickhouseClient function currently ignores its
database parameter; update it so the createClient call uses the passed-in
database when provided and falls back to
getEnvVariable("STACK_CLICKHOUSE_DATABASE", "default") otherwise (i.e., set
database: database ?? getEnvVariable(...)). Modify createClickhouseClient (and
keep existing authType logic and getEnvVariable usage for username/password) so
callers can override the DB; alternatively remove the unused database parameter
if overriding is not desired.

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.

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 523-536: The fetchAnalytics implementation can throw and never
clears the loading flag; wrap its network/JSON parsing logic inside a
try/catch/finally (or add a finally block) so that setIsLoading(false) is always
called regardless of success or error, and in the catch call setAnalytics(new
Map()) or leave previous state as appropriate and log/handle the error; update
the function named fetchAnalytics (and any callers) to ensure
setIsLoading(false) runs in the finally section and keep references to
setAnalytics and setIsLoading unchanged.
🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sign-up-rules/page-client.tsx (1)

512-516: Avoid any on stack app internals access.
(stackAdminApp as any)[stackAppInternalsSymbol] drops type safety and bypasses the repo rule against any. Prefer a narrow internal interface (or add a justification comment if any is unavoidable).

🔧 Suggested refactor (typed internals)
+  type StackAdminAppInternals = {
+    sendRequest: (path: string, init: RequestInit, scope: 'admin') => Promise<Response>,
+  };
-  const response = await (stackAdminApp as any)[stackAppInternalsSymbol].sendRequest(
+  const internals = (stackAdminApp as { [stackAppInternalsSymbol]: StackAdminAppInternals })[stackAppInternalsSymbol];
+  if (!internals) {
+    throw new StackAssertionError("Missing stack app internals");
+  }
+  const response = await internals.sendRequest(
     '/internal/sign-up-rules-stats',
     { method: 'GET' },
     'admin' // Required for internal endpoints
   );

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

@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

@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 2 potential issues.

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

const config = tenancy.config;

let restrictedBecauseOfSignUpRuleId: string | null = null;
for (const [ruleId, rule] of typedEntries(config.auth.signUpRules)) {
Copy link

Choose a reason for hiding this comment

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

Sign-up rules not sorted by priority during evaluation

High Severity

The evaluateSignUpRules function iterates over rules using typedEntries(config.auth.signUpRules), which returns entries in arbitrary object property order rather than sorting by the priority field. The comment on line 37 claims "Rules are evaluated in order of priority (highest first), then alphabetically by ID" but this sorting is never implemented. Users can reorder rules in the UI (which saves priority values), but the backend ignores these priorities entirely, causing rules to potentially match in an unexpected order.

Fix in Cursor Fix in Web

// Log specific events to ClickHouse
const clickhouseEventTypes = ['$token-refresh', '$sign-up-rule-trigger'];
const matchingEventType = eventTypesArray.find(e => clickhouseEventTypes.includes(e.id));
if (matchingEventType) {
Copy link

Choose a reason for hiding this comment

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

ClickHouse configuration check removed breaks deployments

Medium Severity

The isClickhouseConfigured() check was removed from event logging, but getClickhouseAdminClient() now throws when ClickHouse environment variables are missing (because getEnvVariable without a default value throws "Missing environment variable"). When logging $token-refresh or $sign-up-rule-trigger events in deployments without ClickHouse configured, this will cause errors inside runAsynchronouslyAndWaitUntil. Previously, the check prevented attempting to use ClickHouse when not configured.

Additional Locations (1)

Fix in Cursor Fix in Web

@N2D4 N2D4 disabled auto-merge February 3, 2026 19:08
@N2D4 N2D4 merged commit 7a35751 into dev Feb 3, 2026
31 of 32 checks passed
@N2D4 N2D4 deleted the sign-up-rules branch February 3, 2026 19:08
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