@luanpdd/kit-mcp 1.12.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1175,11 +1175,51 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
1175
1175
 
1176
1176
  <script>
1177
1177
  /* ──────────────────────────────────────────────────────────
1178
- kit-mcp sidecar — prototype
1179
- This is a faithful mock of what the production HTML will do.
1180
- In production, replace the MockSource with a real EventSource('/events').
1178
+ kit-mcp sidecar — production client
1179
+ SEC-14-01 (Phase 82): All event-derived content rendered via escapeHtml()
1180
+ before insertion into innerHTML. CSP without 'unsafe-inline' in script-src
1181
+ is primary defense; escapeHtml() is defense-in-depth.
1182
+ When adding new innerHTML sites, always escape dynamic fields.
1181
1183
  ────────────────────────────────────────────────────────── */
1182
1184
 
1185
+ /* ──────────────────────────────────────────────────────────
1186
+ SEC-14-02 (Phase 82 / Plan 02): auth token from URL query param.
1187
+ Server (Plan 01) requires Bearer token on /publish, /shutdown, /state
1188
+ and ?t= on /events. This block extracts ?t= once at boot, scrubs it
1189
+ from the address bar (so it doesn't leak via screen-share / browser
1190
+ history copy-paste), and exposes helpers for fetch + EventSource.
1191
+ Variable scoped to closure (not sessionStorage) — reload re-handshakes.
1192
+ ────────────────────────────────────────────────────────── */
1193
+ const __sidecarToken = (() => {
1194
+ try {
1195
+ const params = new URLSearchParams(window.location.search);
1196
+ const t = params.get("t");
1197
+ if (t && /^[0-9a-f]{64}$/.test(t)) {
1198
+ // Scrub from URL so re-share / screenshot doesn't leak it.
1199
+ params.delete("t");
1200
+ const newSearch = params.toString();
1201
+ const newUrl = window.location.pathname + (newSearch ? "?" + newSearch : "") + window.location.hash;
1202
+ window.history.replaceState(null, "", newUrl);
1203
+ return t;
1204
+ }
1205
+ } catch (_) { /* fall through */ }
1206
+ return null;
1207
+ })();
1208
+
1209
+ function authedFetch(input, init = {}) {
1210
+ const opts = { ...init, headers: { ...(init.headers || {}) } };
1211
+ if (__sidecarToken) {
1212
+ opts.headers["Authorization"] = "Bearer " + __sidecarToken;
1213
+ }
1214
+ return fetch(input, opts);
1215
+ }
1216
+
1217
+ function authedEventSourceUrl(path) {
1218
+ if (!__sidecarToken) return path;
1219
+ const sep = path.includes("?") ? "&" : "?";
1220
+ return path + sep + "t=" + encodeURIComponent(__sidecarToken);
1221
+ }
1222
+
1183
1223
  /* ---------- humanize helpers (preserved API) ---------- */
1184
1224
  const TYPE_LABELS = {
1185
1225
  "run.start": "INICIADO",
@@ -1433,16 +1473,21 @@ function historyRowHtml(h) {
1433
1473
  return `<div class="hist-detail-row"><span class="pct">${escapeHtml(pct)}</span><span class="lbl">${escapeHtml(lbl)}</span>${tk ? `<span class="tok">${tk}t</span>` : ""}</div>`;
1434
1474
  })
1435
1475
  .join("");
1476
+ // SEC-14-01: every dynamic field escaped before injection into the
1477
+ // history-row template. status/statusGlyph/dur/when/tokens are computed
1478
+ // locally (string enum or pre-built HTML literal) so are safe by construction;
1479
+ // h.runId and h.eventsCount cross the publisher boundary and MUST be escaped
1480
+ // even though they should normally be benign — defense in depth.
1436
1481
  return `
1437
- <div class="hist-row" data-status="${status}" data-runid="${h.runId}">
1482
+ <div class="hist-row" data-status="${escapeHtml(status)}" data-runid="${escapeHtml(h.runId)}">
1438
1483
  <div class="hist-status">${statusGlyph}</div>
1439
1484
  <div class="hist-title">${escapeHtml(title)}</div>
1440
1485
  <div class="hist-when">${when}</div>
1441
1486
  <div class="hist-meta-row">
1442
1487
  <span><span class="num">${dur}</span> dur</span>
1443
1488
  <span>${tokens}</span>
1444
- <span><span class="num">${h.eventsCount || 0}</span> eventos</span>
1445
- <span class="num">id ${h.runId.slice(0,8)}</span>
1489
+ <span><span class="num">${escapeHtml(String(h.eventsCount || 0))}</span> eventos</span>
1490
+ <span class="num">id ${escapeHtml(String(h.runId).slice(0,8))}</span>
1446
1491
  </div>
1447
1492
  <div class="hist-detail">${detailRows || '<div class="hist-detail-row"><span class="lbl">(sem progresso registrado)</span></div>'}</div>
1448
1493
  </div>
@@ -1534,10 +1579,14 @@ function activeCardHtml(run) {
1534
1579
  const longRunning = elapsed > 30_000;
1535
1580
  const stepCount = run.total ? `${run.current}/${run.total}` : "";
1536
1581
  const showTokens = run.tokens > 0;
1582
+ // SEC-14-01: family/iconHref/title/stepLabel/stepCount/percent are computed
1583
+ // locally; runId crosses the publisher boundary and is escaped before any
1584
+ // injection (data-attribute or text). Defense in depth: even string enums
1585
+ // (family) get escaped to ensure regression resistance.
1537
1586
  return `
1538
- <article class="run-card" data-runid="${run.runId}">
1587
+ <article class="run-card" data-runid="${escapeHtml(run.runId)}">
1539
1588
  <div class="rc-head">
1540
- <div class="rc-icon" data-tool="${family}"><svg><use href="${iconHref}"/></svg></div>
1589
+ <div class="rc-icon" data-tool="${escapeHtml(family)}"><svg><use href="${escapeHtml(iconHref)}"/></svg></div>
1541
1590
  <div class="rc-title-block">
1542
1591
  <div class="rc-tool">${escapeHtml(safeStr(run.tool) || "processo")}</div>
1543
1592
  <div class="rc-title">${escapeHtml(title)}</div>
@@ -1556,7 +1605,7 @@ function activeCardHtml(run) {
1556
1605
  <div class="rc-step">
1557
1606
  <span class="glyph"><svg><use href="#i-spin"/></svg></span>
1558
1607
  <span class="rc-step-text">${escapeHtml(stepLabel)}</span>
1559
- ${stepCount ? `<span class="rc-step-count">${stepCount}</span>` : ""}
1608
+ ${stepCount ? `<span class="rc-step-count">${escapeHtml(stepCount)}</span>` : ""}
1560
1609
  </div>
1561
1610
 
1562
1611
  <div class="rc-tokens" ${showTokens ? "" : "style=\"display:none\""}>
@@ -1565,7 +1614,7 @@ function activeCardHtml(run) {
1565
1614
  </div>
1566
1615
 
1567
1616
  <div class="rc-foot">
1568
- <span class="rc-runid">id ${run.runId.slice(0, 8)}</span>
1617
+ <span class="rc-runid">id ${escapeHtml(String(run.runId).slice(0, 8))}</span>
1569
1618
  <span class="sep">·</span>
1570
1619
  <span>${escapeHtml(safeStr(run.tool) || "")}</span>
1571
1620
  </div>
@@ -1708,8 +1757,11 @@ function rowHtml(evt, idx, prev) {
1708
1757
  msg = `<span class="ident">${escapeHtml(badge.toLowerCase())}</span>`;
1709
1758
  }
1710
1759
  const sourcePill = renderSourcePill(evt.payload?.source);
1760
+ // SEC-14-01: evt.type and evt.runId cross publisher boundary → escape.
1761
+ // msg/tokenChip/sourcePill are pre-built HTML literals where each interpolation
1762
+ // already used escapeHtml() — safe by construction.
1711
1763
  return `
1712
- <div class="tl-row" data-type="${evt.type}" data-ok="${ok}" data-grouped="${grouped}">
1764
+ <div class="tl-row" data-type="${escapeHtml(evt.type)}" data-ok="${ok}" data-grouped="${grouped}">
1713
1765
  <div class="tl-time" title="${time}">${rel}</div>
1714
1766
  <div class="tl-rail"><div class="tl-node"></div></div>
1715
1767
  <div class="tl-content">
@@ -1717,7 +1769,7 @@ function rowHtml(evt, idx, prev) {
1717
1769
  <span class="tl-msg">${msg}</span>
1718
1770
  ${tokenChip}
1719
1771
  ${sourcePill}
1720
- ${evt.runId ? `<span class="tl-runid">${evt.runId.slice(0,6)}</span>` : ""}
1772
+ ${evt.runId ? `<span class="tl-runid">${escapeHtml(String(evt.runId).slice(0,6))}</span>` : ""}
1721
1773
  </div>
1722
1774
  </div>
1723
1775
  `;
@@ -1917,7 +1969,7 @@ function applyConnState(s) {
1917
1969
 
1918
1970
  async function hydrateFromState() {
1919
1971
  try {
1920
- const res = await fetch("/state", { credentials: "omit" });
1972
+ const res = await authedFetch("/state", { credentials: "omit" });
1921
1973
  if (!res.ok) return;
1922
1974
  const j = await res.json();
1923
1975
  if (j.port && document.querySelector(".brand-sub")) {
@@ -1938,7 +1990,7 @@ function connectRealSource() {
1938
1990
  applyConnState("connecting");
1939
1991
  if (evtSource) try { evtSource.close(); } catch (_) {}
1940
1992
 
1941
- evtSource = new EventSource("/events");
1993
+ evtSource = new EventSource(authedEventSourceUrl("/events"));
1942
1994
 
1943
1995
  evtSource.addEventListener("open", () => {
1944
1996
  lastConnectedAt = Date.now();