@localnerve/csp-hashes 7.0.3 → 7.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/index.js +4 -3
- package/lib/index.js +51 -29
- package/lib/removeCspMeta.js +5 -5
- package/package.json +4 -4
- package/readme.md +113 -2
package/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CSP Hashes.
|
|
3
3
|
*
|
|
4
|
-
* Return a
|
|
5
|
-
* generating the required CSP hashes for inline and attribute
|
|
4
|
+
* Return a Transform object stream to process Vinyl or plain file-like
|
|
5
|
+
* objects for generating the required CSP hashes for inline and attribute
|
|
6
|
+
* scripts, styles.
|
|
6
7
|
*
|
|
7
8
|
* Copyright (c) 2022-2025 Alex Grant (@localnerve), LocalNerve LLC
|
|
8
9
|
* Licensed under the MIT license.
|
|
9
10
|
*/
|
|
10
|
-
export { hashstream as default, hashstream, removeCspMeta, createCspHash } from './lib/index.js';
|
|
11
|
+
export { hashstream as default, hashstream, removeCspMeta, createCspHash, cspHashes } from './lib/index.js';
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CSP Hashes.
|
|
3
3
|
*
|
|
4
|
-
* Return a
|
|
5
|
-
* generating the required CSP hashes for inline and attribute
|
|
4
|
+
* Return a Transform object stream to process Vinyl or plain file-like
|
|
5
|
+
* objects for generating the required CSP hashes for inline and attribute
|
|
6
|
+
* scripts, styles.
|
|
6
7
|
*
|
|
7
8
|
* Copyright (c) 2022-2025 Alex Grant (@localnerve), LocalNerve LLC
|
|
8
9
|
* Licensed under the MIT license.
|
|
@@ -74,15 +75,53 @@ export function createCspHash(input, algo = 'sha256') {
|
|
|
74
75
|
return makeCspHash(algo, input);
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* cspHashes
|
|
80
|
+
* Accepts an algorithm and content buffer/string and returns a hashes object
|
|
81
|
+
* containing all CSP hashes for inline scripts and styles. This is useful for
|
|
82
|
+
* users who just have a few files to process and will handle I/O themselves,
|
|
83
|
+
* without needing a stream.
|
|
84
|
+
*
|
|
85
|
+
* @param {String} [algo] - hash algorithm, default sha256. Can be sha384, sha512.
|
|
86
|
+
* @param {Buffer|String} content - The HTML content to extract hashes from.
|
|
87
|
+
* @returns {Object} The hashes object with script and style elements and attributes.
|
|
88
|
+
*/
|
|
89
|
+
export function cspHashes (algo = 'sha256', content) {
|
|
90
|
+
if (!/^sha(256|384|512)$/.test(algo)) {
|
|
91
|
+
throw new Error('algo option must be one of "sha256", "sha384", or "sha512" only.');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hashes = {
|
|
95
|
+
script: {
|
|
96
|
+
elements: [],
|
|
97
|
+
attributes: [],
|
|
98
|
+
get all () {
|
|
99
|
+
return this.elements.concat(this.attributes);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
style: {
|
|
103
|
+
elements: [],
|
|
104
|
+
attributes: [],
|
|
105
|
+
get all () {
|
|
106
|
+
return this.elements.concat(this.attributes);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
collectHashes(makeCspHash.bind(null, algo), content, hashes);
|
|
112
|
+
|
|
113
|
+
return hashes;
|
|
114
|
+
}
|
|
115
|
+
|
|
77
116
|
/**
|
|
78
117
|
* hashstream
|
|
79
|
-
* Accepts the processing options and returns
|
|
118
|
+
* Accepts the processing options and returns a Transform object stream.
|
|
80
119
|
*
|
|
81
120
|
* @param {Object} options
|
|
82
121
|
* @param {Function} options.callback - Function to call to process the csp hashes.
|
|
83
122
|
* @param {String} [options.algo] - hash algorithm, default sha256. Can be sha384, sha512.
|
|
84
123
|
* @param {Boolean} [options.replace] - True if callback is used for meta html replacements, defaults to false.
|
|
85
|
-
* @returns Transform object stream
|
|
124
|
+
* @returns Transform object stream that accepts Vinyl or plain file-like objects with `path` and `contents`.
|
|
86
125
|
*/
|
|
87
126
|
export function hashstream ({
|
|
88
127
|
algo = 'sha256',
|
|
@@ -100,37 +139,20 @@ export function hashstream ({
|
|
|
100
139
|
|
|
101
140
|
const transformObjectStream = new Transform({
|
|
102
141
|
objectMode: true,
|
|
103
|
-
transform: (
|
|
104
|
-
const path =
|
|
105
|
-
const content =
|
|
106
|
-
|
|
107
|
-
const hashes =
|
|
108
|
-
script: {
|
|
109
|
-
elements: [],
|
|
110
|
-
attributes: [],
|
|
111
|
-
get all () {
|
|
112
|
-
return this.elements.concat(this.attributes);
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
style: {
|
|
116
|
-
elements: [],
|
|
117
|
-
attributes: [],
|
|
118
|
-
get all () {
|
|
119
|
-
return this.elements.concat(this.attributes);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
collectHashes(makeCspHash.bind(null, algo), content, hashes);
|
|
142
|
+
transform: (file, enc, done) => {
|
|
143
|
+
const path = file.path;
|
|
144
|
+
const content = file.contents;
|
|
145
|
+
|
|
146
|
+
const hashes = cspHashes(algo, content);
|
|
125
147
|
|
|
126
148
|
if (replace) {
|
|
127
149
|
const s = callback(path, hashes, content.toString());
|
|
128
|
-
|
|
150
|
+
file.contents = Buffer.from(s, enc);
|
|
129
151
|
} else {
|
|
130
152
|
callback(path, hashes);
|
|
131
153
|
}
|
|
132
|
-
|
|
133
|
-
done(null,
|
|
154
|
+
|
|
155
|
+
done(null, file);
|
|
134
156
|
}
|
|
135
157
|
});
|
|
136
158
|
|
package/lib/removeCspMeta.js
CHANGED
|
@@ -12,19 +12,19 @@ import { Transform } from 'node:stream';
|
|
|
12
12
|
export function removeCspMeta () {
|
|
13
13
|
return new Transform({
|
|
14
14
|
objectMode: true,
|
|
15
|
-
transform: (
|
|
15
|
+
transform: (file, enc, done) => {
|
|
16
16
|
let e = null;
|
|
17
|
-
const input =
|
|
17
|
+
const input = file?.contents?.toString();
|
|
18
18
|
if (input) {
|
|
19
19
|
const output = input.replace(
|
|
20
20
|
/("?Content-Security-Policy"?)(\s+)(content=")([^"]+)"/i, '$1$2$3$2"'
|
|
21
21
|
);
|
|
22
|
-
|
|
22
|
+
file.contents = Buffer.from(output, enc);
|
|
23
23
|
} else {
|
|
24
|
-
e = new Error('removeCspMeta could not get
|
|
24
|
+
e = new Error('removeCspMeta could not get file object contents');
|
|
25
25
|
e.errorCode = 'EBADINPUT';
|
|
26
26
|
}
|
|
27
|
-
done(e,
|
|
27
|
+
done(e, file);
|
|
28
28
|
}
|
|
29
29
|
});
|
|
30
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@localnerve/csp-hashes",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "Flexible library to generate CSP hashes",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
17
|
"@eslint/js": "^10.0.1",
|
|
18
|
-
"eslint": "^10.
|
|
19
|
-
"eslint-plugin-n": "^18.
|
|
20
|
-
"globals": "^17.
|
|
18
|
+
"eslint": "^10.8.0",
|
|
19
|
+
"eslint-plugin-n": "^18.2.2",
|
|
20
|
+
"globals": "^17.8.0",
|
|
21
21
|
"vinyl": "^3.0.1"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
package/readme.md
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
- [Breaking Changes](#breaking-changes)
|
|
14
14
|
- [API](#api)
|
|
15
15
|
- [hashstream (also the default export)](#hashstream-also-the-default-export)
|
|
16
|
+
- [cspHashes](#csphashes)
|
|
16
17
|
- [createCspHash](#createcsphash)
|
|
17
18
|
- [removeCspMeta](#removecspmeta)
|
|
18
19
|
- [Hashstream Options](#hashstream-options)
|
|
@@ -22,11 +23,12 @@
|
|
|
22
23
|
- [Build Step to Maintain CSP Headers](#build-step-to-maintain-csp-headers)
|
|
23
24
|
- [Build Step to Maintain CSP Meta Tags](#build-step-to-maintain-csp-meta-tags)
|
|
24
25
|
- [Build Step to Remove CSP Meta Tag Content](#build-step-to-remove-csp-meta-tag-content)
|
|
26
|
+
- [Native Node.js Streams (No Gulp/Vinyl Required)](#native-nodejs-streams-no-gulpvinyl-required)
|
|
25
27
|
- [Non-ESM usage](#non-esm-usage)
|
|
26
28
|
- [LICENSE](#license)
|
|
27
29
|
|
|
28
30
|
## Overview
|
|
29
|
-
This
|
|
31
|
+
This Node.js library generates script and style inline element and attribute hashes. It is for use in the generation of HTTP content security policy (CSP) headers or to replace/update Meta tags as a website build step. Works with [Gulp](https://github.com/gulpjs/gulp) via Vinyl objects, or directly with native Node.js streams using plain file-like objects.
|
|
30
32
|
|
|
31
33
|
## Breaking Changes
|
|
32
34
|
+ As of Version 2+, this is an ES Module. See [Non-Esm Usage](#non-esm-usage) for how to use outside of ESM.
|
|
@@ -48,6 +50,33 @@ Stream hashstream ({
|
|
|
48
50
|
|
|
49
51
|
See [`hashstream options`](#hashstream-options) for a detailed explanation of the input options.
|
|
50
52
|
|
|
53
|
+
### cspHashes
|
|
54
|
+
This library exports a convenience function that takes an algorithm and HTML content, and returns a hashes object containing all CSP hashes for inline scripts and styles. This is useful for users who just have a few files to process and will handle I/O themselves, without needing a stream.
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
Object cspHashes(algo = 'sha256', content)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
+ {String} **\[algo\]** - Optional - Defaults to `'sha256'`, can be one of 'sha256', 'sha384' or 'sha512'.
|
|
61
|
+
+ {Buffer|String} **content** - Required - The HTML content to extract hashes from.
|
|
62
|
+
|
|
63
|
+
Returns a [`hashes` object](#callback-hashes-object) with the same structure as the `hashstream` callback receives:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
{
|
|
67
|
+
script: {
|
|
68
|
+
elements: [],
|
|
69
|
+
attributes: [],
|
|
70
|
+
get all () { return this.elements.concat(this.attributes); }
|
|
71
|
+
},
|
|
72
|
+
style: {
|
|
73
|
+
elements: [],
|
|
74
|
+
attributes: [],
|
|
75
|
+
get all () { return this.elements.concat(this.attributes); }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
51
80
|
### createCspHash
|
|
52
81
|
This library exports the helper method it uses to make CSP formatted hashes. This is useful if you have a picece of code you need to hash and place into your hash list outside the scope of the page as rendered.
|
|
53
82
|
|
|
@@ -61,7 +90,7 @@ String createCSPHash(inputString, algo = 'sha256')
|
|
|
61
90
|
Returns a ready to use csp hash string (with quotes) in the form of `'sha256-d3ii1Pel57UO62xosCMNgTaZJhJa87Gd/X6e7UdlEU8='`.
|
|
62
91
|
|
|
63
92
|
### removeCspMeta
|
|
64
|
-
This library also exports a convenience helper method, `removeCspMeta` that is useful for some types of development builds. This method takes no options and returns a stream that operates on [Vinyl](https://github.com/gulpjs/vinyl) objects and
|
|
93
|
+
This library also exports a convenience helper method, `removeCspMeta` that is useful for some types of development builds. This method takes no options and returns a stream that operates on [Vinyl](https://github.com/gulpjs/vinyl) objects or plain file-like objects with `path` and `contents` properties, removing any `Content-Security-Policy` content found in the files.
|
|
65
94
|
|
|
66
95
|
```
|
|
67
96
|
Stream removeCspMeta ()
|
|
@@ -178,6 +207,84 @@ export function stripCspMetaContents (settings) {
|
|
|
178
207
|
}
|
|
179
208
|
```
|
|
180
209
|
|
|
210
|
+
### Simple File Processing (No Stream Required)
|
|
211
|
+
If you just have a few files to process and want to handle I/O yourself, use `cspHashes` instead of `hashstream`:
|
|
212
|
+
|
|
213
|
+
```javascript
|
|
214
|
+
import fs from 'node:fs';
|
|
215
|
+
import { cspHashes } from '@localnerve/csp-hashes';
|
|
216
|
+
|
|
217
|
+
const html = fs.readFileSync('./dist/index.html', { encoding: 'utf8' });
|
|
218
|
+
const hashes = cspHashes('sha256', html);
|
|
219
|
+
|
|
220
|
+
console.log('Script element hashes:', hashes.script.elements.join(' '));
|
|
221
|
+
console.log('Style attribute hashes:', hashes.style.attributes.join(' '));
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Native Node.js Streams (No Gulp/Vinyl Required)
|
|
225
|
+
You can use `hashstream` directly with native Node.js streams by wrapping raw file buffers into plain file-like objects. This example reads HTML files from a source directory, processes them through `hashstream`, and writes the output to a destination:
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
import fs from 'node:fs';
|
|
229
|
+
import path from 'node:path';
|
|
230
|
+
import { pipeline, Transform } from 'node:stream';
|
|
231
|
+
import hashstream from '@localnerve/csp-hashes';
|
|
232
|
+
|
|
233
|
+
const srcDir = './src/html';
|
|
234
|
+
const destDir = './dist/html';
|
|
235
|
+
|
|
236
|
+
// Wrap raw buffer chunks into plain file-like objects for the transform stream.
|
|
237
|
+
function wrapFileObject (filePath) {
|
|
238
|
+
return new Transform({
|
|
239
|
+
readableObjectMode: true,
|
|
240
|
+
construct (callback) { callback(); },
|
|
241
|
+
transform (chunk, enc, done) {
|
|
242
|
+
this.push({ path: filePath, contents: chunk });
|
|
243
|
+
done();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Unwrap file-like objects back to raw buffers for writing.
|
|
249
|
+
function unwrapFileObject () {
|
|
250
|
+
return new Transform({
|
|
251
|
+
writableObjectMode: true,
|
|
252
|
+
construct (callback) { callback(); },
|
|
253
|
+
transform (fileObj, enc, done) {
|
|
254
|
+
this.push(fileObj.contents);
|
|
255
|
+
done();
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async function processFile (filePath) {
|
|
261
|
+
const destPath = path.join(destDir, path.relative(srcDir, filePath));
|
|
262
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
263
|
+
|
|
264
|
+
await new Promise((resolve, reject) => {
|
|
265
|
+
pipeline(
|
|
266
|
+
fs.createReadStream(filePath),
|
|
267
|
+
wrapFileObject(filePath),
|
|
268
|
+
hashstream({
|
|
269
|
+
algo: 'sha256',
|
|
270
|
+
callback: (filePath, hashes) => {
|
|
271
|
+
console.log(`Processed ${path.basename(filePath)}:`);
|
|
272
|
+
console.log(` Script hashes: ${hashes.script.all.length}`);
|
|
273
|
+
console.log(` Style hashes: ${hashes.style.all.length}`);
|
|
274
|
+
}
|
|
275
|
+
}),
|
|
276
|
+
unwrapFileObject(),
|
|
277
|
+
fs.createWriteStream(destPath),
|
|
278
|
+
(err) => err ? reject(err) : resolve()
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Process all .html files in the source directory
|
|
284
|
+
const htmlFiles = fs.readdirSync(srcDir).filter(f => f.endsWith('.html')).map(f => path.join(srcDir, f));
|
|
285
|
+
await Promise.all(htmlFiles.map(processFile));
|
|
286
|
+
```
|
|
287
|
+
|
|
181
288
|
### Non-ESM usage
|
|
182
289
|
As of Version 2, this package is an ES Module, making it incompatible with `require`. To use outside of ESM, you can use this with a dynamic import as in the following example:
|
|
183
290
|
|
|
@@ -194,3 +301,7 @@ import('@localnerve/csp-hashes').then(({ hashstream }) => {
|
|
|
194
301
|
## LICENSE
|
|
195
302
|
|
|
196
303
|
* [MIT, Alex Grant, LocalNerve, LLC](license.md)
|
|
304
|
+
|
|
305
|
+
## Author
|
|
306
|
+
|
|
307
|
+
* Alex Grant, alex@localnerve.com, https://localnerve.com
|