Development - #207
Open
jordanlambrecht wants to merge 75 commits into
Open
Conversation
A TorrentLeech account with 2FA enabled cannot be polled at all today. The login posts only username and password, TL answers with no session cookie, and the adapter reports "Invalid TorrentLeech credentials" — sending the user off to re-check a password that was never wrong. TL's own login form takes a third input, alt2FAToken. It is NOT a TOTP secret and nothing here computes a one-time code: the site issues a static token at Site Profile => Alt 2FA Token for exactly this case, which is also how other clients drive this login. - parseTlCredentials reads an optional alt2FAToken. Blank is treated as absent, and the all-lowercase alt2fatoken spelling is accepted too, because that is the key other clients store it under and being strict about the capitalisation of a pasted token helps nobody. - login() sends the field only when it is set, so a request for an account without 2FA is byte-identical to what it was before. - A refused login now reads the body to say why. "One Time Password" in the response names 2FA and points at the Site Profile page; a refusal when a token WAS supplied blames the token as well as the password. - Both credential entry points get an optional field: AddTrackerDialog and TrackerSettingsSheet's change-credentials flow. Drive-by, called out for review: AddTrackerDialog's resetForm never cleared the TorrentLeech username and password, so a closed dialog kept the password in memory and re-showed it on reopen. Adding a third secret without fixing that would have made it worse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured against the live site: TorrentLeech answers a REJECTED login with 200 and the login page, and still sets tluid, tlpass, member_id, pass_hash and session_id. The adapter's success check was "did we get a tluid", so a refused login looked like a good one, the never-signed-in session was cached, and every later page fetch came back as the login page — surfaced as "Session expired". That error is what a user actually sees today when their account has 2FA, and it points at exactly the wrong thing: it sends them off to refresh a session that was never established, rather than telling them a token is missing. It also made the Alt 2FA Token detection added in the previous commit unreachable, since the cookie check passed before the body was ever examined. A real login answers 302. The check is now tluid AND a redirect, falling back to inspecting the body when it is not one — and the fallback rejects only on POSITIVE evidence of a refusal (the One Time Password heading, a login-form, a link to /user/account/login). A 200 that does not look like the login page is still accepted, so this tightens a wrong check without inventing a new way to fail for anyone it works for today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A user who configures a proxy still leaked their real address to TorrentLeech.
The profile GET was proxied; the login POST was not — so every
re-authentication, the one request that carries the password, went out
directly. For anyone proxying because the tracker must only ever see a
specific exit address, that is the whole feature failing on its most sensitive
request.
The comment in the adapter said this was impossible ("proxyFetch is GET-only
with no body support"). That has not been true for a while: proxyFetch takes
`method` and `body` already. The real gap was narrower — it discarded the
response, so a caller could not read the Set-Cookie it logged in for.
- proxyFetch exposes `headers` and adds `text()`. Without headers, no adapter
that authenticates can ever use the proxy.
- TorrentLeech's login uses it when a proxyAgent is present and keeps the
direct fetch otherwise, so nothing changes for users with no proxy
configured. https.request does not follow redirects, matching the
`redirect: "manual"` the direct path relies on to catch the 302 that carries
the session cookie.
- The 2FA and bad-credential detection reads whichever body it got, so the
error messages are identical on both paths.
Stacked on the Alt 2FA Token change — both touch login(), and the error
handling here builds on the branching added there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…parser Every stat the TorrentLeech adapter reports except username and uploaded is wrong, and each one is wrong in a way that looks plausible rather than broken, so nothing surfaces it: field reported class "ic TL" -- for every account on the site downloaded 0 bytes -- for every account on the site ratio Infinity, derived from that zero buffer the entire upload total, derived from that zero seeding the leading digits of a byte total leeching the leading digits of a byte total hit & runs null, meaning "this tracker does not expose it" Four separate causes: - `.profile-downloaded-details` does not exist. Only the uploaded figure carries a details class; its downloaded counterpart is a bare `.profile-info-details` span inside `.profile-downloaded`. The selector matched nothing, so every account parses as having downloaded zero bytes, which makes computeRatio return Infinity and computeBufferBytes return the entire upload total. An account that owes the tracker is reported as unconditionally healthy, and checkRatioBelowMinimum can never fire because Infinity < minimumRatio is false. - The user class was matched with a regex over the whole document's text. Every TorrentLeech page carries a nav link reading "Classic TL", and it comes first, so /Class:?\s*\n?\s*([A-Za-z][A-Za-z ]*)/ captured "ic TL". - Seeding and leeching counts took the first number in a top-bar cell that reads as a size followed by the count in parentheses. The first number is the leading digits of the byte total, not a torrent count. - Hit and runs were hardcoded null while the page has a `title="Hit and Run"` cell all along. For a tool whose purpose is catching hit and runs, that is the number that matters. Class and points now come from the semantic markup (`.label-user-class`, `.total-TL-points`) with the old text-scraping kept as a fallback, so a layout change degrades instead of silently returning a wrong answer. The tests could not have caught any of this: the fixture was written from the same assumption as the parser, inventing a `profile-downloaded-details` span the site never emits, so it agreed with the bug. It is replaced with markup copied from a live page -- nav link, top bar and asymmetric spans intact -- and the parser now reproduces every figure that page displays, including the buffer TorrentLeech prints itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Satisfaction was decided by seed time alone:
const requiredSeedSeconds =
rules?.seedTimeHours > 0 ? rules.seedTimeHours * 3600 : null
const unsatisfied = requiredSeedSeconds
? torrents.filter((t) => t.seedingTime < requiredSeedSeconds)
: []
Ratio never entered it, so a tracker whose rule is an either/or could not be
expressed. TorrentLeech states one outright — "There are two ways for you to
give back to the community": seed to 1:1, or seed for your class's minimum
time. Its registry entry therefore carried `seedTimeHours: 0`, and the
consequence was not that its torrents were judged leniently. It was that
`requiredSeedSeconds` came out null and the Unsatisfied table, the unsatisfied
count and the H&R-risk card **all silently disappeared** for that tracker. No
error, no empty state, just nothing — which is why it has gone unnoticed.
`rules.satisfactionMode: "any" | "all"` now says how the thresholds combine,
and satisfaction moves into `@/lib/satisfaction` as pure functions.
**Ratio participates only for an entry that declares a mode.** `minimumRatio`
has always been in the registry as an ACCOUNT-level figure — it drives the
ratio-danger alert and the analytics baseline. Reading it as a per-torrent
requirement wherever it is non-zero would re-interpret all 55 entries against
rules nobody has re-read, and for a seed-time-only tracker that lands in the
unsafe direction: torrents marked satisfied that are not, which is how hit and
runs are earned. An entry without a mode behaves exactly as it does today, so
the field doubles as a marker that a tracker's rules have been verified.
Progress is the nearer route under `any` and the laggard under `all`. That is
not cosmetic: it drives the sort order, and sorting on seed time alone buried a
torrent sitting at 0.99 ratio — releasable within the hour — beneath one with
days on the clock and no way to clear. "Remaining" now names which route is
nearer rather than always quoting seed time, and reports nothing owed once the
torrent is satisfied by the other route.
`ratioProgress` deliberately treats a non-finite or negative ratio as zero
progress rather than as complete. Clients report Infinity for a torrent that
has downloaded nothing and -1 as a sentinel; either one read as "far past the
requirement" would mark torrents satisfied that are not.
TorrentLeech's entry is filled in from its own wiki: `satisfactionMode: "any"`,
and `seedTimeHours: 240` — 10 days, the Registered-class requirement and the
longest on the site, chosen deliberately because the class is not knowable from
the registry and over-seeding is the safe direction to be wrong in.
28 tests, including the sentinel-ratio cases and an end-to-end check that the
TorrentLeech entry now resolves to a real requirement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trivy failed the Docker Build & Scan job on 32 HIGH/CRITICAL findings in two Go binaries under /schema-sync/node_modules. The TypeScript 7 native compiler (10 HIGH) reaches the image only through devDependencies, so the schema-sync stage now installs with --prod. That needs --ignore-scripts alongside it, because the "prepare": "husky" script is a devDependency and would otherwise fail the install. esbuild 0.25.12 (22 findings, one CRITICAL) is built on go1.23.12 and arrives via drizzle-kit, which pins ^0.25.x at both 0.31.10 and 1.0.0-rc.4. The 0.25.x line was abandoned after Nov 2025 and drizzle-orm#5481 has been open since March 2026, so the esbuild override is pinned to ^0.28.2 (go1.26.5) instead. That collapses three esbuild copies to one and drops the abandoned @esbuild-kit path along with it (esbuild ~0.18.20, GHSA-67mh-4wv8-2f99). Seven Go stdlib CVEs are still genuinely unfixed at go1.26.5. They are documented in .trivyignore rather than suppressed, and clear on the first esbuild built with Go 1.27. Verified: drizzle-kit push applies all 19 tables against postgres:18 from a --prod install, and Trivy reports no gobinary findings.
Knip Code AnalysisFound 128 total issues
View details
Use |
|
✅ Security audit passed Passed (38/38)
Summary: 38/38 checks passed See |
…odule server-only
…cover both writers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.