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.
@@ -3,7 +3,8 @@
3
3
  "allow": [
4
4
  "Bash(node test:*)",
5
5
  "Bash(mkdir:*)",
6
- "Bash(npm test:*)"
6
+ "Bash(npm test:*)",
7
+ "Bash(mv:*)"
7
8
  ],
8
9
  "deny": []
9
10
  }
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
- For Express apps, use [express-boxwood](https://www.npmjs.com/package/express-boxwood).
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.0.0",
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\"",