292 lines
15 KiB
HTML
292 lines
15 KiB
HTML
<!doctype html>
|
||
<html lang="es">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Laboratorio de CORS</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f6f7f9;
|
||
--panel: #ffffff;
|
||
--panel-2: #f1f3f6;
|
||
--ink: #14181f;
|
||
--ink-2: #4a5462;
|
||
--ink-3: #78828f;
|
||
--line: #dde1e7;
|
||
--accent: #2563eb;
|
||
--ok: #157f4a;
|
||
--ok-bg: #e6f5ec;
|
||
--bad: #c0281f;
|
||
--bad-bg: #fdecea;
|
||
--warn: #8a5a00;
|
||
--warn-bg: #fdf3e0;
|
||
--code-bg: #f4f6f9;
|
||
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
||
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||
--radius: 10px;
|
||
--scale: 1;
|
||
}
|
||
@media (prefers-color-scheme: dark) {
|
||
:root:not([data-theme="light"]) {
|
||
--bg: #0e1116;
|
||
--panel: #161b22;
|
||
--panel-2: #1c222b;
|
||
--ink: #e8ecf1;
|
||
--ink-2: #a8b3c0;
|
||
--ink-3: #7d8896;
|
||
--line: #2a313b;
|
||
--accent: #6a9bff;
|
||
--ok: #62d09a;
|
||
--ok-bg: #12291e;
|
||
--bad: #ff8c80;
|
||
--bad-bg: #2d1715;
|
||
--warn: #ffc76b;
|
||
--warn-bg: #2c2210;
|
||
--code-bg: #10151b;
|
||
}
|
||
}
|
||
:root[data-theme="dark"] {
|
||
--bg: #0e1116; --panel: #161b22; --panel-2: #1c222b; --ink: #e8ecf1;
|
||
--ink-2: #a8b3c0; --ink-3: #7d8896; --line: #2a313b; --accent: #6a9bff;
|
||
--ok: #62d09a; --ok-bg: #12291e; --bad: #ff8c80; --bad-bg: #2d1715;
|
||
--warn: #ffc76b; --warn-bg: #2c2210; --code-bg: #10151b;
|
||
}
|
||
|
||
* { box-sizing: border-box; }
|
||
html { font-size: calc(16px * var(--scale)); }
|
||
body {
|
||
margin: 0; background: var(--bg); color: var(--ink);
|
||
font-family: var(--sans); line-height: 1.55;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
code, pre, .mono { font-family: var(--mono); }
|
||
a { color: var(--accent); }
|
||
|
||
/* ---------- header ---------- */
|
||
header.top {
|
||
position: sticky; top: 0; z-index: 30;
|
||
background: var(--panel); border-bottom: 1px solid var(--line);
|
||
padding: 0.7rem 1rem;
|
||
}
|
||
.top-row { display: flex; flex-wrap: wrap; gap: 0.6rem 1rem; align-items: center; }
|
||
.brand { font-weight: 650; font-size: 1.05rem; letter-spacing: -0.01em; margin-right: auto; }
|
||
.brand span { color: var(--ink-3); font-weight: 400; font-size: 0.85rem; margin-left: 0.5rem; }
|
||
.origins { display: flex; flex-wrap: wrap; gap: 0.4rem; font-size: 0.75rem; }
|
||
.chip {
|
||
font-family: var(--mono); background: var(--panel-2); border: 1px solid var(--line);
|
||
padding: 0.15rem 0.45rem; border-radius: 6px; white-space: nowrap;
|
||
}
|
||
.chip b { color: var(--ink-3); font-weight: 500; }
|
||
.chip.live { border-color: var(--ok); color: var(--ok); }
|
||
.chip.dead { border-color: var(--bad); color: var(--bad); }
|
||
button, select, input, textarea {
|
||
font: inherit; font-size: 0.85rem; color: var(--ink);
|
||
background: var(--panel); border: 1px solid var(--line);
|
||
border-radius: 7px; padding: 0.3rem 0.6rem;
|
||
}
|
||
button { cursor: pointer; }
|
||
button:hover { border-color: var(--accent); }
|
||
button.primary { background: var(--accent); color: #fff; border-color: transparent; font-weight: 550; }
|
||
button.primary:hover { filter: brightness(1.08); }
|
||
button.run { font-weight: 550; }
|
||
.toolbar { display: flex; gap: 0.4rem; align-items: center; flex-wrap: wrap; }
|
||
nav.groups { display: flex; gap: 0.35rem; flex-wrap: wrap; margin-top: 0.6rem; font-size: 0.8rem; }
|
||
nav.groups a { text-decoration: none; padding: 0.2rem 0.55rem; border-radius: 6px; background: var(--panel-2); color: var(--ink-2); }
|
||
nav.groups a:hover { color: var(--accent); }
|
||
|
||
/* ---------- layout ---------- */
|
||
.wrap { display: grid; grid-template-columns: minmax(0,1fr) 25rem; gap: 1.2rem; padding: 1.2rem 1rem 6rem; max-width: 96rem; margin: 0 auto; }
|
||
@media (max-width: 1100px) { .wrap { grid-template-columns: minmax(0,1fr); } aside.log { position: static; max-height: 22rem; } }
|
||
|
||
/* ---------- intro ---------- */
|
||
.intro { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 1.2rem; }
|
||
.intro h1 { margin: 0 0 0.4rem; font-size: 1.25rem; letter-spacing: -0.02em; }
|
||
.intro p { margin: 0.4rem 0; color: var(--ink-2); font-size: 0.92rem; max-width: 62ch; }
|
||
.intro ol { margin: 0.5rem 0 0; padding-left: 1.3rem; color: var(--ink-2); font-size: 0.92rem; }
|
||
|
||
/* ---------- groups & cards ---------- */
|
||
section.group { margin-bottom: 2rem; scroll-margin-top: 7rem; }
|
||
section.group > h2 { font-size: 1.1rem; margin: 0 0 0.15rem; letter-spacing: -0.015em; }
|
||
section.group > p.blurb { margin: 0 0 0.9rem; color: var(--ink-3); font-size: 0.88rem; }
|
||
|
||
article.card {
|
||
background: var(--panel); border: 1px solid var(--line); border-left: 3px solid var(--line);
|
||
border-radius: var(--radius); padding: 0.9rem 1rem; margin-bottom: 0.8rem;
|
||
}
|
||
article.card[data-expect="pass"] { border-left-color: var(--ok); }
|
||
article.card[data-expect="fail"] { border-left-color: var(--bad); }
|
||
article.card[data-expect="partial"] { border-left-color: var(--warn); }
|
||
article.card[data-expect="special"] { border-left-color: var(--accent); }
|
||
|
||
.card-head { display: flex; gap: 0.6rem; align-items: baseline; flex-wrap: wrap; }
|
||
.card-head h3 { margin: 0; font-size: 1rem; font-weight: 600; letter-spacing: -0.01em; }
|
||
.num { font-family: var(--mono); font-size: 0.78rem; color: var(--ink-3); }
|
||
.badge {
|
||
font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 650;
|
||
padding: 0.1rem 0.45rem; border-radius: 5px; white-space: nowrap;
|
||
}
|
||
.badge.pass { background: var(--ok-bg); color: var(--ok); }
|
||
.badge.fail { background: var(--bad-bg); color: var(--bad); }
|
||
.badge.partial { background: var(--warn-bg); color: var(--warn); }
|
||
.badge.special { background: var(--panel-2); color: var(--accent); }
|
||
.badge.neutral { background: var(--panel-2); color: var(--ink-2); }
|
||
|
||
.why { margin: 0.5rem 0 0.7rem; font-size: 0.9rem; color: var(--ink-2); max-width: 78ch; }
|
||
.why code { background: var(--code-bg); padding: 0.05em 0.3em; border-radius: 4px; font-size: 0.86em; }
|
||
.why strong { color: var(--ink); }
|
||
|
||
.cols { display: grid; grid-template-columns: 1fr 1fr; gap: 0.6rem; }
|
||
@media (max-width: 720px) { .cols { grid-template-columns: 1fr; } }
|
||
.panel { background: var(--code-bg); border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
|
||
.panel h4 {
|
||
margin: 0; padding: 0.35rem 0.6rem; font-size: 0.68rem; font-weight: 650;
|
||
text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3);
|
||
border-bottom: 1px solid var(--line); background: var(--panel-2);
|
||
}
|
||
.panel pre { margin: 0; padding: 0.55rem 0.6rem; font-size: 0.775rem; line-height: 1.5; overflow-x: auto; white-space: pre; }
|
||
.panel pre .k { color: var(--accent); }
|
||
.panel pre .c { color: var(--ink-3); }
|
||
.panel pre .s { color: var(--ok); }
|
||
.panel pre .hl { background: var(--warn-bg); border-radius: 3px; }
|
||
|
||
.actions { display: flex; gap: 0.6rem; align-items: center; margin-top: 0.7rem; flex-wrap: wrap; }
|
||
.watch { font-size: 0.8rem; color: var(--ink-3); }
|
||
.watch b { color: var(--ink-2); font-weight: 600; }
|
||
|
||
/* ---------- results ---------- */
|
||
.result { margin-top: 0.7rem; display: none; }
|
||
.result.shown { display: block; }
|
||
.verdict { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||
.verdict .big { font-weight: 650; }
|
||
.verdict .big.ok { color: var(--ok); }
|
||
.verdict .big.blocked { color: var(--bad); }
|
||
.meta { color: var(--ink-3); font-size: 0.78rem; font-family: var(--mono); }
|
||
.surprise { background: var(--warn-bg); color: var(--warn); border-radius: 5px; padding: 0.1rem 0.4rem; font-size: 0.72rem; font-weight: 650; }
|
||
.errbox { background: var(--bad-bg); color: var(--bad); border-radius: 7px; padding: 0.5rem 0.65rem; font-family: var(--mono); font-size: 0.78rem; white-space: pre-wrap; }
|
||
.kv { font-family: var(--mono); font-size: 0.78rem; }
|
||
.kv div { padding: 0.1rem 0; }
|
||
.kv .null { color: var(--bad); }
|
||
.kv .val { color: var(--ok); }
|
||
details.more { margin-top: 0.5rem; }
|
||
details.more > summary { cursor: pointer; font-size: 0.8rem; color: var(--ink-3); }
|
||
details.more > summary:hover { color: var(--accent); }
|
||
details.more pre { background: var(--code-bg); border: 1px solid var(--line); border-radius: 7px; padding: 0.5rem 0.6rem; font-size: 0.75rem; overflow-x: auto; margin: 0.4rem 0 0; }
|
||
.trace { margin-top: 0.5rem; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }
|
||
.trace-row { display: flex; gap: 0.5rem; padding: 0.3rem 0.55rem; font-family: var(--mono); font-size: 0.76rem; border-top: 1px solid var(--line); align-items: baseline; }
|
||
.trace-row:first-child { border-top: 0; }
|
||
.trace-row.pre { background: var(--warn-bg); }
|
||
.trace-row .m { font-weight: 650; min-width: 4.8rem; }
|
||
.trace-row .st { margin-left: auto; color: var(--ink-3); }
|
||
.trace-empty { padding: 0.4rem 0.55rem; font-size: 0.78rem; color: var(--ink-3); }
|
||
|
||
/* ---------- server log ---------- */
|
||
aside.log { position: sticky; top: 6.5rem; align-self: start; max-height: calc(100vh - 8rem); display: flex; flex-direction: column;
|
||
background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); overflow: hidden; }
|
||
aside.log header { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.7rem; border-bottom: 1px solid var(--line); background: var(--panel-2); }
|
||
aside.log header h2 { margin: 0; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3); margin-right: auto; }
|
||
.logbody { overflow-y: auto; padding: 0.3rem; font-family: var(--mono); font-size: 0.74rem; }
|
||
.entry { padding: 0.35rem 0.45rem; border-radius: 6px; margin-bottom: 0.25rem; background: var(--panel-2); border-left: 2px solid var(--line); }
|
||
.entry.preflight { border-left-color: var(--warn); }
|
||
.entry.actual { border-left-color: var(--accent); }
|
||
.entry .line1 { display: flex; gap: 0.4rem; align-items: baseline; }
|
||
.entry .line1 .m { font-weight: 650; }
|
||
.entry .line1 .s { margin-left: auto; }
|
||
.entry .s.s2 { color: var(--ok); } .entry .s.s3 { color: var(--warn); }
|
||
.entry .s.s4, .entry .s.s5 { color: var(--bad); }
|
||
.entry .sub { color: var(--ink-3); word-break: break-all; }
|
||
.entry .sent { color: var(--ok); }
|
||
.entry .none { color: var(--bad); }
|
||
.hint { padding: 0.6rem; color: var(--ink-3); font-size: 0.78rem; font-family: var(--sans); }
|
||
|
||
/* ---------- special demos ---------- */
|
||
.demo-row { display: flex; gap: 1rem; flex-wrap: wrap; align-items: flex-start; margin-top: 0.6rem; }
|
||
.demo-box { border: 1px dashed var(--line); border-radius: 8px; padding: 0.6rem; font-size: 0.78rem; color: var(--ink-3); }
|
||
.demo-box img { display: block; max-width: 220px; height: 60px; }
|
||
iframe.sink { width: 100%; height: 6.5rem; border: 1px solid var(--line); border-radius: 7px; background: var(--panel-2); margin-top: 0.5rem; }
|
||
|
||
/* ---------- playground ---------- */
|
||
.pg { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 1rem; }
|
||
.pg h2 { margin: 0 0 0.2rem; font-size: 1.1rem; }
|
||
.pg > p { margin: 0 0 0.9rem; color: var(--ink-3); font-size: 0.88rem; }
|
||
.pg-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); gap: 0.9rem; }
|
||
fieldset { border: 1px solid var(--line); border-radius: 8px; padding: 0.6rem 0.7rem; margin: 0; }
|
||
legend { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.07em; color: var(--ink-3); font-weight: 650; padding: 0 0.3rem; }
|
||
.field { display: grid; grid-template-columns: 8.5rem 1fr; gap: 0.4rem; align-items: center; margin-bottom: 0.35rem; font-size: 0.8rem; }
|
||
.field label { color: var(--ink-2); font-family: var(--mono); font-size: 0.75rem; }
|
||
.field input[type="text"], .field input[type="number"], .field select, .field textarea { width: 100%; }
|
||
.field textarea { min-height: 3.4rem; font-family: var(--mono); font-size: 0.75rem; resize: vertical; }
|
||
.field.check { grid-template-columns: 8.5rem 1fr; }
|
||
.field.check input { justify-self: start; }
|
||
.pg-actions { margin-top: 0.9rem; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
|
||
.footnote { color: var(--ink-3); font-size: 0.78rem; margin-top: 1.2rem; max-width: 70ch; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="top">
|
||
<div class="top-row">
|
||
<div class="brand">Laboratorio de CORS <span>todas las configuraciones, una sola página</span></div>
|
||
<div class="origins" id="origins"></div>
|
||
<div class="toolbar">
|
||
<button id="cookieSet" title="Abre :3001 en una pestaña nueva y deja las cookies ahí">Poner cookies en la API</button>
|
||
<button id="runAll" class="primary">Probar todo</button>
|
||
<button id="clearAll">Limpiar</button>
|
||
<button id="fontDown" title="Más chico">A−</button>
|
||
<button id="fontUp" title="Más grande">A+</button>
|
||
<button id="theme" title="Cambiar tema">◐</button>
|
||
</div>
|
||
</div>
|
||
<nav class="groups" id="nav"></nav>
|
||
</header>
|
||
|
||
<div class="wrap">
|
||
<main>
|
||
<div class="intro">
|
||
<h1>Qué es esta página</h1>
|
||
<p>
|
||
Esta página se sirve desde <code id="pageOrigin"></code>. Hace requests a una API en
|
||
<code id="apiOrigin"></code> — un <strong>origen distinto</strong>, así que todos ellos pasan por CORS.
|
||
La API reconstruye sus headers CORS a partir de los query params en cada request, de modo que cada escenario
|
||
de abajo reconfigura el servidor al vuelo. No hace falta reiniciar nada.
|
||
</p>
|
||
<p>
|
||
El panel de la derecha es el <strong>log del propio servidor</strong>, en vivo. Es lo más útil de todo esto:
|
||
muestra qué recibió y qué respondió el servidor realmente, al lado de lo que el navegador estuvo dispuesto a
|
||
contarle a JavaScript. Esas dos cosas difieren mucho más seguido de lo que uno esperaría.
|
||
</p>
|
||
<ol>
|
||
<li>Abrí DevTools → Network y dejalo a la vista. Todas las respuestas bloqueadas están ahí.</li>
|
||
<li>Hacé clic una vez en <b>Poner cookies en la API</b>, así los escenarios de credenciales tienen algo para mandar.</li>
|
||
<li>Recorré los grupos de arriba abajo, o tocá <b>Probar todo</b> y comentá el resumen.</li>
|
||
</ol>
|
||
</div>
|
||
<div id="scenarios"></div>
|
||
|
||
<section class="group" id="g-playground">
|
||
<h2>6 · Laboratorio libre</h2>
|
||
<p class="blurb">Todas las perillas, para improvisar cuando alguien pregunta "¿y si…?".</p>
|
||
<div class="pg" id="pg"></div>
|
||
</section>
|
||
|
||
<p class="footnote">
|
||
El código del servidor está en <code>server.js</code>: unas 330 líneas de Node puro, sin dependencias, escritas para
|
||
leerse en voz alta. La lista de escenarios son datos en <code>public/scenarios.js</code>, así que podés agregar los tuyos.
|
||
</p>
|
||
</main>
|
||
|
||
<aside class="log">
|
||
<header>
|
||
<h2>Log del servidor</h2>
|
||
<span class="chip" id="logStatus">conectando…</span>
|
||
<button id="logClear">Limpiar</button>
|
||
</header>
|
||
<div class="logbody" id="logBody">
|
||
<div class="hint">Acá aparece cada request que recibe la API — incluidos los preflights que el navegador mandó por su cuenta, y los requests cuyas respuestas el navegador se negó a darle a JavaScript.</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<script src="scenarios.js"></script>
|
||
<script src="app.js"></script>
|
||
</body>
|
||
</html>
|