Skip to content

Commit faeb155

Browse files
committed
fix: security and ux improvements from llm audit
- replace eval() with require() in configLoader for security - add esm compatibility with createRequire(import.meta.url) - fix docker healthcheck to use shell form for runtime port - replace fixed sleep with health check loop in start-live.sh - add isFallback visual indicator in StoryUIPanel - update CLAUDE.md version to 3.10.8 - remove self-referential devDependency
1 parent 96f28d6 commit faeb155

File tree

7 files changed

+140
-52
lines changed

7 files changed

+140
-52
lines changed

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Story UI - AI Assistant Project Guide
22

3-
> **Last Updated**: December 4, 2025
4-
> **Current Version**: 3.6.2
3+
> **Last Updated**: December 14, 2025
4+
> **Current Version**: 3.10.8
55
> **Production URL**: https://app-production-16de.up.railway.app (Vue/Vuetify example)
66
> **Repository**: https://github.com/southleft/story-ui
77

Dockerfile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ RUN chmod +x ./start-live.sh
5252
EXPOSE ${PORT:-4001}
5353

5454
# Health check - verify MCP server is responding
55-
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
56-
CMD wget --no-verbose --tries=1 --spider http://localhost:${PORT:-4001}/story-ui/providers || exit 1
55+
# Use shell form for runtime $PORT expansion (Railway sets PORT dynamically)
56+
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
57+
CMD /bin/sh -c 'wget --no-verbose --tries=1 --spider http://localhost:${PORT:-4001}/story-ui/providers || exit 1'
5758

5859
# Start both servers
5960
CMD ["./start-live.sh"]

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,6 @@
9696
"@semantic-release/github": "^10.3.5",
9797
"@semantic-release/npm": "^12.0.1",
9898
"@semantic-release/release-notes-generator": "^14.0.1",
99-
"@tpitre/story-ui": "^3.6.2",
10099
"@types/cors": "^2.8.17",
101100
"@types/express": "^4.17.21",
102101
"@types/glob": "^8.1.0",

start-live.sh

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,38 @@ cd "$STORYBOOK_DIR"
1717
npm run storybook -- --port "$STORYBOOK_PORT" --host 0.0.0.0 --ci --no-open &
1818
STORYBOOK_PID=$!
1919

20-
# Wait for Storybook to initialize
20+
# Wait for Storybook to be ready (up to 60 seconds)
2121
echo "⏳ Waiting for Storybook to start..."
22-
sleep 10
22+
MAX_WAIT=60
23+
WAIT_INTERVAL=2
24+
ELAPSED=0
2325

24-
# Verify Storybook is running
25-
if ! kill -0 $STORYBOOK_PID 2>/dev/null; then
26-
echo "❌ Storybook failed to start"
26+
while [ $ELAPSED -lt $MAX_WAIT ]; do
27+
# Check if process is still running
28+
if ! kill -0 $STORYBOOK_PID 2>/dev/null; then
29+
echo "❌ Storybook process exited unexpectedly"
30+
exit 1
31+
fi
32+
33+
# Check if Storybook is responding to HTTP requests
34+
if wget -q --spider http://localhost:${STORYBOOK_PORT}/ 2>/dev/null; then
35+
echo "✅ Storybook dev server is ready on port ${STORYBOOK_PORT}"
36+
break
37+
fi
38+
39+
# Wait and retry
40+
sleep $WAIT_INTERVAL
41+
ELAPSED=$((ELAPSED + WAIT_INTERVAL))
42+
echo " Still waiting... (${ELAPSED}s/${MAX_WAIT}s)"
43+
done
44+
45+
# Final check - if we exhausted the wait time
46+
if [ $ELAPSED -ge $MAX_WAIT ]; then
47+
echo "❌ Storybook failed to start within ${MAX_WAIT} seconds"
48+
kill $STORYBOOK_PID 2>/dev/null
2749
exit 1
2850
fi
2951

30-
echo "✅ Storybook dev server running on port ${STORYBOOK_PORT}"
31-
3252
# Start MCP server with Storybook proxy enabled
3353
echo "🤖 Starting MCP server on port ${MCP_PORT}..."
3454
cd /app

story-generator/configLoader.ts

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import fs from 'fs';
22
import path from 'path';
3+
import { createRequire } from 'module';
34
import { StoryUIConfig, DEFAULT_CONFIG, createStoryUIConfig } from '../story-ui.config.js';
45

6+
// Create require function for ESM compatibility
7+
const require = createRequire(import.meta.url);
8+
59
// Config cache to prevent excessive loading
610
let cachedConfig: StoryUIConfig | null = null;
711
let configLoadTime: number = 0;
@@ -37,47 +41,64 @@ export function loadUserConfig(): StoryUIConfig {
3741
} else {
3842
console.log(`Loading Story UI config from: ${configPath}`);
3943
}
40-
// Read and evaluate the config file
41-
const configContent = fs.readFileSync(configPath, 'utf-8');
42-
43-
// Handle both CommonJS and ES modules
44-
if (configContent.includes('module.exports') || configContent.includes('export default')) {
45-
// Create a temporary module context
46-
const module = { exports: {} };
47-
const exports = module.exports;
48-
49-
// For ES modules, convert to CommonJS for evaluation
50-
let evalContent = configContent;
51-
if (configContent.includes('export default')) {
52-
evalContent = configContent.replace(/export\s+default\s+/, 'module.exports = ');
53-
}
5444

55-
// Evaluate the config file content
56-
eval(evalContent);
57-
58-
const userConfig = module.exports as any;
59-
const config = createStoryUIConfig(userConfig.default || userConfig);
60-
61-
// Detect Storybook framework if not already specified
62-
if (!config.storybookFramework) {
63-
const packageJsonPath = path.join(process.cwd(), 'package.json');
64-
if (fs.existsSync(packageJsonPath)) {
65-
try {
66-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
67-
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
68-
config.storybookFramework = detectStorybookFramework(dependencies);
69-
} catch (error) {
70-
console.warn('Failed to detect Storybook framework:', error);
71-
}
45+
// Use require() for safe config loading (no eval)
46+
// Clear require cache to ensure fresh config on reload
47+
const resolvedPath = path.resolve(configPath);
48+
delete require.cache[resolvedPath];
49+
50+
let userConfig: any;
51+
try {
52+
// eslint-disable-next-line @typescript-eslint/no-var-requires
53+
const loadedModule = require(resolvedPath);
54+
userConfig = loadedModule.default || loadedModule;
55+
} catch (requireError) {
56+
// If require() fails (e.g., ESM project with CJS config), fall back to parsing
57+
// This handles "type": "module" projects with module.exports configs
58+
const configContent = fs.readFileSync(configPath, 'utf-8');
59+
60+
// Try to extract the config object from CommonJS module.exports
61+
// Match module.exports = { ... } with potential trailing semicolons and whitespace
62+
const match = configContent.match(/module\.exports\s*=\s*(\{[\s\S]*\})\s*;*/);
63+
if (match) {
64+
try {
65+
// Clean the config object: remove JS comments for JSON.parse compatibility
66+
let configObj = match[1]
67+
.replace(/\/\/[^\n]*/g, '') // Remove single-line comments
68+
.replace(/,(\s*[}\]])/g, '$1'); // Remove trailing commas
69+
70+
// Use Function constructor (safer than eval, runs in isolated scope)
71+
const configFn = new Function(`return ${configObj}`);
72+
userConfig = configFn();
73+
} catch (parseError) {
74+
console.warn(`Failed to parse config from ${configPath}:`, parseError);
75+
throw requireError; // Re-throw original error
76+
}
77+
} else {
78+
throw requireError; // Re-throw if we can't parse it
79+
}
80+
}
81+
const config = createStoryUIConfig(userConfig);
82+
83+
// Detect Storybook framework if not already specified
84+
if (!config.storybookFramework) {
85+
const packageJsonPath = path.join(process.cwd(), 'package.json');
86+
if (fs.existsSync(packageJsonPath)) {
87+
try {
88+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
89+
const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies };
90+
config.storybookFramework = detectStorybookFramework(dependencies);
91+
} catch (error) {
92+
console.warn('Failed to detect Storybook framework:', error);
7293
}
7394
}
95+
}
7496

75-
// Cache the loaded config
76-
cachedConfig = config;
77-
configLoadTime = now;
97+
// Cache the loaded config
98+
cachedConfig = config;
99+
configLoadTime = now;
78100

79-
return config;
80-
}
101+
return config;
81102
} catch (error) {
82103
console.warn(`Failed to load config from ${configPath}:`, error);
83104
}

templates/StoryUI/StoryUIPanel.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,35 @@
11951195
color: hsl(var(--muted-foreground));
11961196
}
11971197

1198+
/* Fallback story styling (error placeholder was created) */
1199+
.sui-completion-fallback .sui-completion-header {
1200+
color: hsl(var(--warning, 38 92% 50%));
1201+
}
1202+
1203+
.sui-completion-fallback-warning {
1204+
margin-top: var(--space-2);
1205+
padding: var(--space-2) var(--space-3);
1206+
background: hsl(var(--warning, 38 92% 50%) / 0.15);
1207+
border-radius: var(--radius-md);
1208+
font-size: 0.8125rem;
1209+
}
1210+
1211+
.sui-completion-fallback-warning strong {
1212+
display: block;
1213+
color: hsl(var(--warning, 38 92% 50%));
1214+
margin-bottom: var(--space-1);
1215+
}
1216+
1217+
.sui-completion-fallback-warning p {
1218+
margin: 0;
1219+
color: hsl(var(--muted-foreground));
1220+
}
1221+
1222+
/* Error completion styling (generation failed completely) */
1223+
.sui-completion-error .sui-completion-header {
1224+
color: hsl(var(--destructive));
1225+
}
1226+
11981227
/* ============================================
11991228
Checkbox
12001229
============================================ */

templates/StoryUI/StoryUIPanel.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ interface StyleChoice {
8080

8181
interface CompletionFeedback {
8282
success: boolean;
83+
isFallback?: boolean; // True when a fallback error placeholder was created
8384
storyId?: string;
8485
fileName?: string;
8586
title?: string;
@@ -704,12 +705,23 @@ const ProgressIndicator: React.FC<ProgressIndicatorProps> = ({ streamingState })
704705
);
705706
}
706707
if (completion) {
708+
// Determine status icon and class based on success and fallback state
709+
const isFallback = completion.isFallback === true;
710+
const statusIcon = completion.success ? '\u2705' : (isFallback ? '\u26A0\uFE0F' : '\u274C');
711+
const statusClass = completion.success ? '' : (isFallback ? 'sui-completion-fallback' : 'sui-completion-error');
712+
707713
return (
708-
<div className="sui-completion">
714+
<div className={`sui-completion ${statusClass}`}>
709715
<div className="sui-completion-header">
710-
<span>{completion.success ? '\u2705' : '\u274C'}</span>
716+
<span>{statusIcon}</span>
711717
<span>{completion.summary.action}: {completion.title}</span>
712718
</div>
719+
{isFallback && (
720+
<div className="sui-completion-fallback-warning">
721+
<strong>Error Placeholder Created</strong>
722+
<p>Generation failed after retries. A placeholder story was saved that you may want to delete or regenerate.</p>
723+
</div>
724+
)}
713725
{completion.componentsUsed.length > 0 && (
714726
<div className="sui-completion-components">
715727
{completion.componentsUsed.map((comp, i) => (
@@ -1073,10 +1085,16 @@ function StoryUIPanel({ mcpPort }: StoryUIPanelProps) {
10731085
// Build response message
10741086
const buildConversationalResponse = (completion: CompletionFeedback, isUpdate: boolean): string => {
10751087
const parts: string[] = [];
1076-
const statusMarker = completion.success ? '[SUCCESS]' : '[ERROR]';
1088+
const isFallback = completion.isFallback === true;
1089+
const statusMarker = completion.success ? '[SUCCESS]' : (isFallback ? '[WARNING]' : '[ERROR]');
10771090
// Show "Failed:" when success is false, otherwise "Created:" or "Updated:"
1078-
const actionWord = completion.success ? (isUpdate ? 'Updated' : 'Created') : 'Failed';
1091+
const actionWord = completion.success ? (isUpdate ? 'Updated' : 'Created') : (isFallback ? 'Placeholder' : 'Failed');
10791092
parts.push(`${statusMarker} **${actionWord}: "${completion.title}"**`);
1093+
1094+
// Add fallback-specific warning
1095+
if (isFallback) {
1096+
parts.push(`\n\n⚠️ **Generation failed** - An error placeholder was saved. You may want to delete this story and try again with a simpler request.`);
1097+
}
10801098
const componentCount = completion.componentsUsed?.length || 0;
10811099
if (componentCount > 0) {
10821100
const names = completion.componentsUsed.slice(0, 5).map(c => `\`${c.name}\``).join(', ');

0 commit comments

Comments
 (0)