From fc3dc501a6f0e4f238a230b0b30148490a65171a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 04:59:57 +0000 Subject: [PATCH] Sync generated provider output --- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .agents/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .claude/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .cursor/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .gemini/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .github/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .grok/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .kiro/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .opencode/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .pi/skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .pi/skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .pi/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .qoder/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .rovodev/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .trae-cn/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .trae/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- .vibe/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- .../skills/impeccable/scripts/live-inject.mjs | 48 +++++++++++++------ .../skills/impeccable/scripts/live-server.mjs | 43 ++++++++++++++++- plugin/skills/impeccable/scripts/live.mjs | 6 ++- .../scripts/live/sveltekit-adapter.mjs | 14 +++--- 60 files changed, 1320 insertions(+), 345 deletions(-) diff --git a/.agents/skills/impeccable/scripts/live-inject.mjs b/.agents/skills/impeccable/scripts/live-inject.mjs index d99c37fd5..a0ab8bee2 100644 --- a/.agents/skills/impeccable/scripts/live-inject.mjs +++ b/.agents/skills/impeccable/scripts/live-inject.mjs @@ -8,9 +8,14 @@ * with zero LLM involvement. * * Usage: - * node live-inject.mjs --port PORT # Insert the live script tag - * node live-inject.mjs --remove # Remove the live script tag - * node live-inject.mjs --check # Check whether live config exists + * node live-inject.mjs --port PORT [--token TOKEN] # Insert the live script tag + * node live-inject.mjs --remove # Remove the live script tag + * node live-inject.mjs --check # Check whether live config exists + * + * When --token is supplied, it is appended to the /live.js src as `?token=...` + * so the server's token-gated /live.js handler will serve the bundle. Omitting + * the token yields a bare `/live.js` src (legacy behavior; the server returns + * 401 for it under the current gate). */ import fs from 'node:fs'; @@ -162,18 +167,22 @@ Output (JSON): console.error(JSON.stringify({ ok: false, error: 'missing_port' })); process.exit(1); } + // Optional server token: appended to the /live.js src so the token-gated + // /live.js handler authorizes the browser fetch. `live.mjs` always passes it. + const tokenIdx = args.indexOf('--token'); + const token = tokenIdx !== -1 ? args[tokenIdx + 1] : undefined; const gitIgnore = ensureLiveGitIgnores( process.cwd(), nuxt ? [nuxt.pluginFile] : [], ); if (svelteKit) { - const adapterResult = applySvelteKitLiveAdapter({ cwd: process.cwd(), port, config }); + const adapterResult = applySvelteKitLiveAdapter({ cwd: process.cwd(), port, token, config }); console.log(JSON.stringify({ ok: true, port, adapter: 'sveltekit', gitIgnore, results: [adapterResult] })); return; } if (nuxt) { - const adapterResult = applyNuxtLiveAdapter({ cwd: process.cwd(), port, project: nuxt }); + const adapterResult = applyNuxtLiveAdapter({ cwd: process.cwd(), port, token, project: nuxt }); console.log(JSON.stringify({ ok: !adapterResult.error, port, @@ -190,7 +199,7 @@ Output (JSON): if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' }; const content = fs.readFileSync(absFile, 'utf-8'); const withoutOld = revertCspMeta(removeTag(content, config.commentSyntax)); - const withTag = insertTag(withoutOld, config, port, relFile); + const withTag = insertTag(withoutOld, config, port, relFile, token); if (withTag === withoutOld) { return { file: relFile, error: 'insertion_point_not_found', anchor: config.insertBefore || config.insertAfter }; } @@ -276,9 +285,9 @@ export function detectNuxtProject(cwd = process.cwd()) { return { configFile, appDir, pluginFile }; } -export function buildNuxtPlugin(port) { +export function buildNuxtPlugin(port, token) { return `/* ${NUXT_PLUGIN_MARKER} */ -const liveSrc = 'http://localhost:${port}/live.js'; +const liveSrc = '${buildLiveScriptSrc(port, token)}'; const liveSelector = 'script[data-impeccable-live-nuxt]'; export default defineNuxtPlugin(() => { @@ -303,7 +312,7 @@ export default defineNuxtPlugin(() => { `; } -export function applyNuxtLiveAdapter({ cwd = process.cwd(), port, project = detectNuxtProject(cwd) }) { +export function applyNuxtLiveAdapter({ cwd = process.cwd(), port, token, project = detectNuxtProject(cwd) }) { if (!project) return { error: 'nuxt_not_detected' }; const absFile = path.join(cwd, project.pluginFile); const existing = fs.existsSync(absFile) ? fs.readFileSync(absFile, 'utf-8') : null; @@ -315,7 +324,7 @@ export function applyNuxtLiveAdapter({ cwd = process.cwd(), port, project = dete }; } - const content = buildNuxtPlugin(port); + const content = buildNuxtPlugin(port, token); fs.mkdirSync(path.dirname(absFile), { recursive: true }); if (content !== existing) fs.writeFileSync(absFile, content, 'utf-8'); return { @@ -497,7 +506,18 @@ function validateConfig(cfg) { function commentOpen(syntax) { return syntax === 'jsx' ? '{/*' : ''; } -function buildTagBlock(syntax, port, filePath) { +/** + * Build the /live.js src the browser loads. When a token is supplied it rides + * as a `?token=...` query param so the server's token-gated /live.js handler + * authorizes the fetch. Shared by every injection path (HTML/JSX script tag, + * the Nuxt plugin, the SvelteKit root component) so they stay in sync. + */ +export function buildLiveScriptSrc(port, token) { + const base = 'http://localhost:' + port + '/live.js'; + return token ? base + '?token=' + encodeURIComponent(token) : base; +} + +function buildTagBlock(syntax, port, filePath, token) { const open = commentOpen(syntax); const close = commentClose(syntax); // Astro processes \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.agents/skills/impeccable/scripts/live-server.mjs b/.agents/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.agents/skills/impeccable/scripts/live-server.mjs +++ b/.agents/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.claude/skills/impeccable/scripts/live-server.mjs b/.claude/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.claude/skills/impeccable/scripts/live-server.mjs +++ b/.claude/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.cursor/skills/impeccable/scripts/live-server.mjs b/.cursor/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.cursor/skills/impeccable/scripts/live-server.mjs +++ b/.cursor/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.gemini/skills/impeccable/scripts/live-server.mjs b/.gemini/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.gemini/skills/impeccable/scripts/live-server.mjs +++ b/.gemini/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.github/skills/impeccable/scripts/live-server.mjs b/.github/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.github/skills/impeccable/scripts/live-server.mjs +++ b/.github/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.grok/skills/impeccable/scripts/live-server.mjs b/.grok/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.grok/skills/impeccable/scripts/live-server.mjs +++ b/.grok/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.kiro/skills/impeccable/scripts/live-server.mjs b/.kiro/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.kiro/skills/impeccable/scripts/live-server.mjs +++ b/.kiro/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.opencode/skills/impeccable/scripts/live-server.mjs b/.opencode/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.opencode/skills/impeccable/scripts/live-server.mjs +++ b/.opencode/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.pi/skills/impeccable/scripts/live-server.mjs b/.pi/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.pi/skills/impeccable/scripts/live-server.mjs +++ b/.pi/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.qoder/skills/impeccable/scripts/live-server.mjs b/.qoder/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.qoder/skills/impeccable/scripts/live-server.mjs +++ b/.qoder/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.rovodev/skills/impeccable/scripts/live-server.mjs b/.rovodev/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.rovodev/skills/impeccable/scripts/live-server.mjs +++ b/.rovodev/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.trae-cn/skills/impeccable/scripts/live-server.mjs b/.trae-cn/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.trae-cn/skills/impeccable/scripts/live-server.mjs +++ b/.trae-cn/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.trae/skills/impeccable/scripts/live-server.mjs b/.trae/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.trae/skills/impeccable/scripts/live-server.mjs +++ b/.trae/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/.vibe/skills/impeccable/scripts/live-server.mjs b/.vibe/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/.vibe/skills/impeccable/scripts/live-server.mjs +++ b/.vibe/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected \n' + + '\n' + open + ' ' + MARKER_CLOSE_TEXT + ' ' + close + '\n' ); } @@ -528,9 +548,9 @@ function readLineEndingAt(content, index) { return ''; } -function insertTag(content, config, port, filePath) { +function insertTag(content, config, port, filePath, token) { const lineEnding = detectLineEnding(content); - const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath), lineEnding); + const block = normalizeLineEndings(buildTagBlock(config.commentSyntax, port, filePath, token), lineEnding); // insertBefore: match the LAST occurrence. Anchors like `` naturally // belong at the end, and the same literal can appear earlier in code blocks // within rendered documentation pages. diff --git a/plugin/skills/impeccable/scripts/live-server.mjs b/plugin/skills/impeccable/scripts/live-server.mjs index c6e951086..c9fc56ed4 100644 --- a/plugin/skills/impeccable/scripts/live-server.mjs +++ b/plugin/skills/impeccable/scripts/live-server.mjs @@ -624,13 +624,37 @@ function statOrNull(filePath) { try { return fs.statSync(filePath); } catch { return null; } } +// Strict loopback-origin test for CORS. Parses the Origin as a URL (never a +// substring match, so `http://localhost.evil.com` and `http://127.0.0.1.evil.com` +// fail) and accepts only http/https on localhost, 127.0.0.1, or the IPv6 loopback. +function isLoopbackOrigin(origin) { + if (typeof origin !== 'string' || origin.length === 0) return false; + let parsed; + try { parsed = new URL(origin); } catch { return false; } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false; + const host = parsed.hostname.toLowerCase(); + return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]'; +} + // HTTP request handler // --------------------------------------------------------------------------- function createRequestHandler({ detectScript, liveScriptParts }) { return (req, res) => { const url = new URL(req.url, `http://localhost:${state.port}`); - res.setHeader('Access-Control-Allow-Origin', '*'); + // Loopback-restricted CORS. Reflect the caller's Origin only when it is a + // loopback origin, always paired with `Vary: Origin` so an intermediary + // cache never serves a response authorized for one origin to another. A + // remote page (e.g. https://evil.example probing the port from a tab open + // on the same machine) gets no Access-Control-Allow-Origin, so its + // JS-initiated fetch cannot read any response. Requests with no Origin + // header (script tags, curl, the agent's own fetches) are not subject to + // CORS and keep working; no ACAO header is needed for them. + const origin = req.headers.origin; + if (origin && isLoopbackOrigin(origin)) { + res.setHeader('Access-Control-Allow-Origin', origin); + res.setHeader('Vary', 'Origin'); + } res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } @@ -639,6 +663,15 @@ function createRequestHandler({ detectScript, liveScriptParts }) { // --- Scripts --- if (p === '/live.js') { + // Token-gated: the script body embeds state.token, which unlocks every + // token-guarded route. Serving it unauthenticated let any local page read + // the token and drive the session. The injected