` +
requested + sent;
logBody.prepend(div);
while (logBody.children.length > 120) logBody.lastElementChild.remove();
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
/* ------------------------------------------------------------------ *
* Running a scenario
* ------------------------------------------------------------------ */
async function runScenario(s, resultEl) {
const target = TARGETS[s.target || 'api'];
const params = new URLSearchParams(s.server || {});
// A unique param keeps each run out of the browser's preflight cache, except
// where the scenario is specifically about that cache.
if (!s.stableUrl) params.set('_', Date.now().toString(36));
const url = target.base + (params.toString() ? '?' + params.toString() : '');
const init = {};
if (s.client.method) init.method = s.client.method;
if (s.client.mode) init.mode = s.client.mode;
if (s.client.credentials) init.credentials = s.client.credentials;
if (s.client.headers) init.headers = s.client.headers;
if (s.client.body) init.body = s.client.body;
const seqBefore = lastSeq;
const t0 = performance.now();
const out = { predicted: preflightReason(s.client) };
try {
const res = await fetch(url, init);
out.elapsed = Math.round(performance.now() - t0);
out.outcome = 'ok';
out.status = res.status;
out.ok = res.ok;
out.type = res.type;
out.readable = {};
for (const h of s.client.readHeaders || []) out.readable[h] = res.headers.get(h);
out.visibleHeaders = [...res.headers.keys()];
try { out.body = await res.text(); } catch (e) { out.body = ''; }
} catch (err) {
out.elapsed = Math.round(performance.now() - t0);
out.outcome = 'blocked';
out.error = `${err.name}: ${err.message}`;
}
await sleep(280); // dejar que el log por SSE se ponga al día
out.trace = logEntries.filter((e) => e.seq > seqBefore);
renderResult(s, out, resultEl);
return out;
}
function renderResult(s, out, el) {
const expectedOutcome = s.expect === 'fail' ? 'blocked' : 'ok';
const matched = s.expect === 'any' || out.outcome === expectedOutcome;
const parts = [];
parts.push(
`
` +
`${out.outcome === 'ok' ? 'La respuesta llegó a JavaScript' : 'Bloqueado por el navegador'}` +
(out.status !== undefined ? `HTTP ${out.status}` : '') +
(out.type ? `type: ${out.type}` : '') +
`${out.elapsed} ms` +
(matched ? '' : `no es lo que esperábamos`) +
`
`
);
if (out.error) parts.push(`
${esc(out.error)}\n\nEl navegador se niega a decir más que esto a propósito: una razón detallada ya filtraría información de otro origen. El mensaje real está en la consola de DevTools.
`);
if (out.readable && Object.keys(out.readable).length) {
parts.push(
`
`
);
}
/* what the browser sent, per the server's own log */
const trace = out.trace || [];
const sawPreflight = trace.some((e) => e.phase === 'preflight');
let note = '';
if (out.predicted && !sawPreflight) note = 'Se esperaba un preflight y no está: el navegador reutilizó uno cacheado.';
if (!out.predicted && !sawPreflight && s.client.mode !== 'no-cors') note = 'Request simple: no hace falta preflight.';
parts.push(
`
<img src="${esc(API)}/api/image">
sin headers CORS, y sin mandar header Origin
todavía no cargó
<img crossorigin="anonymous" …>
ahora SÍ es un request CORS, y falla
todavía no cargó
`;
host.querySelector('#imgGo').onclick = () => {
const stamp = Date.now();
const plain = host.querySelector('#imgPlain');
const cors = host.querySelector('#imgCors');
plain.onload = () => (host.querySelector('#imgPlainState').innerHTML = 'cargó — el navegador nunca le pidió permiso a CORS');
plain.onerror = () => (host.querySelector('#imgPlainState').innerHTML = 'falló');
cors.onload = () => (host.querySelector('#imgCorsState').innerHTML = 'cargó');
cors.onerror = () => (host.querySelector('#imgCorsState').innerHTML = 'bloqueada — crossorigin metió a esta imagen dentro de CORS, y el servidor no mandó Allow-Origin');
plain.src = `${API}/api/image?t=${stamp}`;
cors.src = `${API}/api/image?t=${stamp}&nocors=1`;
};
host.classList.add('shown');
return;
}
if (s.special === 'form') {
host.innerHTML = `
La respuesta del servidor se renderiza en ese frame, así que podés ver que ocurrió — pero el JavaScript de esta
página no puede leer ni un byte. La escritura pasó; lo único protegido es la lectura.
`;
const list = window.SCENARIOS.filter((s) => s.group === group.id);
list.forEach((s, i) => {
const card = buildCard(s, `${gi + 1}.${i + 1}`);
section.appendChild(card);
if (card._run) cards.push({ s, card });
});
scenarioHost.appendChild(section);
nav.insertAdjacentHTML('beforeend', `${group.title}`);
});
nav.insertAdjacentHTML('beforeend', `6 · Laboratorio`);
/* ------------------------------------------------------------------ *
* Header: origin chips, cookies, run-all, theme, font size
* ------------------------------------------------------------------ */
document.getElementById('pageOrigin').textContent = PAGE;
document.getElementById('apiOrigin').textContent = API;
document.getElementById('origins').innerHTML =
`página ${PAGE}` +
`API ${API}` +
`otro sitio ${CROSS_SITE}` +
`cookies ?`;
async function refreshCookieChip() {
const chip = document.getElementById('cookieChip');
try {
const res = await fetch(`${API}/api/data?acao=echo&acac=true&_=${Date.now()}`, { credentials: 'include' });
const data = await res.json();
const n = data.seenByServer.cookies.length;
chip.innerHTML = `cookies ${n ? n + ' en la API' : 'ninguna'}`;
chip.className = 'chip ' + (n ? 'live' : 'dead');
} catch (e) {
chip.innerHTML = 'API no responde';
chip.className = 'chip dead';
}
}
document.getElementById('cookieSet').onclick = () => {
window.open(`${API}/api/cookie/set`, '_blank');
setTimeout(refreshCookieChip, 1500);
};
document.getElementById('runAll').onclick = async (ev) => {
const btn = ev.currentTarget;
btn.disabled = true;
let surprises = 0;
for (let i = 0; i < cards.length; i++) {
const { s, card } = cards[i];
btn.textContent = `probando ${i + 1}/${cards.length}…`;
card.scrollIntoView({ block: 'center', behavior: 'smooth' });
const out = await card._run();
const expected = s.expect === 'fail' ? 'blocked' : 'ok';
if (out.outcome !== expected) surprises++;
}
btn.disabled = false;
btn.textContent = 'Probar todo';
alert(
`Se probaron ${cards.length} escenarios.\n` +
`${cards.length - surprises} se comportaron exactamente como dice la ficha.\n` +
(surprises
? `${surprises} no — buscá la etiqueta naranja "no es lo que esperábamos". ` +
`La causa habitual es que todavía no pusiste las cookies, o que el navegador bloquea cookies de terceros.`
: `Ninguna sorpresa. Tu navegador está jugando según el manual.`)
);
};
document.getElementById('clearAll').onclick = () => {
document.querySelectorAll('.result').forEach((r) => {
if (r.querySelector('form') || r.querySelector('img')) return;
r.classList.remove('shown');
r.innerHTML = '';
});
document.querySelectorAll('button.run').forEach((b) => { if (b.textContent === 'Probar de nuevo') b.textContent = 'Probar'; });
};
document.getElementById('logClear').onclick = () => {
logEntries.length = 0;
logBody.innerHTML = '
Limpio.
';
};
const root = document.documentElement;
document.getElementById('theme').onclick = () => {
const current = root.getAttribute('data-theme');
root.setAttribute('data-theme', current === 'dark' ? 'light' : 'dark');
};
let scale = 1;
const setScale = (v) => { scale = Math.min(1.6, Math.max(0.8, v)); root.style.setProperty('--scale', scale); };
document.getElementById('fontUp').onclick = () => setScale(scale + 0.1);
document.getElementById('fontDown').onclick = () => setScale(scale - 0.1);
connectLog();
refreshCookieChip();
/* ------------------------------------------------------------------ *
* The free playground: every knob, wired to the same runner.
* The two preview panels update as you type, so the class can see what
* a toggle changes before anything is sent.
* ------------------------------------------------------------------ */
const pg = document.getElementById('pg');
pg.innerHTML = `
Armá cualquier configuración de CORS
Cambiá lo que quieras: los paneles de abajo se actualizan en vivo. Después probalo y mirá el log del servidor.
El servidor va a mandar
La página va a ejecutar
`;
const P = (id) => document.getElementById('p_' + id);
function parseHeaderLines(text) {
const out = {};
for (const line of text.split('\n')) {
const i = line.indexOf(':');
if (i > 0) out[line.slice(0, i).trim()] = line.slice(i + 1).trim();
}
return Object.keys(out).length ? out : undefined;
}
function playgroundScenario() {
const server = {};
const put = (key, value) => { if (value !== '' && value !== undefined && value !== null) server[key] = String(value); };
put('acao', P('acao').value.trim());
if (P('acac').checked) server.acac = 'true';
put('aceh', P('aceh').value.trim());
if (P('vary').checked) server.vary = '1';
if (P('status').value && P('status').value !== '200') server.status = P('status').value;
if (P('errnocors').checked) server.errnocors = '1';
if (Number(P('delay').value) > 0) server.delay = P('delay').value;
put('acam', P('acam').value.trim());
put('acah', P('acah').value.trim());
put('acma', P('acma').value.trim());
if (P('pfstatus').value && P('pfstatus').value !== '204') server.pfstatus = P('pfstatus').value;
if (P('pfcors').checked) server.pfcors = '0';
if (P('pfacao').checked) server.pfacao = '0';
if (P('pfacac').checked) server.pfacac = '0';
if (P('pfredirect').checked) server.pfredirect = '1';
const client = { method: P('method').value };
if (P('mode').value) client.mode = P('mode').value;
if (P('credentials').value) client.credentials = P('credentials').value;
const headers = parseHeaderLines(P('headers').value);
if (headers) client.headers = headers;
const body = P('body').value.trim();
if (body && !['GET', 'HEAD'].includes(client.method)) client.body = body;
const read = P('read').value.split(',').map((h) => h.trim()).filter(Boolean);
if (read.length) client.readHeaders = read;
return {
id: 'playground',
target: P('target').value,
stableUrl: P('stable').checked,
expect: 'any',
server,
client,
};
}
function refreshPlayground() {
const s = playgroundScenario();
document.getElementById('pgServer').innerHTML = serverPanel(s);
document.getElementById('pgClient').innerHTML = clientPanel(s);
const reason = preflightReason(s.client);
document.getElementById('pgVerdict').innerHTML = reason
? `Este request va a tener preflight — ${esc(reason)}.`
: s.client.mode === 'no-cors'
? `Modo no-cors: sin preflight, y los headers fuera de la safelist se descartan.`
: `Esto es un request simple — sin preflight.`;
}
pg.addEventListener('input', refreshPlayground);
pg.addEventListener('change', refreshPlayground);
refreshPlayground();
document.getElementById('pgRun').onclick = async (ev) => {
const btn = ev.currentTarget;
btn.disabled = true;
btn.textContent = 'probando…';
await runScenario(playgroundScenario(), document.getElementById('pgResult'));
btn.disabled = false;
btn.textContent = 'Probar';
};