Skip to content

fix(tracker-adapters): accept numeric UNIT3D byte and ratio fields - #11

Merged
patrickdundas merged 1 commit into
mainfrom
fix/unit3d-numeric-fields
Aug 21, 2026
Merged

fix(tracker-adapters): accept numeric UNIT3D byte and ratio fields#11
patrickdundas merged 1 commit into
mainfrom
fix/unit3d-numeric-fields

Conversation

@patrickdundas

Copy link
Copy Markdown
Owner

The bug

Adding Blutopia failed with a bare tracker test failed, which reads like a rejected API key. The real error, only visible in the container log, was:

error: "e.trim is not a function"

UNIT3D deployments disagree about the JSON types of these fields. Seed Pool and DarkPeers return formatted strings. Blutopia returns bare numbers:

{"username":"...","group":"User","uploaded":53687091200,"downloaded":1073741824,
 "ratio":50,"buffer":133143986176,"seeding":0,"leeching":0,
 "seedbonus":"200001.00","hit_and_runs":0}

parseBytes() opened with formatted.trim(), so the numeric shape threw immediately. Note seedbonus is a string in the same response while ratio is a number, so this is not a simple "one site is numeric" split — the shapes are mixed within a single payload.

The fix

parseBytes now accepts string | number. The numeric path truncates fractional counts, since a fractional byte is meaningless and BigInt() throws on a non-integral Number, and it rejects negatives exactly as the string path does. Non-finite input returns 0n rather than throwing inside BigInt().

Unit3dApiResponse widens the affected fields to unions, and ratio/seedbonus go through a small toNumber() helper, because parseFloat() only accepts a string.

The string path is untouched.

Why this was worth more than a coercion at the call site

Coercing in the adapter would have fixed Blutopia and left the next UNIT3D site to rediscover it. The type union documents that both shapes are legitimate UNIT3D.

Also in here

The Blutopia registry entry gains four things from a fresh read of the site's rules:

  • Its IRC network is retired in favour of Matrix. blu-announce is the upload feed. This matters operationally: autobrr ships no Blutopia definition and searching for one is a dead end, because the network it would connect to no longer exists.
  • The tracker's client blacklist — qBittorrent 4.5.0/4.5.1, Transmission 2.04/2.93/4.0.6, uTorrent 3.6.x.
  • The connectability requirement, which the site enforces rather than merely advises.
  • RSS feed URLs embed the PID, so a leaked feed is treated as account sharing, and the site's own warning that RSS auto-downloading can wreck a ratio.

The existing Hit & Run and Account Pruning sections were checked against the current rules and are accurate, so they are unchanged.

Tests

5 new cases in parser-numeric-bytes.test.ts using the exact values Blutopia returned, plus the string path, truncation, negatives and non-finite input. Typecheck and biome clean. 2951 passing; the single failure in chart-transforms.test.ts is pre-existing on main and unrelated.

🤖 Generated with Claude Code

UNIT3D deployments disagree about the JSON types of these fields. Seed Pool
and DarkPeers return formatted strings ("1.5 TiB", "2.31"). Blutopia returns
bare numbers:

  {"uploaded":53687091200,"downloaded":1073741824,"ratio":50,
   "buffer":133143986176,"seedbonus":"200001.00","hit_and_runs":0}

parseBytes() opened with formatted.trim(), so the numeric shape threw
"formatted.trim is not a function". That surfaced in the UI as a bare
"tracker test failed", which reads like a rejected API key and sends you
looking at credentials instead of at the response body.

parseBytes now takes string | number, truncating fractional counts and
rejecting negatives the same way the string path does. The response
interface widens to unions, and ratio/seedbonus go through a toNumber()
helper because parseFloat() only accepts a string.

Also records in the Blutopia registry entry that its IRC network is retired
in favour of Matrix, so autobrr has no network to connect to; the tracker's
client blacklist; the connectability requirement; and the PID exposure and
ratio risk in its RSS feeds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@patrickdundas
patrickdundas merged commit 806efd8 into main Aug 21, 2026
@patrickdundas
patrickdundas deleted the fix/unit3d-numeric-fields branch August 21, 2026 22:08
@github-actions

Copy link
Copy Markdown

Knip Code Analysis

Found 9 total issues

Category Count
Unused Dependencies 1
Unused Dev Dependencies 3
Unused Exports 5
View details

Run pnpm knip locally to see the full report.

Use pnpm knip:filter pattern to filter results by file path.


Use /** @public */ JSDoc tags to mark intentionally exported symbols.

@github-actions

Copy link
Copy Markdown

🚫 Security audit failed

Critical Failures

Check File Details
External fetch calls have timeouts src/lib/adapters/btn.ts:69 fetch() call without a timeout signal — add signal: AbortSignal.timeout(ms)

Warnings

Check File Details
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:108 catch block swallows error without logging or re-throwing
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:114 catch block swallows error without logging or re-throwing
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:175 catch block swallows error without logging or re-throwing
No swallowed errors in catch blocks src/lib/adapters/iptorrents.ts:181 catch block swallows error without logging or re-throwing

Passed (36/38)

  • ✅ Auth enforcement on protected routes (per-handler)
  • ✅ No dangerous functions (eval, innerHTML, etc.)
  • ✅ No hardcoded secrets in source
  • ✅ Security headers in next.config.ts
  • ✅ Cookie security (httpOnly, sameSite, secure)
  • ✅ No sensitive fields in API responses
  • ✅ No .env files committed to repo
  • ✅ No raw SQL in API routes
  • ✅ No fetch/redirect with unvalidated URLs in routes
  • ✅ Timing-safe comparison for secret values
  • ✅ No raw SQL migration files (schema-first only)
  • ✅ Docker container runs as non-root user
  • ✅ Public routes match proxy allowlist
  • ✅ File delete operations have path traversal defense
  • ✅ Password hashing uses Argon2 (not SHA-256/bcrypt)
  • ✅ Encrypted columns written via encrypt()
  • ✅ TOTP 2FA flow integrity
  • ✅ Emergency lockdown flow integrity
  • ✅ Scrub & delete (nuke) flow integrity
  • ✅ Backup restore flow integrity
  • ✅ Login flow integrity
  • ✅ Auth result checked before proceeding
  • ✅ Backup password inputs bounded before key derivation
  • ✅ Webhook delivery fetch uses redirect: "error"
  • ✅ SESSION_SECRET minimum-length guard in auth/crypto modules
  • ✅ Notification URL validators include SSRF protection
  • ✅ Dockerfile does not COPY sensitive files
  • ✅ No secret env vars in client components
  • ✅ Adapter Cookie headers guard against injection
  • ✅ Adapter files do not log credential values
  • ✅ No console.log in API routes
  • ✅ No TODO/FIXME in security-critical files
  • ✅ JSON.parse wrapped in try-catch
  • ✅ Request body size validation on upload routes
  • ✅ BigInt fields use string serialization
  • ✅ No raw error messages in API responses

Summary: 36/38 checks passed
1 critical failure(s)
— 1 warning(s)

See scripts/security-audit.ts for check definitions and SECURITY.md for the full security architecture.

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.

1 participant