feat(qbittorrent): login compat for v4/v5.0-5.2 and three auth modes

- Login success = HTTP 200 "Ok." or 204 (5.2+); session cookie taken from
  Set-Cookie by name (SID / QBT_SID_<port> / custom) and sent back as-is.
- Failure classes: "Fails." / 401 -> auth; 403 IP ban / 429 -> rate;
  wrong API Key (403) -> auth with hint; 5xx -> protocol.
- Downloader instance auth kind: password / api_key (qB 5.2+ Bearer, no
  /auth/login) / none (whitelisted subnet); stored in settings.authKind,
  credential write-only in secret_ref, cleared when switching to none.
  One qbAuthConfig() shared by sync, agent capability and connection test.
- Settings form: auth kind select, password / API Key fields (no echo).
- testkit: mock qB 5.2 login behaviour, IP ban, bypassAuth;
  makeTorrent({ realPieces }) builds real piece hashes for real qB checks.
- Real-container matrix (QB_REAL, skipped by default): 5.1.2 and 5.2.4,
  login / add / query / tags / pause-resume / delete keeping data.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 20:42:17 +08:00
1 parent c846e91cc4
commit 74f1566985
14 files changed
+756 -59

No files matched your search

@@ -9,6 +9,7 @@ import {
getSiteAdapter,
getSiteDefinition,
type ProbeResult,
qbAuthConfig,
type SecretResolver,
SiteError,
type TorrentSource,
@@ -97,12 +98,10 @@ export function createPorts(
if (hit && hit.stamp === stamp) return hit.client;
const settings = (inst.settings ?? {}) as Record<string, unknown>;
const secret = await resolveSecret(inst.secret_ref);
const useApiKey = settings.authKind === "api_key";
const client = createQbClient({
instanceId: inst.id,
baseUrl: inst.base_url,
...(inst.username ? { username: inst.username } : {}),
...(secret ? (useApiKey ? { apiKey: secret } : { password: secret }) : {}),
...qbAuthConfig({ authKind: settings.authKind, username: inst.username, secret: secret ?? null }),
});
clients.set(inst.id, { stamp, client });
return client;