Sync generated provider output

This commit is contained in:
github-actions[bot]
2026-07-23 04:59:57 +00:00
parent 3f9fccdfd0
commit fc3dc501a6
60 changed files with 1320 additions and 345 deletions
@@ -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 commentClose(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 <script> tags by default and rewrites src to its own
@@ -506,7 +526,7 @@ function buildTagBlock(syntax, port, filePath) {
const scriptAttrs = isAstro ? 'is:inline ' : '';
return (
open + ' ' + MARKER_OPEN_TEXT + ' ' + close + '\n' +
'<script ' + scriptAttrs + 'src="http://localhost:' + port + '/live.js"></script>\n' +
'<script ' + scriptAttrs + 'src="' + buildLiveScriptSrc(port, token) + '"></script>\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 `</body>` naturally
// belong at the end, and the same literal can appear earlier in code blocks
// within rendered documentation pages.
@@ -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 <script src> carries
// `?token=...` (see live-inject.mjs). A missing/wrong token → 401.
if (url.searchParams.get('token') !== state.token) {
res.writeHead(401, { 'Content-Type': 'text/plain' });
res.end('Unauthorized');
return;
}
// Re-read from disk each request so edits to live-browser.js land on
// the next tab reload. No-store headers prevent browser caching across
// sessions — during iteration, a cached old script silently breaks
@@ -846,7 +879,13 @@ function createRequestHandler({ detectScript, liveScriptParts }) {
const filePath = url.searchParams.get('path');
if (!filePath || filePath.includes('..')) { res.writeHead(400); res.end('Bad path'); return; }
const absPath = path.resolve(process.cwd(), filePath);
if (!absPath.startsWith(process.cwd())) { res.writeHead(403); res.end('Forbidden'); return; }
// Confine to the project root. A bare `startsWith(cwd)` string check lets a
// sibling dir whose name extends the root name (projeto -> projeto-backup)
// slip through; compare on the relative path instead (same pattern as
// sessionFileMetadataFromPollReply below). An empty rel means the request
// resolved to the root directory itself, which this file route never serves.
const rel = path.relative(process.cwd(), absPath);
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) { res.writeHead(403); res.end('Forbidden'); return; }
let content;
try { content = fs.readFileSync(absPath, 'utf-8'); }
catch { res.writeHead(404); res.end('File not found'); return; }
+5 -1
View File
@@ -112,7 +112,11 @@ The agent should then:
}
// 3. Inject the script tag at the current port
const injectOut = runScript('live-inject.mjs', ['--port', String(serverInfo.port)], { cwd: activeCwd });
const injectOut = runScript(
'live-inject.mjs',
['--port', String(serverInfo.port), '--token', String(serverInfo.token)],
{ cwd: activeCwd },
);
const injectResult = safeParse(injectOut);
if (!injectResult || !injectResult.ok) {
console.log(JSON.stringify({
@@ -36,14 +36,14 @@ export function detectSvelteKitProject(cwd = process.cwd(), config = null) {
};
}
export function applySvelteKitLiveAdapter({ cwd = process.cwd(), port, config = null } = {}) {
export function applySvelteKitLiveAdapter({ cwd = process.cwd(), port, token, config = null } = {}) {
if (!Number.isFinite(Number(port))) {
throw new Error('SvelteKit live adapter requires a numeric port');
}
const detected = detectSvelteKitProject(cwd, config);
if (!detected) return null;
ensureSvelteLiveRootComponent(cwd, Number(port));
ensureSvelteLiveRootComponent(cwd, Number(port), token);
const layoutRel = detected.layoutFile;
const layoutAbs = path.join(cwd, layoutRel);
@@ -136,18 +136,20 @@ export function unpatchSvelteLayout(content) {
return out.replace(/\n{3,}/g, '\n\n');
}
export function ensureSvelteLiveRootComponent(cwd, port) {
export function ensureSvelteLiveRootComponent(cwd, port, token) {
const file = path.join(cwd, SVELTE_LIVE_ROOT_COMPONENT);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.writeFileSync(file, buildSvelteLiveRootComponent(port), 'utf-8');
fs.writeFileSync(file, buildSvelteLiveRootComponent(port, token), 'utf-8');
return file;
}
export function buildSvelteLiveRootComponent(port) {
export function buildSvelteLiveRootComponent(port, token) {
const liveUrl = 'http://localhost:' + Number(port) + '/live.js'
+ (token ? '?token=' + encodeURIComponent(token) : '');
return `<script>
import { onMount } from 'svelte';
const LIVE_URL = 'http://localhost:${Number(port)}/live.js';
const LIVE_URL = '${liveUrl}';
const HOST_ID = 'impeccable-live-root';
onMount(() => {