Files
Multi-Hoster-Upload/lib/hoster-transport-error.js
T
Sucukdeluxe b64cdd0ff3 fix: harden hoster confirmation and recovery
Require every successful upload to expose a validated HTTPS result and rebuild all Doodstream and DSVPlay output from the confirmed file code.

Keep failed baselines distinct from empty accounts, reject stale, foreign, and ambiguous recovery candidates across Doodstream, Byse, VOE, and Vidmoly, and preserve exact filename recovery with normalized extensions.

Emit bounded structured transport diagnostics without raw response bodies or tokenized URLs, and remove sensitive values from Doodstream debug traces.

Tests: node --test tests/upload-confirmation.test.js tests/hosters.test.js tests/doodstream-api-upload.test.js tests/doodstream-upload.test.js tests/byse-reject-recovery.test.js tests/hoster-recovery-provenance.test.js tests/suspect-reject-alternates.test.js

Lint: eslint lib/hoster-transport-error.js lib/hosters.js lib/doodstream-upload.js lib/voe-upload.js lib/vidmoly-upload.js lib/upload-confirmation.js
2026-08-13 20:43:48 +02:00

91 lines
3.7 KiB
JavaScript

function normalizeContentType(value) {
const contentType = String(value || '').trim().slice(0, 120);
const parts = contentType.split(';').map(part => part.trim());
if (parts.length < 1 || parts.length > 2) return null;
const slashIndex = parts[0].indexOf('/');
if (slashIndex <= 0 || slashIndex === parts[0].length - 1) return null;
const tokenCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.+-_';
const validToken = token => Array.from(token).every(char => tokenCharacters.includes(char));
if (!validToken(parts[0].slice(0, slashIndex)) || !validToken(parts[0].slice(slashIndex + 1))) return null;
if (parts.length === 2) {
const charsetPrefix = 'charset=';
if (!parts[1].toLowerCase().startsWith(charsetPrefix)) return null;
const charset = parts[1].slice(charsetPrefix.length);
if (!charset || !validToken(charset)) return null;
}
return contentType;
}
function safeEndpoint(value) {
try {
const url = new URL(String(value || ''));
return `${url.hostname.toLowerCase()}${url.pathname}`;
} catch {
return null;
}
}
function safeEndpointHost(value) {
try {
return new URL(String(value || '')).hostname.toLowerCase();
} catch {
return null;
}
}
function responseKind(body, contentType) {
const text = String(body || '').trim();
if (!text) return 'empty';
const type = String(contentType || '').toLowerCase();
if (type.includes('json') || /^[\[{]/.test(text)) return 'json';
if (type.includes('html') || /<\s*(?:!doctype|html|body|form|input)\b/i.test(text)) return 'html';
return 'text';
}
function summarizeResponse(body, contentType) {
const text = String(body || '');
const kind = responseKind(text, contentType);
return `${kind} response (${Buffer.byteLength(text, 'utf8')} bytes)`;
}
function sanitizeRemoteText(value, limit = 180) {
let text = String(value || '').replace(/[\r\n\t]+/g, ' ').replace(/\s+/g, ' ').trim();
text = text.replace(/https?:\/\/[^\s"'<>]+/gi, (raw) => safeEndpoint(raw) || '[URL]');
text = text.replace(/\b(?:authorization|proxy-authorization|cookie|set-cookie)\s*[:=]\s*[^,]+/gi, '[redacted]');
text = text.replace(/((?:api[_-]?key|token|password|secret|session|sess[_-]?id|csrf)["']?\s*[:=]\s*["']?)[^\s,;"'<>]+/gi, '$1[redacted]');
text = text.replace(/(<(?:input|textarea)[^>]*(?:name|id)=["'][^"']*(?:key|token|password|secret|session|sess|csrf)[^"']*["'][^>]*(?:value=["']))[^"']*(["'])/gi, '$1[redacted]$2');
text = text.replace(/\b[A-Za-z0-9_-]{20,}\b/g, '[redacted]');
return text.slice(0, limit);
}
function createTransportError(message, options = {}) {
const httpStatus = Number(options.httpStatus);
const hasHttpStatus = Number.isInteger(httpStatus) && httpStatus >= 100 && httpStatus <= 599;
const contentType = normalizeContentType(options.contentType);
const endpointHost = safeEndpointHost(options.endpoint);
const kind = responseKind(options.body, contentType);
const suffix = hasHttpStatus ? ` (HTTP ${httpStatus})` : '';
const error = new Error(`${sanitizeRemoteText(message, 220)}${suffix}`);
error.diagnostic = {
phase: String(options.phase || 'transport').slice(0, 80),
http: hasHttpStatus ? httpStatus : null,
contentType,
safeEndpointHost: endpointHost,
responseKind: kind,
retryable: options.retryable === true,
payloadSnippet: summarizeResponse(options.body, contentType)
};
if (options.transientNetwork === true) error.transientNetwork = true;
if (options.hosterTransient === true) error.hosterTransient = true;
if (options.accountError === true) error.accountError = true;
if (options.fileRejected === true) error.fileRejected = true;
return error;
}
module.exports = {
createTransportError,
safeEndpoint,
sanitizeRemoteText,
summarizeResponse
};