Skip to content

feat: Captcha Auto mode & other improvements - #540

Merged
Blaumaus merged 14 commits into
mainfrom
feature/captcha-auto-difficulty
May 22, 2026
Merged

Blaumaus merged 14 commits into
mainfrom
feature/captcha-auto-difficulty

Conversation

@Blaumaus

@Blaumaus Blaumaus commented May 20, 2026 •

Copy link
Copy Markdown
Member

Changes

If applicable, please describe what changes were made in this pull request.

Community Edition support

  • Your feature is implemented for the Swetrix Community Edition
  • This PR only updates the Cloud (Enterprise) Edition code (e.g. Paddle webhooks, blog, payouts, etc.)

Database migrations

  • Clickhouse / MySQL migrations added for this PR
  • No table schemas changed in this PR

Documentation

  • You have updated the documentation according to your PR
  • This PR did not change any publicly documented endpoints

Summary by CodeRabbit

  • New Features

    • Auto difficulty mode for CAPTCHA with project setting (manual/auto), ability to clear secret, adaptive difficulty selection, and richer event logging
    • Expanded CAPTCHA analytics: multi‑series charts (generated/passed/failed/validationFailed/replayed) plus summary cards (totals, pass rate, solve‑time quantiles, difficulty/reason distributions)
  • Bug Fixes

    • Fixed CAPTCHA validation API endpoint and updated client example field names
  • Documentation

    • Updated CAPTCHA docs, analytics guides, README, and localization strings
  • Style/UX

    • API key and form input UX/accessibility tweaks (readOnly/password handling)

Review Change Stack

@Blaumaus Blaumaus self-assigned this May 20, 2026
@coderabbitai

coderabbitai Bot commented May 20, 2026 •

Copy link
Copy Markdown

Caution

Review failed

Failed to post review comments

📝 Walkthrough

Walkthrough

Adds adaptive CAPTCHA PoW (manual/auto), structured verification and Redis-based outcome counters; enriches analytics with ClickHouse meta-to-SQL helpers, multi-series CAPTCHA charts and summary stats; updates controllers, DTOs, DB migrations, frontend settings, chart/UI, docs, and client validator defaults.

Changes

CAPTCHA Core & Analytics

Layer / File(s) Summary
Event transformer meta support & DTO docs
backend/.../analytics/utils/transformers.ts, backend/.../captcha/dtos/validate.dto.ts
Captcha events may include meta key/value; transformer emits meta.key/meta.value. ValidateDto swagger typo fixed.
Project captchaDifficultyMode: DTO/entity & migrations
backend/.../project/dto/update-project.dto.ts, backend/.../project/entity/project.entity.ts, backend/migrations/*
Add captchaDifficultyMode to DTO and entity, include ClickHouse/MySQL migrations and ClickHouse init DDL; include field in cached project projection.
Project controller/service update & secret delete
backend/.../project/project.controller.ts, backend/.../project/project.service.ts
Secret regeneration replaced with DELETE secret endpoint that clears Redis; project update applies origins/ipBlacklist/countryBlacklist only when provided and enforces caps; captchaDifficultyMode assigned only when present; Redis/ClickHouse counting uses CAPTCHA_PASS_CONDITION.
Service types, bounds & Redis helpers
backend/.../captcha/captcha.service.ts
Introduce PoW difficulty bounds, clamping, header heuristics, PID/IP hashing, Redis counter keying/increment/get, and atomic challenge consumption (GET+DEL and spent recording).
Centralized captcha event logging & outcome counters
backend/.../captcha/captcha.service.ts
Add logCaptchaEvent, recordCaptchaOutcome, and public logCaptchaPass/Failure/Replay that update Redis counters and emit enriched ClickHouse events including difficulty/mode/reason/solve/failure metadata.
Token validation + failure recording
backend/.../captcha/captcha.service.ts
Extend validateToken to accept headers/IP, enforce payload/pid/expiry/replay checks, and call logValidationFailure to record validation_fail events before throwing.
Adaptive PoW generation & verification
backend/.../captcha/captcha.service.ts
Implement selectDifficulty auto mode using Redis counters and risk heuristics; generateChallenge stores enriched payload (difficulty, mode, issuedAt); verifyPoW atomically consumes stored challenges and returns structured PowVerificationResult with valid and failureReason.
Controller integration and error/log flows
backend/.../captcha/captcha.controller.ts
Controllers pass headers/IP, handle structured verifyPoW results with replay/expired detection and guarded logging, and call validateToken with request context.
Analytics SQL helpers & mapping
backend/.../analytics/analytics.service.ts
Add helpers to map logical captcha columns (event/difficulty/reason/solve_time) to ClickHouse expressions derived from events.meta and extend filtering/resolution to DataType.CAPTCHA.
Chart aggregation: multi-series counts & cumulative handling
backend/.../analytics/analytics.service.ts
Aggregate per-bucket multi-series counts (generated/passed/failed/validationFailed/replayed) via countIf and compute cumulative sums independently per series for cumulative charts.
Chart extraction & summary computation
backend/.../analytics/analytics.service.ts
Extract aligned multi-series arrays, set results to passed, add getCaptchaSummary() (totals, pass rate, solve-time quantiles, difficulty and solve-time distributions), and return { params, chart, summary }.
Task manager/project counting parity
backend/.../task-manager, backend/.../project/project.service.ts
Introduce CAPTCHA_PASS_CONDITION SQL snippet and use countIf(CAPTCHA_PASS_CONDITION) in plan-usage, usage info, and event counts to only include passed captcha events.

Frontend: types, settings UI & analytics view

Layer / File(s) Summary
API types & Project model update
web/app/api/api.server.ts, web/app/lib/models/Project.ts
Expand CaptchaChartData to optional multi-series, add CaptchaSummaryData, include optional summary in response; add captchaDifficultyMode to Project model.
Project Settings UI & form wiring
web/app/pages/Project/Settings/ProjectSettings.tsx, web/app/routes/projects.settings.$id.tsx
Add captchaDifficultyMode state/select, sync with server, send captchaDifficulty only when in manual mode, and conditionally include form fields when present.
Chart helper and CaptchaChart prop updates
web/app/pages/Project/View/ViewProject.helpers.tsx, web/app/pages/Project/tabs/Captcha/CaptchaChart.tsx
getSettingsCaptcha accepts multiple optional series and selects active ones; CaptchaChart accepts optional multi-series chart and children.
CaptchaView: summary cards, panels & data loading
web/app/pages/Project/tabs/Captcha/CaptchaView.tsx
Fetch chart+summary (and compare) in parallel, render summary MetricCards, add panels for captcha_event, captcha_reason, captcha_difficulty, and adapt panel data mapping/rendering.
Snippets, locales & docs
web/app/ui/CaptchaSetup.tsx, packages/captcha-validator/*, docs/*, web/public/locales/en.json
Update client validation snippet to use /v1/captcha/validate + token, update captcha-validator default host/docs, extend analytics docs and locale strings for new metrics and modes.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant CaptchaController
  participant CaptchaService
  participant Redis
  participant ClickHouse
  Client->>CaptchaController: generate/verify/validate request (headers, ip, payload)
  CaptchaController->>CaptchaService: generateChallenge / verifyPoW / validateToken (with headers, ip)
  CaptchaService->>Redis: read/write challenge, increment counters (selectDifficulty, recordOutcome, token replay NX)
  CaptchaService->>ClickHouse: eventTransformer -> insert enriched captcha event
  CaptchaService-->>CaptchaController: structured PowVerificationResult / validation response
  CaptchaController-->>Client: HTTP response (token/result)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Swetrix/swetrix#526: The main PR’s CAPTCHA analytics query/aggregation logic builds on prior analytics migration to the unified events table.
  • Swetrix/swetrix#443: Prior PoW/controller verification changes that this PR extends with structured verification results and replay logging.
  • Swetrix/swetrix#439: Overlaps in CAPTCHA PoW API wiring and verification handling.

Poem

A rabbit hopped through code at night,
Counters ticked beneath Redis light,
ClickHouse tallied five bright streams,
Auto or manual — choose your dreams,
The bunny logged each pass with delight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Captcha Auto mode & other improvements' directly summarizes the main feature (automatic CAPTCHA difficulty mode) and acknowledges additional changes, matching the extensive changeset across CAPTCHA analytics, validation, and project configuration.
Description check ✅ Passed The PR description follows the template structure with all required sections (Changes, Community Edition support, Database migrations, Documentation) and properly checks off applicable items, though the 'Changes' section lacks detailed explanation of what was implemented.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/captcha-auto-difficulty

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.

Blaumaus and others added 5 commits May 20, 2026 23:39
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
web/app/pages/Project/tabs/Captcha/CaptchaView.tsx (1)

189-193: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don’t gate chart rendering on results only.

Line 189 currently requires chart.results, which drops valid multi-series payloads (e.g., only generated/passed/failed/...) and hides the chart.

Proposed diff
-      if (chart && chart.x && chart.results) {
+      const hasChartSeries = Boolean(
+        chart &&
+          (chart.results?.length ||
+            chart.generated?.length ||
+            chart.passed?.length ||
+            chart.failed?.length ||
+            chart.validationFailed?.length ||
+            chart.replayed?.length)
+      )
+
+      if (chart?.x?.length && hasChartSeries) {
         setChartData(chart)
       } else {
         setChartData(null)
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/pages/Project/tabs/Captcha/CaptchaView.tsx` around lines 189 - 193,
The code is incorrectly gating chart rendering on chart.results which causes
valid multi-series payloads to be dropped; update the conditional in the
component (where chart and setChartData are used) to check for chart and chart.x
(or other required axis/series existence) instead of requiring chart.results so
that multi-series charts (e.g., generated/passed/failed) are accepted; modify
the block that currently reads "if (chart && chart.x && chart.results) {
setChartData(chart) } else { setChartData(null) }" to only require the necessary
axis/series presence (e.g., chart && chart.x) before calling
setChartData(chart).
🧹 Nitpick comments (6)
backend/apps/community/src/captcha/captcha.service.ts (1)

335-335: 💤 Low value

Incorrect method name in error log.

Same issue as in the cloud version - the error message references logCaptchaPass but this is the generic logCaptchaEvent method.

📝 Proposed fix
-      this.logger.error(`[CaptchaService -> logCaptchaPass] ${e}`)
+      this.logger.error(`[CaptchaService -> logCaptchaEvent] ${e}`)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/community/src/captcha/captcha.service.ts` at line 335, The error
log in the logCaptchaEvent method incorrectly references `logCaptchaPass`;
update the logger call in `logCaptchaEvent` to reference the correct method name
(`logCaptchaEvent`) and include the caught error `e` for context (i.e., change
the message string passed to `this.logger.error` to mention `logCaptchaEvent`
while still logging the error variable). Ensure the change is made in the
`logCaptchaEvent` method so the log message accurately reflects the originating
function.
backend/apps/cloud/src/captcha/captcha.service.ts (2)

335-335: 💤 Low value

Incorrect method name in error log.

The error message references logCaptchaPass but this is the generic logCaptchaEvent method.

📝 Proposed fix
-      this.logger.error(`[CaptchaService -> logCaptchaPass] ${e}`)
+      this.logger.error(`[CaptchaService -> logCaptchaEvent] ${e}`)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/cloud/src/captcha/captcha.service.ts` at line 335, The error log
in the catch block incorrectly names the method as logCaptchaPass; update the
log message to reference the actual method name logCaptchaEvent (or use a
generic contextual message) so it accurately reflects where the error occurred;
locate the catch in the logCaptchaEvent function and change the logger.error
call from referencing logCaptchaPass to logCaptchaEvent (include the error
object in the log as currently done).

156-164: 💤 Low value

Potential race condition on TTL expiration.

incrCounter checks value === 1 after INCR to set TTL, but in high-concurrency scenarios, two concurrent calls could both see value > 1 if one finishes before the other's INCR returns. This could leave a key without TTL if the first increment races past before expire is called.

Consider using atomic INCR + EXPIRE via pipeline or Lua script, or just always call expire (Redis is idempotent for this).

♻️ Safer approach using Redis pipeline
 const incrCounter = async (key: string, ttl: number): Promise<number> => {
-  const value = await redis.incr(key)
-
-  if (value === 1) {
-    await redis.expire(key, ttl)
-  }
-
-  return value
+  const pipeline = redis.pipeline()
+  pipeline.incr(key)
+  pipeline.expire(key, ttl)
+  const results = await pipeline.exec()
+  return results?.[0]?.[1] as number
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/cloud/src/captcha/captcha.service.ts` around lines 156 - 164,
The incrCounter function can lose the TTL in high-concurrency races because it
calls redis.incr(key) then conditionally redis.expire(key, ttl); fix by making
the INCR+EXPIRE atomic: either use a Redis pipeline/transaction to call incr and
expire together, or execute a small Lua script that runs INCR and sets EXPIRE
when the new value is 1, or simply always call redis.expire(key, ttl) after
redis.incr(key) to rely on Redis idempotency; update the incrCounter function to
use one of these approaches (referencing incrCounter, redis.incr, redis.expire)
so the TTL is reliably set.
backend/apps/community/src/captcha/captcha.controller.ts (1)

126-143: 💤 Low value

Catch block assumes all verifyPoW exceptions are replays.

Same observation as the cloud controller - the catch block logs logCaptchaReplay for any exception from verifyPoW, but not all exceptions indicate replays.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/community/src/captcha/captcha.controller.ts` around lines 126 -
143, The catch block around this.captchaService.verifyPoW currently treats every
thrown error as a replay; change it to inspect the thrown error (e.g., check
error type/class or a distinguishing property like error.code or error.message
for a replay marker such as 'REPLAY' or a CaptchaReplayError) and only call
this.captchaService.logCaptchaReplay(pid, headers, timestamp, ip) when the error
indicates an actual replay; for all other errors rethrow immediately (preserving
the original error) and avoid calling logCaptchaReplay. Ensure the logic
references verifyPoW and logCaptchaReplay and preserves original exceptions when
not handling replay cases.
backend/apps/cloud/src/captcha/captcha.controller.ts (1)

126-143: 💤 Low value

Catch block assumes all verifyPoW exceptions are replays.

The catch block logs logCaptchaReplay for any exception from verifyPoW, but verifyPoW throws BadRequestException for both "Invalid or expired challenge" (replay-like) and "Invalid challenge difficulty" or "Challenge was issued for a different project" (not replays). This could misclassify some failures as replays in analytics.

Consider checking the exception message or introducing distinct exception types.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/cloud/src/captcha/captcha.controller.ts` around lines 126 - 143,
The catch in the verify handler treats every error from captchaService.verifyPoW
as a replay and calls logCaptchaReplay; change it to only call logCaptchaReplay
for genuine replay errors by distinguishing error types/messages: inspect the
thrown error from verifyPoW (e.g., check instanceof BadRequestException and the
error.message or introduce and check a dedicated ReplayCaptchaException from
captchaService.verifyPoW) and only invoke
this.captchaService.logCaptchaReplay(pid, headers, timestamp, ip) when the error
indicates "Invalid or expired challenge" (or the new ReplayCaptchaException);
rethrow the original error otherwise. Ensure references:
captchaService.verifyPoW, captchaService.logCaptchaReplay, and the controller's
verify catch block are updated accordingly.
web/app/routes/projects.settings.$id.tsx (1)

130-133: ⚡ Quick win

Validate captchaDifficultyMode before forwarding to backend.

captchaDifficultyMode is accepted as any string and passed through. Add a strict 'manual' | 'auto' guard at the route boundary to avoid propagating invalid values.

Proposed hardening
-      const captchaDifficultyMode = formData
-        .get('captchaDifficultyMode')
-        ?.toString()
+      const rawCaptchaDifficultyMode = formData
+        .get('captchaDifficultyMode')
+        ?.toString()
+      const captchaDifficultyMode =
+        rawCaptchaDifficultyMode === 'manual' ||
+        rawCaptchaDifficultyMode === 'auto'
+          ? rawCaptchaDifficultyMode
+          : undefined
+
+      if (
+        rawCaptchaDifficultyMode !== undefined &&
+        captchaDifficultyMode === undefined
+      ) {
+        return data<ProjectSettingsActionData>(
+          { intent, error: 'Invalid captcha difficulty mode' },
+          { status: 400 },
+        )
+      }
@@
       if (captchaDifficultyMode !== undefined)
         updateData.captchaDifficultyMode = captchaDifficultyMode

Also applies to: 177-178

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/app/routes/projects.settings`.$id.tsx around lines 130 - 133, The code
reads captchaDifficultyMode from formData and forwards it without validation;
add a strict guard that only accepts 'manual' or 'auto' (or omits the field)
before sending to the backend. In the route handler where
formData.get('captchaDifficultyMode') is used (variable captchaDifficultyMode),
validate the string against the allowed union ('manual' | 'auto') and coerce to
undefined/null when it doesn't match; apply the same guard at the other
occurrence that reads captchaDifficultyMode (lines referenced in the review).
Reject or sanitize any other values so only 'manual' or 'auto' are propagated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/app/api/api.server.ts`:
- Around line 1535-1565: The CaptchaChartData interface incorrectly requires
results even though charts may now be sent as series-only
(generated/passed/failed/validationFailed/replayed); update CaptchaChartData
(the interface named CaptchaChartData) to make results optional (results?:
number[]) and verify any downstream guards or consumers that access results in
code handling CaptchaDataResponse.chart handle undefined (e.g., check for
chart.results before using it or prefer the new generated/passed/... arrays).

---

Outside diff comments:
In `@web/app/pages/Project/tabs/Captcha/CaptchaView.tsx`:
- Around line 189-193: The code is incorrectly gating chart rendering on
chart.results which causes valid multi-series payloads to be dropped; update the
conditional in the component (where chart and setChartData are used) to check
for chart and chart.x (or other required axis/series existence) instead of
requiring chart.results so that multi-series charts (e.g.,
generated/passed/failed) are accepted; modify the block that currently reads "if
(chart && chart.x && chart.results) { setChartData(chart) } else {
setChartData(null) }" to only require the necessary axis/series presence (e.g.,
chart && chart.x) before calling setChartData(chart).

---

Nitpick comments:
In `@backend/apps/cloud/src/captcha/captcha.controller.ts`:
- Around line 126-143: The catch in the verify handler treats every error from
captchaService.verifyPoW as a replay and calls logCaptchaReplay; change it to
only call logCaptchaReplay for genuine replay errors by distinguishing error
types/messages: inspect the thrown error from verifyPoW (e.g., check instanceof
BadRequestException and the error.message or introduce and check a dedicated
ReplayCaptchaException from captchaService.verifyPoW) and only invoke
this.captchaService.logCaptchaReplay(pid, headers, timestamp, ip) when the error
indicates "Invalid or expired challenge" (or the new ReplayCaptchaException);
rethrow the original error otherwise. Ensure references:
captchaService.verifyPoW, captchaService.logCaptchaReplay, and the controller's
verify catch block are updated accordingly.

In `@backend/apps/cloud/src/captcha/captcha.service.ts`:
- Line 335: The error log in the catch block incorrectly names the method as
logCaptchaPass; update the log message to reference the actual method name
logCaptchaEvent (or use a generic contextual message) so it accurately reflects
where the error occurred; locate the catch in the logCaptchaEvent function and
change the logger.error call from referencing logCaptchaPass to logCaptchaEvent
(include the error object in the log as currently done).
- Around line 156-164: The incrCounter function can lose the TTL in
high-concurrency races because it calls redis.incr(key) then conditionally
redis.expire(key, ttl); fix by making the INCR+EXPIRE atomic: either use a Redis
pipeline/transaction to call incr and expire together, or execute a small Lua
script that runs INCR and sets EXPIRE when the new value is 1, or simply always
call redis.expire(key, ttl) after redis.incr(key) to rely on Redis idempotency;
update the incrCounter function to use one of these approaches (referencing
incrCounter, redis.incr, redis.expire) so the TTL is reliably set.

In `@backend/apps/community/src/captcha/captcha.controller.ts`:
- Around line 126-143: The catch block around this.captchaService.verifyPoW
currently treats every thrown error as a replay; change it to inspect the thrown
error (e.g., check error type/class or a distinguishing property like error.code
or error.message for a replay marker such as 'REPLAY' or a CaptchaReplayError)
and only call this.captchaService.logCaptchaReplay(pid, headers, timestamp, ip)
when the error indicates an actual replay; for all other errors rethrow
immediately (preserving the original error) and avoid calling logCaptchaReplay.
Ensure the logic references verifyPoW and logCaptchaReplay and preserves
original exceptions when not handling replay cases.

In `@backend/apps/community/src/captcha/captcha.service.ts`:
- Line 335: The error log in the logCaptchaEvent method incorrectly references
`logCaptchaPass`; update the logger call in `logCaptchaEvent` to reference the
correct method name (`logCaptchaEvent`) and include the caught error `e` for
context (i.e., change the message string passed to `this.logger.error` to
mention `logCaptchaEvent` while still logging the error variable). Ensure the
change is made in the `logCaptchaEvent` method so the log message accurately
reflects the originating function.

In `@web/app/routes/projects.settings`.$id.tsx:
- Around line 130-133: The code reads captchaDifficultyMode from formData and
forwards it without validation; add a strict guard that only accepts 'manual' or
'auto' (or omits the field) before sending to the backend. In the route handler
where formData.get('captchaDifficultyMode') is used (variable
captchaDifficultyMode), validate the string against the allowed union ('manual'
| 'auto') and coerce to undefined/null when it doesn't match; apply the same
guard at the other occurrence that reads captchaDifficultyMode (lines referenced
in the review). Reject or sanitize any other values so only 'manual' or 'auto'
are propagated.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a1c2085f-9f30-4d97-92fa-8f0d77f0342c

📥 Commits

Reviewing files that changed from the base of the PR and between d9e053f and 2ec8eb6.

📒 Files selected for processing (39)
  • backend/apps/cloud/src/analytics/analytics.service.ts
  • backend/apps/cloud/src/analytics/utils/transformers.ts
  • backend/apps/cloud/src/captcha/captcha.controller.ts
  • backend/apps/cloud/src/captcha/captcha.service.ts
  • backend/apps/cloud/src/captcha/dtos/validate.dto.ts
  • backend/apps/cloud/src/common/constants.ts
  • backend/apps/cloud/src/project/dto/update-project.dto.ts
  • backend/apps/cloud/src/project/entity/project.entity.ts
  • backend/apps/cloud/src/project/project.controller.ts
  • backend/apps/cloud/src/project/project.service.ts
  • backend/apps/community/src/analytics/analytics.service.ts
  • backend/apps/community/src/analytics/utils/transformers.ts
  • backend/apps/community/src/captcha/captcha.controller.ts
  • backend/apps/community/src/captcha/captcha.service.ts
  • backend/apps/community/src/captcha/dtos/validate.dto.ts
  • backend/apps/community/src/common/constants.ts
  • backend/apps/community/src/common/utils.ts
  • backend/apps/community/src/project/dto/update-project.dto.ts
  • backend/apps/community/src/project/entity/project.entity.ts
  • backend/apps/community/src/project/project.controller.ts
  • backend/migrations/clickhouse/initialise_selfhosted.js
  • backend/migrations/clickhouse/selfhosted_2026_01_02_captcha.js
  • backend/migrations/clickhouse/selfhosted_2026_05_20_captcha_auto_difficulty.js
  • backend/migrations/mysql/2026_05_20_captcha_auto_difficulty.sql
  • docs/content/docs/analytics-dashboard/captcha.mdx
  • docs/content/docs/captcha/introduction.mdx
  • docs/content/docs/captcha/server-side-validation.mdx
  • packages/captcha-validator/README.md
  • packages/captcha-validator/src/index.ts
  • packages/captcha-validator/src/utils.ts
  • web/app/api/api.server.ts
  • web/app/lib/models/Project.ts
  • web/app/pages/Project/Settings/ProjectSettings.tsx
  • web/app/pages/Project/View/ViewProject.helpers.tsx
  • web/app/pages/Project/tabs/Captcha/CaptchaChart.tsx
  • web/app/pages/Project/tabs/Captcha/CaptchaView.tsx
  • web/app/routes/projects.settings.$id.tsx
  • web/app/ui/CaptchaSetup.tsx
  • web/public/locales/en.json

Comment thread web/app/api/api.server.ts Resolved

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
backend/apps/cloud/src/project/project.controller.ts (1)

801-806: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing length limit on countryBlacklist.

ipBlacklist is sliced to BLACKLIST_ITEMS_MAXIMUM at line 795, but countryBlacklist has no such limit. This inconsistency may leave the CodeQL "Loop bound injection" issue unresolved for this field.

Proposed fix
     if (projectDTO.countryBlacklist !== undefined) {
       project.countryBlacklist =
         projectDTO.countryBlacklist && Array.isArray(projectDTO.countryBlacklist)
-          ? (_map(projectDTO.countryBlacklist, _trim) as string[])
+          ? (_map(
+              projectDTO.countryBlacklist.slice(0, BLACKLIST_ITEMS_MAXIMUM),
+              _trim,
+            ) as string[])
           : null
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/cloud/src/project/project.controller.ts` around lines 801 - 806,
The countryBlacklist field is not being bounded like ipBlacklist and can trigger
a Loop bound injection; update the same method that slices ipBlacklist
(referencing ipBlacklist and BLACKLIST_ITEMS_MAXIMUM) to also slice/limit
countryBlacklist to BLACKLIST_ITEMS_MAXIMUM before any loop/processing and
before returning the response (look for usages of countryBlacklist in the
controller method to ensure the truncated array is used everywhere).
♻️ Duplicate comments (1)
backend/apps/community/src/project/project.controller.ts (1)

1063-1067: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing length limit on origins.

The CodeQL "Loop bound injection" issue at line 1065 persists because projectDTO.origins is mapped without any length bound. The cloud version uses .slice(0, PROJECTS_MAXIMUM) for this field.

Proposed fix
+const PROJECTS_MAXIMUM = 50
+
 if (projectDTO.origins !== undefined) {
-  project.origins = projectDTO.origins
-    ? (_map(projectDTO.origins, _trim) as string[])
+  const safeOrigins = Array.isArray(projectDTO.origins)
+    ? projectDTO.origins.slice(0, PROJECTS_MAXIMUM)
+    : []
+
+  project.origins = safeOrigins.length > 0
+    ? (_map(safeOrigins, _trim) as string[])
     : []
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/apps/community/src/project/project.controller.ts` around lines 1063 -
1067, The assignment to project.origins maps projectDTO.origins without bounding
its length, allowing a Loop bound injection; before mapping in ProjectController
(the block handling projectDTO.origins), limit the input array to
PROJECTS_MAXIMUM (e.g., use projectDTO.origins.slice(0, PROJECTS_MAXIMUM)) and
then apply _map(..., _trim) so project.origins is set from the
sliced-and-trimmed array (or [] when falsy), ensuring the same length cap used
elsewhere.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@backend/apps/cloud/src/project/project.controller.ts`:
- Around line 801-806: The countryBlacklist field is not being bounded like
ipBlacklist and can trigger a Loop bound injection; update the same method that
slices ipBlacklist (referencing ipBlacklist and BLACKLIST_ITEMS_MAXIMUM) to also
slice/limit countryBlacklist to BLACKLIST_ITEMS_MAXIMUM before any
loop/processing and before returning the response (look for usages of
countryBlacklist in the controller method to ensure the truncated array is used
everywhere).

---

Duplicate comments:
In `@backend/apps/community/src/project/project.controller.ts`:
- Around line 1063-1067: The assignment to project.origins maps
projectDTO.origins without bounding its length, allowing a Loop bound injection;
before mapping in ProjectController (the block handling projectDTO.origins),
limit the input array to PROJECTS_MAXIMUM (e.g., use projectDTO.origins.slice(0,
PROJECTS_MAXIMUM)) and then apply _map(..., _trim) so project.origins is set
from the sliced-and-trimmed array (or [] when falsy), ensuring the same length
cap used elsewhere.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ca7dba24-4a60-4ddf-bee7-5d17f13c4ea0

📥 Commits

Reviewing files that changed from the base of the PR and between 2ec8eb6 and c13ba9d.

📒 Files selected for processing (2)
  • backend/apps/cloud/src/project/project.controller.ts
  • backend/apps/community/src/project/project.controller.ts

@Blaumaus

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Blaumaus
Blaumaus merged commit ce54d5e into main May 22, 2026
12 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jul 7, 2026
3 of 6 tasks
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