Skip to content

Commit e4c56aa

Browse files
committed
Create update-csp-directives.js
1 parent 4963f95 commit e4c56aa

File tree

1 file changed

+299
-0
lines changed

1 file changed

+299
-0
lines changed

scripts/update-csp-directives.js

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
#!/usr/bin/env node
2+
/*
3+
* Copyright (c) 2025, Salesforce, Inc.
4+
* All rights reserved.
5+
* SPDX-License-Identifier: BSD-3-Clause
6+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
7+
*/
8+
9+
/**
10+
* Simple script to update the CSP directives in ssr.js by merging in the new configuration
11+
*/
12+
13+
const fs = require('fs')
14+
const path = require('path')
15+
16+
// Default path to the ssr.js file
17+
const DEFAULT_SSR_FILE_PATH = path.join(
18+
__dirname,
19+
'../packages/template-retail-react-app/app/ssr.js'
20+
)
21+
22+
/**
23+
* Get the SSR file path from command line args or use default
24+
*/
25+
function getSSRFilePath(args) {
26+
const pathIndex = args.indexOf('--ssr-path')
27+
if (pathIndex !== -1 && pathIndex + 1 < args.length) {
28+
return args[pathIndex + 1]
29+
}
30+
return DEFAULT_SSR_FILE_PATH
31+
}
32+
33+
/**
34+
* Parse current CSP directives from ssr.js
35+
*/
36+
function getCurrentCSPConfig(ssrFilePath = DEFAULT_SSR_FILE_PATH) {
37+
const content = fs.readFileSync(ssrFilePath, 'utf8')
38+
const directivesMatch = content.match(/directives:\s*{([^}]+)}/s)
39+
40+
if (!directivesMatch) {
41+
throw new Error('Could not find CSP directives in ssr.js')
42+
}
43+
44+
const directivesContent = directivesMatch[1]
45+
const config = {}
46+
47+
// Match each directive and its array
48+
const directiveMatches = directivesContent.matchAll(/'([^']+)':\s*\[([^\]]+)\]/gs)
49+
50+
Array.from(directiveMatches).forEach((match) => {
51+
const directiveName = match[1]
52+
const valuesContent = match[2]
53+
54+
config[directiveName] = []
55+
56+
// Parse values and comments from existing CSP
57+
const lines = valuesContent.split('\n')
58+
let currentComment = null
59+
60+
lines.forEach((line) => {
61+
const trimmedLine = line.trim()
62+
63+
if (trimmedLine.startsWith('//')) {
64+
currentComment = trimmedLine.substring(2).trim()
65+
} else if (trimmedLine.includes("'")) {
66+
const valueMatch = trimmedLine.match(/'([^']+)'/)
67+
if (valueMatch) {
68+
config[directiveName].push({
69+
comment: currentComment,
70+
value: valueMatch[1]
71+
})
72+
currentComment = null
73+
}
74+
}
75+
})
76+
})
77+
78+
return config
79+
}
80+
81+
/**
82+
* Merge new CSP configuration with existing configuration
83+
*/
84+
function mergeCSPConfig(existingConfig, newConfig) {
85+
const mergedConfig = {...existingConfig}
86+
87+
Object.entries(newConfig).forEach(([directiveName, newEntries]) => {
88+
if (!mergedConfig[directiveName]) {
89+
mergedConfig[directiveName] = []
90+
}
91+
92+
newEntries.forEach((newEntry) => {
93+
// Handle both string values and object format for backward compatibility
94+
const newValue = typeof newEntry === 'string' ? newEntry : newEntry.value
95+
96+
// Check if value already exists
97+
const existingEntry = mergedConfig[directiveName].find(
98+
(existing) => existing.value === newValue
99+
)
100+
101+
if (!existingEntry) {
102+
// Add new entry (no comment since input format is simplified)
103+
mergedConfig[directiveName].push({
104+
value: newValue
105+
})
106+
}
107+
})
108+
})
109+
110+
return mergedConfig
111+
}
112+
113+
/**
114+
* Generate CSP directives string from config
115+
*/
116+
function generateCSPDirectives(config) {
117+
const directiveLines = []
118+
119+
Object.entries(config).forEach(([directiveName, entries]) => {
120+
if (entries.length === 0) return
121+
122+
const valueLines = []
123+
entries.forEach((entry, i) => {
124+
if (entry.comment) {
125+
valueLines.push(` // ${entry.comment}`)
126+
}
127+
valueLines.push(
128+
` '${entry.value}'${i < entries.length - 1 ? ',' : ''}`
129+
)
130+
})
131+
132+
const valuesString = valueLines.join('\n')
133+
directiveLines.push(
134+
` '${directiveName}': [\n${valuesString}\n ]`
135+
)
136+
})
137+
138+
return directiveLines.join(',\n')
139+
}
140+
141+
/**
142+
* Parse input from file path
143+
*/
144+
function parseInputFile(filePath) {
145+
if (!fs.existsSync(filePath)) {
146+
throw new Error(`Config file not found: ${filePath}`)
147+
}
148+
149+
const configContent = fs.readFileSync(filePath, 'utf8')
150+
return JSON.parse(configContent)
151+
}
152+
153+
/**
154+
* Parse input from stdin
155+
*/
156+
function parseInputFromStdin() {
157+
return new Promise((resolve, reject) => {
158+
let stdinData = ''
159+
160+
process.stdin.setEncoding('utf8')
161+
162+
process.stdin.on('data', (chunk) => {
163+
stdinData += chunk
164+
})
165+
166+
process.stdin.on('end', () => {
167+
try {
168+
const config = JSON.parse(stdinData.trim())
169+
resolve(config)
170+
} catch (error) {
171+
reject(new Error(`Invalid JSON from stdin: ${error.message}`))
172+
}
173+
})
174+
175+
process.stdin.on('error', (error) => {
176+
reject(new Error(`Error reading from stdin: ${error.message}`))
177+
})
178+
})
179+
}
180+
181+
/**
182+
* Update ssr.js with new CSP configuration
183+
*/
184+
function updateSSRFile(config, ssrFilePath = DEFAULT_SSR_FILE_PATH) {
185+
const content = fs.readFileSync(ssrFilePath, 'utf8')
186+
const directivesString = generateCSPDirectives(config)
187+
188+
const newContent = content.replace(
189+
/directives:\s*{[^}]+}/s,
190+
`directives: {\n${directivesString}\n }`
191+
)
192+
193+
fs.writeFileSync(ssrFilePath, newContent, 'utf8')
194+
console.log('✅ Successfully updated CSP directives in ssr.js')
195+
}
196+
197+
/**
198+
* Main function
199+
*/
200+
async function main() {
201+
const args = process.argv.slice(2)
202+
const filteredArgs = args.filter((arg, index) => {
203+
return !(arg === '--ssr-path' || args[index - 1] === '--ssr-path')
204+
})
205+
206+
// Show help if no file argument and stdin is a TTY (not piped)
207+
if (filteredArgs.length === 0 && process.stdin.isTTY) {
208+
console.log(`
209+
Usage:
210+
node scripts/update-csp-directives-simple.js <config.json>
211+
echo '{"img-src": ["*.example.com"]}' | node scripts/update-csp-directives-simple.js
212+
213+
Options:
214+
--ssr-path <path> - Custom path to ssr.js file (default: packages/template-retail-react-app/app/ssr.js)
215+
216+
Examples:
217+
# Merge CSP from configuration file
218+
node scripts/update-csp-directives-simple.js csp-config.json
219+
220+
# Merge CSP from JSON via stdin/pipe
221+
echo '{"img-src": ["*.example.com"]}' | node scripts/update-csp-directives-simple.js
222+
cat csp-config.json | node scripts/update-csp-directives-simple.js
223+
224+
# Use custom ssr.js path
225+
node scripts/update-csp-directives-simple.js --ssr-path custom/path/ssr.js csp-config.json
226+
echo '{"script-src": ["cdn.example.com"]}' | node scripts/update-csp-directives-simple.js --ssr-path custom/path/ssr.js
227+
228+
Config JSON Format:
229+
{
230+
"img-src": [
231+
"*.commercecloud.salesforce.com",
232+
"*.example.com"
233+
],
234+
"script-src": [
235+
"cdn.example.com",
236+
"another-cdn.com"
237+
]
238+
}
239+
`)
240+
process.exit(1)
241+
}
242+
243+
const ssrFilePath = getSSRFilePath(args)
244+
245+
try {
246+
let newConfig
247+
248+
// Check if we have a file argument or should read from stdin
249+
if (filteredArgs.length > 0) {
250+
// Read from file
251+
const configFile = filteredArgs[0]
252+
newConfig = parseInputFile(configFile)
253+
} else {
254+
// Read from stdin
255+
newConfig = await parseInputFromStdin()
256+
}
257+
258+
// Validate config structure
259+
Object.entries(newConfig).forEach(([directive, entries]) => {
260+
if (!Array.isArray(entries)) {
261+
throw new Error(`Directive '${directive}' must be an array`)
262+
}
263+
264+
entries.forEach((entry) => {
265+
if (typeof entry !== 'string' && (typeof entry !== 'object' || !entry.value)) {
266+
throw new Error(`Each entry must be a string or object with 'value' property`)
267+
}
268+
})
269+
})
270+
271+
// Get current configuration and merge with new configuration
272+
const currentConfig = getCurrentCSPConfig(ssrFilePath)
273+
const mergedConfig = mergeCSPConfig(currentConfig, newConfig)
274+
275+
updateSSRFile(mergedConfig, ssrFilePath)
276+
console.log(`✅ Merged CSP directives successfully`)
277+
} catch (error) {
278+
console.error('❌ Error:', error.message)
279+
process.exit(1)
280+
}
281+
}
282+
283+
// Run the script
284+
if (require.main === module) {
285+
main().catch((error) => {
286+
console.error('❌ Error:', error.message)
287+
process.exit(1)
288+
})
289+
}
290+
291+
module.exports = {
292+
getCurrentCSPConfig,
293+
mergeCSPConfig,
294+
generateCSPDirectives,
295+
updateSSRFile,
296+
getSSRFilePath,
297+
parseInputFile,
298+
parseInputFromStdin
299+
}

0 commit comments

Comments
 (0)