Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Plugin `vortex-mod-1fichier` v1.0.0** (scope `plugin`, sprint task 38, PRD-v2 §P1.19 / PRD §4.1, §4.2): new official Vortex plugin in a sibling repo `vortex-mod-1fichier/`. Hoster category, `http` capability only — no subprocess, no captcha solver in v1. Recognises `https://(www\.)?1fichier\.com/?<id>` URLs (alphanumeric `<id>` of 6+ characters; rejected when the path contains anything other than `/`, when the id is shorter than 6 chars, or when extra query tokens appear before the id). Operates in two modes selected automatically: **premium** when a 1fichier API key is configured in the host credential store under the plugin's own service name (`vortex-mod-1fichier`), in which case the plugin posts to `https://api.1fichier.com/v1/download/get_token.cgi` with `Authorization: Bearer <key>` and returns the one-shot direct CDN URL plus optional `traffic_used` / `traffic_total` monitoring; **free** otherwise (or as a fallback when the API rejects the key as invalid / expired), in which case the plugin scrapes the public landing page and surfaces `wait_seconds` (read from `data-wait="…"`, `class="countdown"`, or `var c = …` — covers the three layouts 1fichier ships) plus a `requires_captcha` flag (detected via `g-recaptcha` / `h-captcha` / `class="captcha"`) so the host's `WaitManager` (task 39) can park the download in `Waiting` state for the advertised duration before the future captcha solver pipeline (task 43+) takes over. Free `resolve_stream_url` therefore surfaces `PluginError::CaptchaRequired` until the captcha pipeline ships — the metadata path (`extract_links`) still succeeds so the UI can show filename / size / wait countdown. The premium response parser maps the `{"status":"KO","message":"…"}` envelope onto typed errors: `Invalid key` → `InvalidCredentials`, `Subscription expired` / `not premium` → `AccountExpired` (both trigger free-mode auto-fallback inside `extract_links`), `Flood detected` / rate / too many → `RateLimited`, `not found` / `offline` → `Offline`, anything else → `InvalidApiResponse`. Landing-page parser also detects the `the file you are trying to access is no longer available` / `requested file could not be found` / `fichier introuvable` strings and surfaces `PluginError::Offline` so the engine flags the link as removed instead of retrying. Body size capped at `MAX_BODY_BYTES = 1 MiB` against memory abuse from a malicious server (real 1fichier landing pages weigh well under 100 KB). `FileLink.resumable` is reported as `true` so the host's segmented engine engages multi-connection ranged downloads. The IPC envelope adds `mode: "free" | "premium"`, `wait_seconds: Option<u32>`, `requires_captcha: bool`, and optional `traffic_used_bytes` / `traffic_total_bytes` alongside the standard `id` / `url` / `filename` / `size_bytes` / `direct_url` / `resumable` shape. 53 native unit tests (URL matcher × 16, free landing parser × 13 incl. recaptcha detection / offline / no-metadata / `var c` countdown, premium API parser × 14 covering OK / OK+traffic / KO classification / malformed JSON, credential parser × 4, response builders × 5, routing helpers × 5) + 10 fixture-driven cases over `tests/fixtures/*.html` and `tests/fixtures/*.json` (basic landing, captcha landing, JS countdown landing, offline, no-metadata, premium OK, premium OK+traffic, premium invalid-key, premium expired, premium rate-limit) + 3 WASM-loaded smoke tests (Extism with stub `http_request` + stub `get_credential` that signals "no credential" — exercises the `can_handle` / `supports_playlist` exports against the compiled `wasm32-wasip1` artefact). Registered in `vortex/registry/registry.toml` with `checksum_sha256 = 02035a49…3232c` (wasm) and `checksum_sha256_toml = c16cb7d4…87c091` (manifest).

- **Hoster wait-time flow** (scope `download`, sprint task 39, PRD-v2 §P1.20 / PRD §6.1.2): new `adapters/driven/network/wait_manager.rs` service owns one `tokio::time::sleep` per parked download and drives the `Waiting` ↔ `Downloading` cycle that hoster cooldowns require. `schedule_wait(id, total_seconds, reason)` loads the aggregate, transitions it via `Download::wait()`, persists, then publishes both the legacy `DownloadWaiting` (state-transition signal) and the new rich `DownloadWaitingStarted { id, until_unix_ms, total_seconds, reason }` (front-end countdown payload). Natural expiry calls `expire_wait(id)` which transitions back to `Downloading` via `resume_from_wait`, persists, and publishes `DownloadWaitingEnded { expired_naturally: true }` plus `DownloadResumedFromWait`. `skip_wait(id)` (premium "skip-the-queue" path) aborts the timer and resumes immediately with `expired_naturally: false`. `cancel_wait(id)` aborts the timer cleanly and emits the ended event without transitioning the aggregate (used by the cancel flow before `handle_cancel_download` runs). Multi-concurrent waits run in parallel — each download owns its own `JoinHandle` in `Mutex<HashMap<DownloadId, JoinHandle>>` so a 10s wait, a 30s wait, and a 60s wait all expire independently. New IPC `download_skip_wait(id)` exposed; existing `download_cancel` now calls `wait_manager.cancel_wait` first to avoid a spurious resume racing with the cancellation. `Clock` port grew a default `now_unix_ms()` (default impl multiplies `now_unix_secs()` by 1 000; `SystemClock` overrides for full ms precision). New `DomainEvent::DownloadWaitingStarted` / `::DownloadWaitingEnded` variants wired through `tauri_bridge` (event names `download-waiting-started` / `download-waiting-ended`) and the download log bridge. Frontend additions: `useCountdown(untilUnixMs)` hook ticks once per second and returns `{ remainingSeconds, label, expired }` with `MM:SS` (or `HH:MM:SS` past one hour) formatting, clamping the remainder to `[0, ∞)` and treating a `null` deadline as a no-op; `downloadStore` gained `waitMap` + `setWait` / `clearWait` populated from the new events; `WaitCountdownCell` replaces `EtaCell` in the table whenever a row's `state === "Waiting"`, rendering the live label plus a `SkipForward` icon button that invokes `download_skip_wait`. 7 `wait_manager` async tests (paused tokio runtime, deterministic `FakeClock`) cover schedule + Started payload, natural expiry → resume + Ended `(true)`, cancel before expiry → Ended `(false)` + no resume + state untouched, skip → Ended `(false)` + immediate resume, unknown-id skip returns `AppError::NotFound`, three concurrent waits expire independently with one Started + one Ended each, and `cancel_wait` on an unknown id is a silent no-op. 6 `useCountdown` tests + 5 `downloadStore` wait-map tests + 3 `WaitCountdownCell` tests + 2 new `DomainEvent` payload tests round out the coverage. `tokio` dev-dependency gained the `test-util` feature so `#[tokio::test(start_paused = true)]` + `tokio::time::advance` work for deterministic timer tests.

- **Plugin `vortex-mod-mega` v1.0.0** (scope `plugin`, sprint task 37 ✅, PRD-v2 §P1.18 / PRD §4.1): new official Vortex plugin in a sibling repo `vortex-mod-mega/` (commits `e949c7b` foundation, `5e25cad` folder enumeration). Hoster category, `http` capability only — no wait, no captcha, no subprocess. Recognises both modern (`https://mega.nz/file/<id>#<key>` / `https://mega.nz/folder/<id>#<key>`) and legacy (`https://mega.co.nz/#!<id>!<key>` / `#F!<id>!<key>`) URL shapes; classification is anchored on strict id (6–12 chars base64url) and key length (43 chars for files / 22 chars for folders) so a truncated URL never reaches the JSON-RPC layer. File key parsing decodes the 32-byte blob (8 × u32 BE) and XOR-folds words 0..3 with words 4..7 to recover the AES-128 key, then exposes the raw IV (top 8 bytes) and `metaMac` (bottom 8 bytes) verbatim. Folder key parsing decodes the 16-byte master key directly. Resolution uses one HTTP round-trip per file via the `g` command (`POST https://g.api.mega.co.nz/cs?id=<seq>` body `[{"a":"g","g":1,"p":"<fileId>"}]` → `[{"g":"<encryptedCdnUrl>","s":<size>,"at":"<encAttrs>","msd":1}]`); folder URLs trigger the `f` command (`[{"a":"f","c":1,"r":1}]`) to enumerate children. The `id` query param uses an atomic seq counter (`AtomicU32::fetch_add`) so two concurrent invocations never race for the same id. MEGA error vocabulary maps onto the host's plugin-error type: code `-9` → `PluginError::Offline`, codes `-3` and `-6` → `PluginError::RateLimited`, any other negative code → `PluginError::ApiError(code)`; HTTP statuses 429 and 509 also collapse to `RateLimited` so the engine's exponential backoff kicks in instead of failing the link. The IPC envelope extends the standard `FileLink` shape with a non-optional `encryption: EncryptionInfo { scheme: "mega-aes128-ctr", aes_key_hex, iv_hex, meta_mac_hex }` so any host that treats the bytes from `direct_url` as plaintext crashes loudly rather than writing ciphertext to disk; `direct_url` itself is `Option<String>` — `Some(<cdnUrl>)` for the file-URL path, `None` for folder children which the host re-resolves via `resolve_stream_url` per child. The crypto core (`src/crypto.rs`) ships three pieces: (1) `MegaDecryptor` for CTR streaming (`process`/`encrypt_only`/`absorb_plaintext`/`finalize`/`verify_against`), (2) `aes128_ecb_decrypt(key, &mut [u8])` for unwrapping per-node 32-byte key blobs with the folder master key, and (3) `aes128_cbc_decrypt(key, iv, &mut [u8])` for decrypting the AES-128-CBC node attribute blob (IV = 0₁₂₈, "MEGA{json}" envelope). The chunk schedule is the canonical MEGA one — 128 KiB, 256 KiB, 384 KiB, 512 KiB, 640 KiB, 768 KiB, 896 KiB, 1024 KiB, then 1 MiB forever — implemented as `chunk_size_at(usize) -> u64` so it can be unit-tested independently. Memory bounds: caller buffer is whatever the host hands in (test exercises 4 KiB caller buffers over a 16 MiB stream), and decryptor state grows only with `chunk_macs.len()` (≈ 16 entries × 16 B for 16 MiB, ≈ 4 K entries × 16 B = 64 KB for a hypothetical 4 GB stream — bounded extrapolation rather than 4 GB allocation in CI). Folder enumeration (`src/node_parser.rs::parse_folder_listing`) walks the `f` response, filters file children (`t == 0`), unwraps each encrypted key blob via AES-128-ECB with the folder master key, XOR-folds the recovered 32-byte raw key into the same `MegaFileKey` shape as a URL key, and decrypts the `a` field via AES-128-CBC to recover the filename; non-decryptable attributes degrade gracefully (filename = `None`) so the host can fall back to the handle. Each child is emitted with its raw 32-byte key reserialised into a synthetic `mega.nz/file/<handle>#<base64url(key)>` URL, so the host can re-feed every entry through the regular file-URL pipeline. 61 native unit tests across `error` / `url_matcher` / `key_parser` / `crypto` (incl. ECB / CBC round-trips + alignment checks) / `api_client` / `node_parser` (incl. fixture-driven decrypt, non-file skip, short-key skip, corrupted attrs fallback, garbage-body rejection, `split_key_blob` cases) / `lib` (incl. folder builder + synthetic-URL round-trip via `url_matcher`) + 4 WASM-loaded smoke tests for `can_handle` / `supports_playlist` + 1 end-to-end folder-decryption test (`tests/folder_e2e.rs::wasm_extract_links_decrypts_folder_listing_with_one_child` loads the WASM artefact via Extism, stubs `http_request` to return a synthetic `f` response built with real AES-128-ECB key wrapping + AES-128-CBC attribute encryption, then asserts the decrypted child has handle "ChildHnd", filename "holiday.mp4", size 12345, synthetic `mega.nz/file/ChildHnd#<key>` URL, and null `direct_url`); clippy clean (`-D warnings`) and `cargo fmt` clean, 1 JSON fixture for the `g` response shape. Registered in `vortex/registry/registry.toml` with `checksum_sha256 = ee4fd771…290f07` (wasm) and `checksum_sha256_toml = c9420281…ef805` (manifest). Acceptance criteria status (6/6 verified) and per-criterion verification methods are recorded in `.claude/output/sprints/prd-v2-roadmap/tasks/37-plugin-mega.md`.
Expand Down
12 changes: 12 additions & 0 deletions registry/registry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,15 @@ checksum_sha256 = "ee4fd7716f56802e43cef2804ddf6a49346177b716afffc974867a33
checksum_sha256_toml = "c9420281eb587afc938142c5f70104b0febb176fe68cd2ec441c275c0b4ef805"
official = true
min_vortex_version = "0.1.0"

[[plugin]]
name = "vortex-mod-1fichier"
description = "1fichier hoster — free (60s wait + captcha stub) and premium (API key)"
author = "vortex-community"
version = "1.0.0"
category = "hoster"
repository = "https://github.com/mpiton/vortex-mod-1fichier"
checksum_sha256 = "02035a49da81566cf7e4ff64cd0b5ed858e05dfdca643dbd14e53b359d93232c"
checksum_sha256_toml = "c16cb7d418ee6d261114b0ed66701629f7aa7b92317dc97fb7593ede9487c091"
official = true
min_vortex_version = "0.1.0"
Loading