diff --git a/lib/account-auth.js b/lib/account-auth.js new file mode 100644 index 0000000..1fd0a4a --- /dev/null +++ b/lib/account-auth.js @@ -0,0 +1,36 @@ +// Decides which credential an upload task should use for a given hoster. +// Extracted from main.js buildTaskFromAccount so the routing can be unit-tested +// without Electron. +// +// DOODSTREAM SPECIAL CASE: prefer the official doodapi.co API key whenever the +// account has one. The web-login path (username/password) drives doodstream's +// browser upload flow, which hands the filecode back inside an XFileSharing +// HTML form. On long/large uploads that form comes back empty (no fn) because a +// per-page-load sess_id token ages out over the multi-minute upload and/or the +// server-side file-registration callback times out — the upload then "succeeds" +// (bytes sent, HTTP 200) but yields no link. The JSON API returns the filecode +// directly in result[0].filecode and authenticates with a persistent api_key, +// so it has no empty-form failure mode for result retrieval. The API path was +// doodstream's ORIGINAL upload path (present since the initial commit); web +// login was added later only as an alternative for keyless accounts — so +// preferring the key here restores the intended primary path, it doesn't fight +// a deliberate choice. Keyless accounts keep using web login unchanged. +function selectUploadAuth(hoster, account) { + if (!account || typeof account !== 'object') return {}; + + if (hoster === 'doodstream.com' && account.apiKey) { + return { apiKey: account.apiKey }; + } + if (account.authType === 'api' && account.apiKey) { + return { apiKey: account.apiKey }; + } + if (account.username && account.password) { + return { username: account.username, password: account.password }; + } + if (account.apiKey) { + return { apiKey: account.apiKey }; + } + return {}; +} + +module.exports = { selectUploadAuth }; diff --git a/lib/hosters.js b/lib/hosters.js index 2b8fe7a..f9f9961 100644 --- a/lib/hosters.js +++ b/lib/hosters.js @@ -34,7 +34,10 @@ const HOSTER_CONFIGS = { 'doodstream.com': { apiBase: 'https://doodapi.co', serverEndpoints: ['/api/upload/server'], - fallbackUploadServers: ['https://tr1128ve.cloudatacdn.com/upload/01'], + // No hardcoded fallback node: that stale CDN host (tr1128ve.cloudatacdn.com) + // accepts the bytes but returns an empty result form with no filecode, so a + // failed server lookup must throw cleanly rather than upload ~1 GB into a + // dead end. (Same reasoning as the web-session path's fail-fast.) buildUploadUrl: (url, key) => appendRawQuery(url, key), formFields: (key) => ({ api_key: key }), parseResult: parseDoodstreamResult diff --git a/main.js b/main.js index 1802794..a34ab83 100644 --- a/main.js +++ b/main.js @@ -8,6 +8,7 @@ const { HOSTER_CONFIGS } = require('./lib/hosters'); const VidmolyUploader = require('./lib/vidmoly-upload'); const VoeUploader = require('./lib/voe-upload'); const DoodstreamUploader = require('./lib/doodstream-upload'); +const { selectUploadAuth } = require('./lib/account-auth'); const ClouddropUploader = require('./lib/clouddrop-upload'); const { checkForUpdate, installUpdate, abortUpdate } = require('./lib/updater'); const backupCrypto = require('./lib/backup-crypto'); @@ -568,15 +569,7 @@ function getNextFallbackAccount(config, hosterName, failedAccountId) { } function buildTaskFromAccount(hoster, account, extra) { - const task = { ...extra, hoster, accountId: account.id }; - if (account.authType === 'api' && account.apiKey) { - task.apiKey = account.apiKey; - } else if (account.username && account.password) { - task.username = account.username; - task.password = account.password; - } else if (account.apiKey) { - task.apiKey = account.apiKey; - } + const task = { ...extra, hoster, accountId: account.id, ...selectUploadAuth(hoster, account) }; return task; } diff --git a/tests/account-auth.test.js b/tests/account-auth.test.js new file mode 100644 index 0000000..1fd5afc --- /dev/null +++ b/tests/account-auth.test.js @@ -0,0 +1,44 @@ +const { test } = require('node:test'); +const assert = require('node:assert'); +const { selectUploadAuth } = require('../lib/account-auth'); + +test('doodstream prefers the API key even when username/password are also set', () => { + const auth = selectUploadAuth('doodstream.com', { + apiKey: 'KEY123', username: 'u', password: 'p' + }); + assert.deepEqual(auth, { apiKey: 'KEY123' }); // API path — no username leaks through +}); + +test('doodstream with only username/password uses web login (keyless fallback)', () => { + const auth = selectUploadAuth('doodstream.com', { username: 'u', password: 'p' }); + assert.deepEqual(auth, { username: 'u', password: 'p' }); +}); + +test('doodstream with empty apiKey + creds falls back to web login (no false API route)', () => { + const auth = selectUploadAuth('doodstream.com', { apiKey: '', username: 'u', password: 'p' }); + assert.deepEqual(auth, { username: 'u', password: 'p' }); +}); + +test('doodstream with nothing usable returns empty', () => { + assert.deepEqual(selectUploadAuth('doodstream.com', { apiKey: '', username: '', password: '' }), {}); +}); + +test('voe.sx is unaffected by the doodstream special-case: username/password wins', () => { + // voe also supports both, but the empty-form bug is doodstream-specific; do + // not change voe routing. + const auth = selectUploadAuth('voe.sx', { apiKey: 'VKEY', username: 'u', password: 'p' }); + assert.deepEqual(auth, { username: 'u', password: 'p' }); +}); + +test('authType=api forces the API key for any hoster', () => { + assert.deepEqual(selectUploadAuth('voe.sx', { authType: 'api', apiKey: 'K', username: 'u', password: 'p' }), { apiKey: 'K' }); +}); + +test('api-key-only account (no creds) uses the key', () => { + assert.deepEqual(selectUploadAuth('byse.sx', { apiKey: 'BKEY' }), { apiKey: 'BKEY' }); +}); + +test('null / non-object account does not throw', () => { + assert.deepEqual(selectUploadAuth('doodstream.com', null), {}); + assert.deepEqual(selectUploadAuth('doodstream.com', undefined), {}); +});