boxwood 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +2 -1
- package/README.md +70 -1
- package/adapters/express/index.js +49 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -114,7 +114,76 @@ console.log(html)
|
|
|
114
114
|
// <div><h1>Hello, World!</h1><p>Welcome to Boxwood</p></div>
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
### Express Integration
|
|
118
|
+
|
|
119
|
+
Boxwood includes built-in Express support:
|
|
120
|
+
|
|
121
|
+
```js
|
|
122
|
+
import express from 'express'
|
|
123
|
+
import engine from 'boxwood/adapters/express'
|
|
124
|
+
import crypto from 'crypto'
|
|
125
|
+
|
|
126
|
+
const app = express()
|
|
127
|
+
|
|
128
|
+
// Register Boxwood as template engine
|
|
129
|
+
app.engine('js', engine())
|
|
130
|
+
app.set('views', './views')
|
|
131
|
+
app.set('view engine', 'js')
|
|
132
|
+
|
|
133
|
+
// CSP (Content Security Policy) nonce for inline scripts
|
|
134
|
+
// A nonce is a unique random value generated for each request that allows
|
|
135
|
+
// specific inline scripts to execute while blocking potential XSS attacks
|
|
136
|
+
app.use((req, res, next) => {
|
|
137
|
+
// Generate a cryptographically secure random nonce
|
|
138
|
+
res.locals.nonce = crypto.randomBytes(16).toString('base64')
|
|
139
|
+
|
|
140
|
+
// Set CSP header - only scripts with this exact nonce can execute
|
|
141
|
+
res.setHeader(
|
|
142
|
+
'Content-Security-Policy',
|
|
143
|
+
`script-src 'nonce-${res.locals.nonce}' 'strict-dynamic';`
|
|
144
|
+
)
|
|
145
|
+
next()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// Render templates - nonce is automatically injected into all inline scripts
|
|
149
|
+
app.get('/', (req, res) => {
|
|
150
|
+
res.render('home', { title: 'Welcome' })
|
|
151
|
+
// Boxwood automatically adds nonce="${res.locals.nonce}" to script tags
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The Express adapter automatically:
|
|
156
|
+
- Handles template caching in production
|
|
157
|
+
- Hot reloads templates in development
|
|
158
|
+
- Injects CSP nonces from `res.locals.nonce` into all inline scripts and styles
|
|
159
|
+
|
|
160
|
+
#### Understanding CSP Nonces
|
|
161
|
+
|
|
162
|
+
A Content Security Policy (CSP) nonce is a security feature that helps prevent Cross-Site Scripting (XSS) attacks:
|
|
163
|
+
|
|
164
|
+
1. **Without CSP**: Any injected `<script>` tag can execute, making XSS attacks possible
|
|
165
|
+
2. **With CSP nonce**: Only scripts with the correct nonce attribute can run
|
|
166
|
+
3. **How it works**:
|
|
167
|
+
- Server generates a unique random nonce for each request
|
|
168
|
+
- Server adds this nonce to the CSP header: `script-src 'nonce-abc123'`
|
|
169
|
+
- Server adds the same nonce to legitimate scripts: `<script nonce="abc123">`
|
|
170
|
+
- Browser only executes scripts that have the matching nonce
|
|
171
|
+
- Attackers can't guess the nonce, so injected scripts are blocked
|
|
172
|
+
|
|
173
|
+
Example output:
|
|
174
|
+
```html
|
|
175
|
+
<!-- HTTP Header -->
|
|
176
|
+
Content-Security-Policy: script-src 'nonce-rAnd0m123' 'strict-dynamic';
|
|
177
|
+
|
|
178
|
+
<!-- Generated HTML -->
|
|
179
|
+
<script nonce="rAnd0m123">
|
|
180
|
+
console.log("This legitimate script will execute")
|
|
181
|
+
</script>
|
|
182
|
+
|
|
183
|
+
<script>
|
|
184
|
+
console.log("This injected script will be blocked!")
|
|
185
|
+
</script>
|
|
186
|
+
```
|
|
118
187
|
|
|
119
188
|
## Features
|
|
120
189
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const NODE_ENV = process.env.NODE_ENV || "development"
|
|
2
|
+
const { compile } = require("../..")
|
|
3
|
+
|
|
4
|
+
function purge(path) {
|
|
5
|
+
const name = require.resolve(path)
|
|
6
|
+
const dependency = require.cache[name]
|
|
7
|
+
if (dependency && dependency.children) {
|
|
8
|
+
dependency.children.forEach((child) => {
|
|
9
|
+
delete require.cache[child.id]
|
|
10
|
+
purge(child.id)
|
|
11
|
+
})
|
|
12
|
+
delete require.cache[name]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function engine(options = {}) {
|
|
17
|
+
const env = options.env || NODE_ENV
|
|
18
|
+
const cache = new Map()
|
|
19
|
+
async function compileFile(path) {
|
|
20
|
+
if (env === "development") {
|
|
21
|
+
purge(path)
|
|
22
|
+
const { template } = await compile(path)
|
|
23
|
+
return template
|
|
24
|
+
}
|
|
25
|
+
if (cache.has(path)) return cache.get(path)
|
|
26
|
+
const { template } = await compile(path)
|
|
27
|
+
cache.set(path, template)
|
|
28
|
+
return template
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function render(path, options, callback) {
|
|
32
|
+
try {
|
|
33
|
+
const template = await compileFile(path)
|
|
34
|
+
// Automatically inject nonce from res.locals if available
|
|
35
|
+
const templateOptions = options && options._locals && options._locals.nonce
|
|
36
|
+
? { ...options, nonce: options._locals.nonce }
|
|
37
|
+
: options
|
|
38
|
+
const html = template(templateOptions)
|
|
39
|
+
if (callback) return callback(null, html)
|
|
40
|
+
return html
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (callback) return callback(error)
|
|
43
|
+
return error.message
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return render
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = engine
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "boxwood",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Compile HTML templates into JS",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./adapters/express": "./adapters/express/index.js"
|
|
10
|
+
},
|
|
7
11
|
"scripts": {
|
|
8
12
|
"test": "node --test --test-reporter=dot \"test/**/*.test.js\" \"test/**/*.spec.js\"",
|
|
9
13
|
"test:debug": "node --test --test-reporter=spec \"test/**/*.test.js\" \"test/**/*.spec.js\"",
|