feat(captcha): implement MAT-140 CAPTCHA pipeline#181
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
📝 WalkthroughWalkthroughThe PR adds a persistent CAPTCHA workflow spanning hoster extraction, domain state transitions, SQLite storage, timeout and retry handling, Tauri IPC, frontend queue controls, event propagation, configuration, and redacted challenge history. ChangesCAPTCHA pipeline
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
cubic analysis
All reported issues were addressed across 67 files
Linked issue analysis
Linked issue: MAT-140: [Lot 4] Construire le pipeline CAPTCHA de bout en bout
| Status | Acceptance criteria | Notes |
|---|---|---|
| ✅ | R-01 — A download waiting for CAPTCHA passes into the CAPTCHA queue without occupying a download slot. | Queue/slot handling and CAPTCHA pending event are implemented so the queue frees a download slot when a captcha is pending (QueueManager.handle_captcha_pending calls decrement_and_schedule; CommandBus integrates a captcha handler and cancel/remove logic avoids cancelling downloads that are waiting for CAPTCHA). |
| ✅ | R-02 — The CAPTCHA view displays the image, remaining time, and allows solve / skip / retry. | React UI components render the challenge image, show countdown, and expose solve/skip/retry actions; tests exercise the view. |
| ✅ | R-03 — A correct resolution resumes the download automatically; skip marks it failed with an explicit reason. | Command handlers and domain transitions emit CaptchaSolved/CaptchaSkipped and drive download state transitions (queue_after_wait / DownloadQueued) so solving resumes the download and skipping records failure. |
| ✅ | R-04 — Timeout of an entry triggers the configured fallback (skip by default) and is logged in captcha_log. | Config, timeout handling, events, persistence and migration are present; timeouts emit events and are recorded in the captcha_log implementation and tested by the added handler/repo tests. |
| ✅ | R-05 — Flow respects architecture: entities/transitions in domain/, orchestration in application/, no state mutated outside Command Handler. | New captcha domain model and events live under domain/, orchestration and command handlers live under application/, and adapters produce DomainEvent/DomainError rather than mutating domain state directly. |
| ✅ | R-06 — Coverage: domain tests on the CAPTCHA state machine, handler tests with in-memory mocks, frontend test on the view. | Comprehensive tests were added across domain, application handlers, sqlite repo, and frontend view covering behavior and edge cases. |
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Merging this PR will degrade performance by 0.8%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | normalize_link_check_parallelism |
120.8 ns | 150 ns | -19.44% |
| ❌ | normalize_max_concurrent |
120.8 ns | 150 ns | -19.44% |
| ⚡ | detect_md5 |
745 ns | 628.3 ns | +18.57% |
| ⚡ | detect_sha256 |
821.9 ns | 734.4 ns | +11.91% |
| ⚡ | create_valid |
281.4 ns | 252.2 ns | +11.56% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing feat/mat-140-captcha-pipeline (74ad5a7) with main (a375ad7)
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (4)
src-tauri/src/domain/model/captcha.rs (1)
329-408: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winHand-rolled PNG/GIF/JPEG sniffer for untrusted hoster bytes.
image_metadata/jpeg_dimensionsparse hoster-supplied binary data by hand to bound CAPTCHA image pixels/MIME. The indexing is memory-safe (bounded via.get()/checked_add), but rolling a custom binary-format parser for untrusted input is exactly the kind of task better delegated to an established, audited crate (e.g.imagesize, which reads PNG/GIF/JPEG dimensions from a byte slice without decoding).♻️ Sketch using the `imagesize` crate
-pub fn captcha_image_mime_type(data: &[u8]) -> Option<&'static str> { - let (mime, width, height) = image_metadata(data)?; - let pixels = u64::from(width).checked_mul(u64::from(height))?; - (width > 0 && height > 0 && pixels <= MAX_CAPTCHA_IMAGE_PIXELS).then_some(mime) -} +pub fn captcha_image_mime_type(data: &[u8]) -> Option<&'static str> { + let info = imagesize::blob_size(data).ok()?; + let pixels = (info.width as u64).checked_mul(info.height as u64)?; + if pixels == 0 || pixels > MAX_CAPTCHA_IMAGE_PIXELS { + return None; + } + match detect_format(data)? { // still need format allow-listing to PNG/GIF/JPEG + ImageType::Png => Some("image/png"), + ImageType::Gif => Some("image/gif"), + ImageType::Jpeg => Some("image/jpeg"), + _ => None, + } +}🤖 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 `@src-tauri/src/domain/model/captcha.rs` around lines 329 - 408, Replace the hand-rolled image format parsing in image_metadata and jpeg_dimensions with the established imagesize crate’s byte-slice dimension detection. Map supported PNG, GIF, and JPEG results to their MIME types and preserve captcha_image_mime_type’s nonzero-dimension and MAX_CAPTCHA_IMAGE_PIXELS validation behavior.src-tauri/src/adapters/driven/plugin/hoster_contract.rs (1)
68-92: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winImage/TextInput CAPTCHA responses can be accepted without any image data.
Image validation only runs
if file.captcha_image_data.as_ref().is_some_and(...)— when the field is absent, the check is skipped entirely regardless ofchallenge_type. ForCaptchaType::Image/TextInput(which, per this PR's manual-solver UX, need a rendered image to solve), a plugin response withrequires_captcha: true, captcha_type: "image"and nocaptcha_image_datawill pass extraction and surface an unsolvable challenge to the user later, rather than being rejected here as malformed.🐛 Suggested guard
Some(ExtractedCaptchaChallenge { challenge_type, image_data: file.captcha_image_data, }) + } else if matches!(challenge_type, CaptchaType::Image | CaptchaType::TextInput) + && file.captcha_image_data.is_none() + { + return Err(limit_error()); } else { None };🤖 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 `@src-tauri/src/adapters/driven/plugin/hoster_contract.rs` around lines 68 - 92, Update the CAPTCHA extraction block around ExtractedCaptchaChallenge so CaptchaType::Image and CaptchaType::TextInput require present, non-empty, size-bounded image data with a valid MIME type; reject missing or invalid data via limit_error(). Preserve the existing optional image-data behavior for challenge types that do not require a rendered image.src-tauri/src/adapters/driven/sqlite/captcha_repo.rs (1)
18-21: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider adding a LIMIT to
PENDING_CAPTCHA_METADATA_QUERY.
CAPTCHA_METADATA_QUERYbounds results withLIMIT 200, butPENDING_CAPTCHA_METADATA_QUERYhas no limit. While pending captchas should be few in practice, an unbounded query is inconsistent with the list query's defensive approach.♻️ Suggested fix
const PENDING_CAPTCHA_METADATA_QUERY: &str = "SELECT id, download_id, challenge_type, \ '[redacted]' AS challenge_url, NULL AS image_data, status, solver, attempts, created_at, expires_at, \ resolved_at, duration_ms, failure_reason FROM captcha_log WHERE status = ? \ - ORDER BY created_at ASC"; + ORDER BY created_at ASC LIMIT 200";🤖 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 `@src-tauri/src/adapters/driven/sqlite/captcha_repo.rs` around lines 18 - 21, Add a defensive LIMIT matching CAPTCHA_METADATA_QUERY to PENDING_CAPTCHA_METADATA_QUERY, preserving its existing filtering and created_at ordering.src-tauri/src/adapters/driven/logging/download_log_bridge.rs (1)
82-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
CaptchaSkippedlog entry lacks "CAPTCHA" prefix for consistency.All other CAPTCHA log messages include "CAPTCHA" in the text, but
CaptchaSkippedlogs only"[WARN] {reason}". If thereasonstring doesn't itself contain "CAPTCHA", the log entry will be ambiguous in the download log view.♻️ Suggested fix
DomainEvent::CaptchaSkipped { download_id, reason, .. } => { - store.push(download_id.0, format!("[WARN] {reason}")); + store.push(download_id.0, format!("[WARN] CAPTCHA skipped: {reason}")); }🤖 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 `@src-tauri/src/adapters/driven/logging/download_log_bridge.rs` around lines 82 - 88, Update the DomainEvent::CaptchaSkipped logging branch to include an explicit "CAPTCHA" prefix in the formatted warning message before the reason, matching the wording of other CAPTCHA log entries while preserving the existing warning level and download ID.
🤖 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 `@src-tauri/src/adapters/driven/plugin/hoster_contract.rs`:
- Around line 37-40: Update the hoster payload sizing around the
captcha_image_data field and MAX_HOSTER_PAYLOAD_BYTES so a maximum-size captcha
image, when JSON-serialized as a byte array, is accepted before the per-image
limit is enforced. Prefer increasing the payload cap with sufficient headroom
while preserving the existing captcha_image_data size validation.
In `@src/types/events.ts`:
- Around line 36-40: Update CaptchaEventPayload and the captcha entries in
TauriEventMap to model each backend event’s actual payload shape: include solver
and durationMs for captcha-solved, reason for captcha-skipped, durationMs for
captcha-timed-out, and retain the shared challengeId/downloadId fields. Keep
captcha events with distinct payload types so consumers can access
event-specific fields.
In `@src/views/CaptchaSolverSettings.tsx`:
- Around line 15-23: Rename the timeout state and setter in
CaptchaSolverSettings from timeout/setTimeout to distinct names such as
timeoutSeconds/setTimeoutSeconds, and update the useEffect and persistTimeout
references accordingly. Ensure no component-local identifier shadows the global
setTimeout.
---
Nitpick comments:
In `@src-tauri/src/adapters/driven/logging/download_log_bridge.rs`:
- Around line 82-88: Update the DomainEvent::CaptchaSkipped logging branch to
include an explicit "CAPTCHA" prefix in the formatted warning message before the
reason, matching the wording of other CAPTCHA log entries while preserving the
existing warning level and download ID.
In `@src-tauri/src/adapters/driven/plugin/hoster_contract.rs`:
- Around line 68-92: Update the CAPTCHA extraction block around
ExtractedCaptchaChallenge so CaptchaType::Image and CaptchaType::TextInput
require present, non-empty, size-bounded image data with a valid MIME type;
reject missing or invalid data via limit_error(). Preserve the existing optional
image-data behavior for challenge types that do not require a rendered image.
In `@src-tauri/src/adapters/driven/sqlite/captcha_repo.rs`:
- Around line 18-21: Add a defensive LIMIT matching CAPTCHA_METADATA_QUERY to
PENDING_CAPTCHA_METADATA_QUERY, preserving its existing filtering and created_at
ordering.
In `@src-tauri/src/domain/model/captcha.rs`:
- Around line 329-408: Replace the hand-rolled image format parsing in
image_metadata and jpeg_dimensions with the established imagesize crate’s
byte-slice dimension detection. Map supported PNG, GIF, and JPEG results to
their MIME types and preserve captcha_image_mime_type’s nonzero-dimension and
MAX_CAPTCHA_IMAGE_PIXELS validation behavior.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c9e04a2d-6dd9-4542-a97b-0d1a0f72ef52
📒 Files selected for processing (67)
CHANGELOG.mdsrc-tauri/src/adapters/driven/config/toml_config_store.rssrc-tauri/src/adapters/driven/event/tauri_bridge.rssrc-tauri/src/adapters/driven/logging/download_log_bridge.rssrc-tauri/src/adapters/driven/network/download_engine.rssrc-tauri/src/adapters/driven/network/download_engine_tests.rssrc-tauri/src/adapters/driven/plugin/hoster_contract.rssrc-tauri/src/adapters/driven/plugin/hoster_contract_tests.rssrc-tauri/src/adapters/driven/sqlite/captcha_repo.rssrc-tauri/src/adapters/driven/sqlite/captcha_repo_tests.rssrc-tauri/src/adapters/driven/sqlite/entities/captcha_log.rssrc-tauri/src/adapters/driven/sqlite/entities/mod.rssrc-tauri/src/adapters/driven/sqlite/migrations/m20260719_000011_create_captcha_log.rssrc-tauri/src/adapters/driven/sqlite/migrations/mod.rssrc-tauri/src/adapters/driven/sqlite/mod.rssrc-tauri/src/adapters/driving/tauri_ipc.rssrc-tauri/src/application/command_bus.rssrc-tauri/src/application/commands/cancel_download.rssrc-tauri/src/application/commands/captcha.rssrc-tauri/src/application/commands/captcha_tests.rssrc-tauri/src/application/commands/hoster_download_source.rssrc-tauri/src/application/commands/mod.rssrc-tauri/src/application/commands/remove_download.rssrc-tauri/src/application/commands/resolve_links.rssrc-tauri/src/application/commands/resolve_links_hoster_tests.rssrc-tauri/src/application/commands/resolve_links_tests.rssrc-tauri/src/application/commands/resolve_premium_source_test_support.rssrc-tauri/src/application/commands/tests_support.rssrc-tauri/src/application/queries/captcha.rssrc-tauri/src/application/queries/mod.rssrc-tauri/src/application/query_bus.rssrc-tauri/src/application/read_models/captcha_view.rssrc-tauri/src/application/read_models/mod.rssrc-tauri/src/application/services/queue_manager.rssrc-tauri/src/application/services/startup_recovery.rssrc-tauri/src/domain/error.rssrc-tauri/src/domain/event.rssrc-tauri/src/domain/model/captcha.rssrc-tauri/src/domain/model/config.rssrc-tauri/src/domain/model/download.rssrc-tauri/src/domain/model/mod.rssrc-tauri/src/domain/ports/driven/captcha_repository.rssrc-tauri/src/domain/ports/driven/captcha_solver.rssrc-tauri/src/domain/ports/driven/hoster_link.rssrc-tauri/src/domain/ports/driven/mod.rssrc-tauri/src/lib.rssrc/api/queries.tssrc/components/__tests__/ClipboardIndicator.test.tsxsrc/hooks/__tests__/useAppEffects.test.tssrc/hooks/__tests__/useDownloadEvents.test.tssrc/hooks/useCaptchaQueue.tssrc/hooks/useDownloadEvents.tssrc/i18n/__tests__/issue30-ui-fr.test.tsxsrc/i18n/locales/en.jsonsrc/i18n/locales/fr.jsonsrc/layouts/__tests__/AppLayout.test.tsxsrc/stores/__tests__/settingsStore.test.tssrc/types/captcha.tssrc/types/events.tssrc/types/settings.tssrc/views/CaptchaChallengePanel.tsxsrc/views/CaptchaSolverSettings.tsxsrc/views/CaptchaView.tsxsrc/views/LinkGrabberView/__tests__/LinkGrabberView.test.tsxsrc/views/SettingsView/__tests__/Sections.test.tsxsrc/views/SettingsView/__tests__/SettingsView.test.tsxsrc/views/__tests__/CaptchaView.test.tsx
|
Review follow-up for 74ad5a7:
Local and pre-push gates passed: Rust tests, frontend tests, formatting, lint, typecheck, Clippy, cargo-deny, Knip, and production build. |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src-tauri/src/adapters/driven/plugin/hoster_contract_tests.rs (1)
101-117: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winAssert that the maximum-size image is preserved.
is_ok()alone would still pass if parsing silently truncated or discarded the image. Assert the parsed CAPTCHA type and exactMAX_CAPTCHA_IMAGE_BYTESlength to protect the boundary contract.Proposed test strengthening
- assert!(parse_hoster_link(&payload).is_ok()); + let parsed = parse_hoster_link(&payload).expect("maximum-size image is accepted"); + let captcha = parsed.captcha.expect("captcha metadata"); + assert_eq!(captcha.challenge_type, CaptchaType::Image); + assert_eq!( + captcha.image_data.as_ref().map(|bytes| bytes.len()), + Some(MAX_CAPTCHA_IMAGE_BYTES) + );🤖 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 `@src-tauri/src/adapters/driven/plugin/hoster_contract_tests.rs` around lines 101 - 117, Strengthen test_parse_hoster_link_accepts_the_largest_valid_image_serialization by capturing the successful parsed result and asserting it contains the image CAPTCHA variant with exactly MAX_CAPTCHA_IMAGE_BYTES bytes. Preserve the existing maximum-size payload setup while verifying the image is neither truncated nor discarded.src-tauri/src/application/queries/captcha.rs (1)
111-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest doesn't actually exercise "oldest" tie-breaking.
Only one pending challenge exists in the fixture (
captcha-oldest), so thechallenge_id: Nonefallback path trivially returns it regardless of whether oldest-first ordering is implemented correctly. Add a second, newer pending challenge to make this assertion meaningful.♻️ Suggested test strengthening
async fn get_pending_supports_explicit_id_and_oldest_fallback() { let mut solved = challenge("captcha-solved", 500); solved.solve(600, "manual").unwrap(); - let bus = bus(vec![challenge("captcha-oldest", 1_000), solved]); + let bus = bus(vec![ + challenge("captcha-oldest", 1_000), + challenge("captcha-newer", 2_000), + solved, + ]);🤖 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 `@src-tauri/src/application/queries/captcha.rs` around lines 111 - 140, Strengthen get_pending_supports_explicit_id_and_oldest_fallback by adding a second pending challenge with a later timestamp to the bus fixture. Keep the existing assertions and verify that the challenge_id: None fallback still returns captcha-oldest, thereby exercising oldest-first selection rather than a single-item result.
🤖 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.
Nitpick comments:
In `@src-tauri/src/adapters/driven/plugin/hoster_contract_tests.rs`:
- Around line 101-117: Strengthen
test_parse_hoster_link_accepts_the_largest_valid_image_serialization by
capturing the successful parsed result and asserting it contains the image
CAPTCHA variant with exactly MAX_CAPTCHA_IMAGE_BYTES bytes. Preserve the
existing maximum-size payload setup while verifying the image is neither
truncated nor discarded.
In `@src-tauri/src/application/queries/captcha.rs`:
- Around line 111-140: Strengthen
get_pending_supports_explicit_id_and_oldest_fallback by adding a second pending
challenge with a later timestamp to the bus fixture. Keep the existing
assertions and verify that the challenge_id: None fallback still returns
captcha-oldest, thereby exercising oldest-first selection rather than a
single-item result.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: e45e18f9-7689-4c7f-b74a-e8c460a2ac74
⛔ Files ignored due to path filters (1)
src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (31)
CHANGELOG.mdsrc-tauri/Cargo.tomlsrc-tauri/src/adapters/driven/config/toml_config_store.rssrc-tauri/src/adapters/driven/event/tauri_bridge.rssrc-tauri/src/adapters/driven/logging/download_log_bridge.rssrc-tauri/src/adapters/driven/network/download_engine.rssrc-tauri/src/adapters/driven/network/download_engine_tests.rssrc-tauri/src/adapters/driven/plugin/hoster_contract.rssrc-tauri/src/adapters/driven/plugin/hoster_contract_tests.rssrc-tauri/src/adapters/driven/sqlite/captcha_repo.rssrc-tauri/src/adapters/driven/sqlite/captcha_repo_tests.rssrc-tauri/src/application/commands/cancel_download.rssrc-tauri/src/application/commands/captcha.rssrc-tauri/src/application/commands/captcha_tests.rssrc-tauri/src/application/commands/remove_download.rssrc-tauri/src/application/queries/captcha.rssrc-tauri/src/application/read_models/captcha_view.rssrc-tauri/src/domain/error.rssrc-tauri/src/domain/event.rssrc-tauri/src/domain/model/captcha.rssrc-tauri/src/domain/model/download.rssrc-tauri/src/domain/ports/driven/hoster_link.rssrc/hooks/__tests__/useCaptchaQueue.test.tsxsrc/hooks/useCaptchaQueue.tssrc/hooks/useDownloadEvents.tssrc/types/captcha.tssrc/types/events.tssrc/views/CaptchaChallengePanel.tsxsrc/views/CaptchaSolverSettings.tsxsrc/views/CaptchaView.tsxsrc/views/__tests__/CaptchaView.test.tsx
🚧 Files skipped from review as they are similar to previous changes (17)
- src/types/captcha.ts
- src-tauri/src/adapters/driven/sqlite/captcha_repo_tests.rs
- src/hooks/useDownloadEvents.ts
- src/hooks/useCaptchaQueue.ts
- src/views/CaptchaSolverSettings.tsx
- src-tauri/src/domain/model/download.rs
- src-tauri/src/adapters/driven/logging/download_log_bridge.rs
- src-tauri/src/adapters/driven/network/download_engine_tests.rs
- src/views/CaptchaView.tsx
- src-tauri/src/adapters/driven/network/download_engine.rs
- src-tauri/src/domain/event.rs
- src-tauri/src/adapters/driven/event/tauri_bridge.rs
- src-tauri/src/application/commands/cancel_download.rs
- src-tauri/src/adapters/driven/sqlite/captcha_repo.rs
- src-tauri/src/adapters/driven/plugin/hoster_contract.rs
- src-tauri/src/application/commands/captcha.rs
- src-tauri/src/domain/model/captcha.rs
There was a problem hiding this comment.
All reported issues were addressed across 32 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Summary
Verification
Result: 734 frontend tests passed; 1,728 Rust unit tests and 4 Rust integration tests passed. Five OS/format-dependent unit tests and two doctests remain ignored.
Scope note
reCAPTCHA/hCaptcha solving and a plugin ABI for submitting a manual answer remain out of scope for MAT-140. The current official 1fichier flow surfaces these challenge types as unsupported so the user can skip or retry; image/text challenges are handled by the manual solver.
Linear: MAT-140
Summary by cubic
Implements the end-to-end manual CAPTCHA pipeline with a separate queue, CQRS/Tauri events, UI, timeout with automatic download resumption, compact base64 image transport, and persistent SQLite
captcha_log(with migration). Also hardens recovery and removal flows. Satisfies Linear MAT-140 acceptance.New Features
download-queuedwhen re-queued.captcha_solve,captcha_skip,captcha_retry; Queries:captcha_list,captcha_get_pending; Tauri events:captcha-pending,captcha-solved,captcha-skipped,captcha-timed-out.requires_captcha, type, optional image); engine emits typed internalCaptchaRequired.captcha_timeout_secondswith safe defaults and bounds; clean scheduling on CAPTCHA and startup recovery.Bug Fixes
captcha-requiredto avoid frontend races; download logging includesdownload-queuedand CAPTCHA lifecycle entries.Written for commit 4075849. Summary will update on new commits.
Summary by CodeRabbit
download-queued, plus CAPTCHA pending/solved/skipped/timed-out.