feat(engine): merge cloudflare-migration — paradigm D engine, BYOK proxy, story persistence (#95)
Squash-merge the cloudflare-migration branch (7 commits by Kai ki) into staging with conflict resolution, feature integration, and bug fixes. Engine: - Paradigm D: single-stream Writer replacing dual-phase Plan/Beats - Delete Architect agent; story bible generated via Writer <plan> tag - Modular prompt architecture (segments/registry/builder) - StreamRouter for tagged stream splitting (<plan>/<story>/<choices>) Infrastructure: - Cloudflare Workers deployment (wrangler.jsonc, OpenNext adapter) - D1 database schema + Drizzle ORM (scaffolded, not yet active) - R2 storage helpers (scaffolded, not yet active) - Story persistence API routes + client-side persistence BYOK (Bring Your Own Key): - /api/llm/user-proxy with SSRF-protected LLM proxy (+ requireUser auth) - CORS-aware fetch in ai-client: auto-detect CORS failure, fallback to server proxy transparently via OpenAI SDK custom fetch - BYO config support added to classify-freeform and vision routes - SettingsModal CORS privacy notice (keys never logged/stored) SSE streaming: - engineClient.ts: fetchSSE helper for progressive scene events - startSession/requestScene accept optional emit callback - Fix SSE error event field name (error → message) in scene/start routes i18n integration: - Wire buildLanguageDirective into paradigm D's prompt builder - Update corsNotice i18n keys (zh-CN/en/ja) with CORS proxy privacy text - Preserve Session.language + LanguageSwitcher from i18n commit Co-authored-by: Kai ki <155355644+zbf1009@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Bundle Secret Scanner
|
||||
* Scans Next.js production build artifacts for leaked prompt secrets.
|
||||
* Usage: node scripts/scan-bundle-secrets.mjs
|
||||
* Exit 0 if clean, exit 1 if secrets found (for CI).
|
||||
*/
|
||||
|
||||
import { readFileSync, readdirSync, statSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
// Critical prompt constant names that MUST NOT appear in client bundle
|
||||
const SECRET_PATTERNS = [
|
||||
"CHARACTER_WRITER_SYSTEM",
|
||||
"CHARACTER_DESIGNER_SYSTEM",
|
||||
"CINEMATOGRAPHER_SYSTEM",
|
||||
"ARCHITECT_SYSTEM",
|
||||
"WRITER_PLAN_SYSTEM",
|
||||
"WRITER_BEATS_SYSTEM",
|
||||
"VOICE_DESIGNER_SYSTEM",
|
||||
"FREEFORM_CLASSIFY_SYSTEM",
|
||||
"loadEngineConfig", // config.ts function should not leak
|
||||
];
|
||||
|
||||
// Directories to scan (Next.js client bundle output)
|
||||
const SCAN_DIRS = [
|
||||
".next/static/chunks", // Client-side JS chunks
|
||||
".next/static/css", // CSS bundles (shouldn't have JS, but scan anyway)
|
||||
];
|
||||
|
||||
/**
|
||||
* Recursively scan directory for files
|
||||
*/
|
||||
function* walkDir(dir) {
|
||||
try {
|
||||
const entries = readdirSync(dir);
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry);
|
||||
const stat = statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
yield* walkDir(fullPath);
|
||||
} else if (stat.isFile() && /\.(js|css)$/i.test(entry)) {
|
||||
yield fullPath;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Directory might not exist yet (e.g. fresh clone before build)
|
||||
if (err.code !== "ENOENT") throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan a single file for secret patterns
|
||||
*/
|
||||
function scanFile(filePath) {
|
||||
const content = readFileSync(filePath, "utf8");
|
||||
const found = [];
|
||||
|
||||
for (const pattern of SECRET_PATTERNS) {
|
||||
if (content.includes(pattern)) {
|
||||
found.push(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main scanner
|
||||
*/
|
||||
function main() {
|
||||
console.log("🔍 Scanning Next.js client bundles for leaked secrets...\n");
|
||||
|
||||
let totalFiles = 0;
|
||||
let leaksFound = false;
|
||||
const leakReport = [];
|
||||
|
||||
for (const dir of SCAN_DIRS) {
|
||||
for (const filePath of walkDir(dir)) {
|
||||
totalFiles++;
|
||||
const secrets = scanFile(filePath);
|
||||
if (secrets.length > 0) {
|
||||
leaksFound = true;
|
||||
leakReport.push({ file: filePath, secrets });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (leaksFound) {
|
||||
console.error("❌ SECRET LEAK DETECTED!\n");
|
||||
for (const { file, secrets } of leakReport) {
|
||||
console.error(` File: ${file}`);
|
||||
console.error(` Leaked: ${secrets.join(", ")}\n`);
|
||||
}
|
||||
console.error(
|
||||
"Fix: Ensure lib/engine/prompts.ts and lib/config.ts have 'import \"server-only\"' at the top."
|
||||
);
|
||||
console.error(
|
||||
" Verify no client components import these modules (directly or transitively).\n"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`✅ No secrets found in ${totalFiles} client bundle files.`);
|
||||
console.log(" Prompt isolation is intact.\n");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user