Transport policy enforcer

Enforce request policies for fetch() or GM_xmlhttpRequest methods with fallback routing

Este script no debería instalarse directamente. Es una biblioteca que utilizan otros scripts mediante la meta-directiva de inclusión // @require https://update.greasyfork.org/scripts/591898/1906160/Transport%20policy%20enforcer.js

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name           Transport policy enforcer
// @namespace      861ddd094884eac5bea7a3b12e074f34
// @author         Anonymous, Codex
// @version        1.0.0
// @license        BSD-0
// @grant          none
// ==/UserScript==

(function (global) {
    'use strict';

    const TRANSPORTS = ['gm-first', 'fetch-first', 'gm-only', 'fetch-only'];
    const config = {
        transport: 'gm-first',
        fallbackOn: {
            transportError: true,
            statuses: [403, 429, [500, 599]],
            predicate: null,
        },
        gmProvider: () =>
            (typeof GM !== 'undefined' && GM.xmlHttpRequest) ||
            (typeof GM_xmlhttpRequest === 'function' && GM_xmlhttpRequest) ||
            null,
        fetchProvider: () =>
            (typeof window !== 'undefined' && window.fetch)
                ? window.fetch.bind(window)
                : (typeof fetch === 'function' ? fetch : null),
        timeoutMs: 30000,
        onTransportEvent: null,
    };

    function configure(options) {
        return Object.assign(config, options || {});
    }

    function matchesStatus(status, list) {
        if (!Array.isArray(list)) return false;
        const value = Number(status);
        return list.some((entry) => {
            if (Array.isArray(entry)) {
                return entry.length === 2 && value >= Number(entry[0]) && value <= Number(entry[1]);
            }
            return value === Number(entry);
        });
    }

    function emit(event) {
        if (typeof config.onTransportEvent !== 'function') return;
        try { config.onTransportEvent(event); } catch (e) { /* host hook is best-effort */ }
    }

    function asError(value, message) {
        if (value instanceof Error) return value;
        const error = new Error(value && value.message ? value.message : message);
        if (value !== undefined) error.cause = value;
        return error;
    }

    function transportError(value, transport, details, kind) {
        const error = asError(value, `TransportPolicy: ${kind} using ${transport} for ${details.url}`);
        try { error.transport = transport; } catch (e) { /* a frozen provider error is replaced below */ }
        if (error.transport !== transport) {
            const replacement = new Error(error.message);
            replacement.cause = error;
            replacement.transport = transport;
            if (kind === 'timeout') replacement.timedOut = true;
            return replacement;
        }
        if (kind === 'timeout') error.timedOut = true;
        return error;
    }

    function normaliseGmResponse(raw, details) {
        raw = raw || {};
        const responseText = typeof raw.responseText === 'string' ? raw.responseText : '';
        return {
            status: Number(raw.status) || 0,
            statusText: raw.statusText || '',
            responseText,
            response: raw.response !== undefined ? raw.response : responseText,
            responseHeaders: raw.responseHeaders || '',
            finalUrl: raw.finalUrl || details.url,
            transport: 'gm',
            fellBack: false,
        };
    }

    function gmRequest(details, provider) {
        return new Promise((resolve, reject) => {
            const options = Object.assign({}, details, {
                timeout: details.timeout ?? config.timeoutMs,
                onload: (response) => resolve(normaliseGmResponse(response, details)),
                onerror: (error) => reject(transportError(error, 'gm', details, 'network error')),
                ontimeout: (error) => reject(transportError(error, 'gm', details, 'timeout')),
                onabort: (error) => reject(transportError(error, 'gm', details, 'abort')),
            });
            try {
                provider(options);
            } catch (error) {
                reject(transportError(error, 'gm', details, 'provider error'));
            }
        });
    }

    function headerString(headers) {
        let lines = '';
        if (headers && typeof headers.forEach === 'function') {
            headers.forEach((value, name) => { lines += `${name}: ${value}\r\n`; });
        }
        return lines;
    }

    async function fetchRequest(details, provider) {
        const timeout = details.timeout ?? config.timeoutMs;
        const init = {
            method: details.method || 'GET',
            credentials: 'include',
            cache: 'no-store',
            redirect: 'follow',
        };
        if (details.headers) init.headers = Object.assign({}, details.headers);
        if (details.data != null) init.body = details.data;

        let controller;
        let timer;
        if (timeout > 0 && typeof AbortController === 'function') {
            controller = new AbortController();
            init.signal = controller.signal;
            timer = setTimeout(() => controller.abort(), timeout);
        }

        try {
            const raw = await provider(details.url, init);
            const responseType = details.responseType || 'text';
            let response;
            let responseText = '';
            if (responseType === 'json') {
                responseText = await raw.text();
                response = JSON.parse(responseText);
            } else if (responseType === 'arraybuffer') {
                response = await raw.arrayBuffer();
            } else if (responseType === 'blob') {
                response = await raw.blob();
            } else {
                responseText = await raw.text();
                response = responseText;
            }
            return {
                status: Number(raw.status) || 0,
                statusText: raw.statusText || '',
                responseText,
                response,
                responseHeaders: headerString(raw.headers),
                finalUrl: raw.url || details.url,
                transport: 'fetch',
                fellBack: false,
            };
        } catch (error) {
            const timedOut = !!(controller && controller.signal.aborted);
            throw transportError(error, 'fetch', details, timedOut ? 'timeout' : 'transport error');
        } finally {
            if (timer !== undefined) clearTimeout(timer);
        }
    }

    function providerFor(transport) {
        const factory = transport === 'gm' ? config.gmProvider : config.fetchProvider;
        if (typeof factory !== 'function') return null;
        return factory();
    }

    async function attempt(transport, details) {
        emit({ type: 'attempt', transport, url: details.url, method: details.method || 'GET' });
        let provider;
        try {
            provider = providerFor(transport);
        } catch (error) {
            throw transportError(error, transport, details, 'provider error');
        }
        if (typeof provider !== 'function') {
            const error = transportError(undefined, transport, details, 'unavailable');
            error.unavailable = true;
            throw error;
        }
        return transport === 'gm'
            ? gmRequest(details, provider)
            : fetchRequest(details, provider);
    }

    function ordering(policy) {
        switch (policy) {
        case 'gm-first': return ['gm', 'fetch'];
        case 'fetch-first': return ['fetch', 'gm'];
        case 'gm-only': return ['gm'];
        case 'fetch-only': return ['fetch'];
        default: throw new Error(`TransportPolicy: unknown transport policy ${policy}`);
        }
    }

    function fallbackDecision(outcome, rules, transport) {
        if (typeof rules.predicate === 'function') {
            const verdict = rules.predicate(outcome.response, transport);
            if (typeof verdict === 'boolean') {
                return verdict ? { fallback: true, reason: 'predicate' } : { fallback: false };
            }
        }
        if (outcome.error) {
            return rules.transportError
                ? { fallback: true, reason: 'transport-error' }
                : { fallback: false };
        }
        return matchesStatus(outcome.response.status, rules.statuses)
            ? { fallback: true, reason: 'status' }
            : { fallback: false };
    }

    function primarySummary(transport, outcome) {
        if (outcome.error) return { transport, error: outcome.error };
        return { transport, status: outcome.response.status };
    }

    function finalEvent(type, transport, details, value, reason) {
        const event = { type, transport, url: details.url, method: details.method || 'GET' };
        if (value && typeof value.status === 'number') event.status = value.status;
        if (value instanceof Error) event.error = value;
        if (reason) event.reason = reason;
        emit(event);
    }

    async function request(input) {
        const details = input || {};
        const policy = details.transport || config.transport;
        const order = ordering(policy);
        const rules = Object.assign({}, config.fallbackOn || {}, details.fallbackOn || {});
        const { transport: ignoredTransport, fallbackOn: ignoredFallback, ...networkDetails } = details;
        const primaryTransport = order[0];
        let primary;

        try {
            primary = { response: await attempt(primaryTransport, networkDetails) };
        } catch (error) {
            primary = { error };
        }

        if (order.length === 1) {
            if (primary.error) {
                finalEvent('rejected', primaryTransport, networkDetails, primary.error,
                    primary.error.unavailable ? 'unavailable' : 'transport-error');
                throw primary.error;
            }
            finalEvent('resolved', primaryTransport, networkDetails, primary.response);
            return primary.response;
        }

        let decision;
        if (primary.error && primary.error.unavailable) {
            decision = { fallback: true, reason: 'unavailable' };
        } else {
            try {
                decision = fallbackDecision(primary, rules, primaryTransport);
            } catch (error) {
                const rejection = transportError(error, primaryTransport, networkDetails, 'predicate error');
                finalEvent('rejected', primaryTransport, networkDetails, rejection);
                throw rejection;
            }
        }

        if (!decision.fallback) {
            if (primary.error) {
                finalEvent('rejected', primaryTransport, networkDetails, primary.error, 'transport-error');
                throw primary.error;
            }
            finalEvent('resolved', primaryTransport, networkDetails, primary.response);
            return primary.response;
        }

        const fallbackEvent = {
            type: 'fallback',
            transport: primaryTransport,
            url: networkDetails.url,
            method: networkDetails.method || 'GET',
            reason: decision.reason,
        };
        if (primary.response) fallbackEvent.status = primary.response.status;
        if (primary.error) fallbackEvent.error = primary.error;
        emit(fallbackEvent);

        const secondaryTransport = order[1];
        try {
            const response = await attempt(secondaryTransport, networkDetails);
            response.fellBack = true;
            response.primary = primarySummary(primaryTransport, primary);
            finalEvent('resolved', secondaryTransport, networkDetails, response);
            return response;
        } catch (error) {
            error.transport = secondaryTransport;
            if (primary.error) error.primaryError = primary.error;
            else error.primaryResponse = primary.response;
            finalEvent('rejected', secondaryTransport, networkDetails, error,
                error.unavailable ? 'unavailable' : 'transport-error');
            throw error;
        }
    }

    function xmlHttpRequest(details) {
        const { onload, onerror, ontimeout, ...requestDetails } = details;
        request(requestDetails).then(
            (response) => { if (typeof onload === 'function') onload(response); },
            (error) => {
                if (error.timedOut && typeof ontimeout === 'function') ontimeout(error);
                else if (typeof onerror === 'function') onerror(error);
            }
        );
        return { abort() {} };
    }

    global.TransportPolicy = {
        configure,
        config,
        request,
        xmlHttpRequest,
        TRANSPORTS,
        matchesStatus,
    };
}(typeof globalThis !== 'undefined' ? globalThis : this));