@dintero/sri-to-dist 1.0.3
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/LICENSE +20 -0
- package/README.md +64 -0
- package/bin/sri-to-dist.js +4 -0
- package/dist/package.json +57 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +74 -0
- package/dist/src/lib.d.ts +11 -0
- package/dist/src/lib.js +262 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2025 Dintero
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
10
|
+
subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# sri-to-dist
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Dintero/sri-to-dist/actions?query=branch%3Amaster+workflow%3ACI+) [](https://www.npmjs.com/package/@dintero/sri-to-dist)
|
|
4
|
+
|
|
5
|
+
A tool to add subresource integrity (SRI) hashes to HTML files.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @dintero/sri-to-dist
|
|
11
|
+
# or
|
|
12
|
+
npx @dintero/sri-to-dist
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
sri-to-dist -i input.html -o output.html
|
|
19
|
+
sri-to-dist --input input.html --output output.html --base-url https://example.com
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Options
|
|
23
|
+
|
|
24
|
+
- `-i, --input <file>`: Input HTML file (required)
|
|
25
|
+
- `-o, --output <file>`: Output HTML file (optional, defaults to stdout)
|
|
26
|
+
- `-b, --base-url <url>`: Base URL for resolving relative paths (optional)
|
|
27
|
+
- `-n, --no-remote <url>`: Optional flag, no remote sri files allowed
|
|
28
|
+
- `-v, --verify <url>`: Optional flag, verify that all sri resources from input have correct sha384 hashes
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
MIT
|
|
33
|
+
|
|
34
|
+
## Security
|
|
35
|
+
|
|
36
|
+
Contact us at [security@dintero.com](mailto:security@dintero.com)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
## Step 8: Build and test locally
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install
|
|
43
|
+
npm run build
|
|
44
|
+
npm run test
|
|
45
|
+
npm link # This makes your package available globally for testing
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Now you can run:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
sri-to-dist -i test.html
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Step 9: Publish to npm
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm login
|
|
58
|
+
npm publish
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Creating a new release
|
|
62
|
+
|
|
63
|
+
1. Enforce all commits to the master branch to be formatted according to the [Angular Commit Message Format](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-format)
|
|
64
|
+
2. When merged to master, it will automatically be released with [semantic-release](https://github.com/semantic-release/semantic-release)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dintero/sri-to-dist",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "HTML tool for adding subresource integrity hashes",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sri-to-dist": "bin/sri-to-dist.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"prepare": "npm run build",
|
|
17
|
+
"start": "node bin/sri-to-dist.js",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"lint": "biome lint .",
|
|
21
|
+
"format": "biome format . --write",
|
|
22
|
+
"prepublishOnly": "yarn run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"subresource",
|
|
26
|
+
"integrity",
|
|
27
|
+
"sri",
|
|
28
|
+
"html",
|
|
29
|
+
"security"
|
|
30
|
+
],
|
|
31
|
+
"homepage": "https://github.com/Dintero/sri-to-dist?tab=readme-ov-file#sri-to-dist",
|
|
32
|
+
"author": "Sven Nicolai Viig <sven@dintero.com> (http://dintero.com)",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "1.9.4",
|
|
36
|
+
"@types/node": "22.14.0",
|
|
37
|
+
"@types/temp": "0.9.4",
|
|
38
|
+
"semantic-release": "24.2.3",
|
|
39
|
+
"temp": "0.9.4",
|
|
40
|
+
"typescript": "5.8.2",
|
|
41
|
+
"vitest": "3.1.1"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"commander": "13.1.0"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/Dintero/sri-to-dist/issues"
|
|
48
|
+
},
|
|
49
|
+
"private": false,
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/Dintero/sri-to-dist.git"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("node:fs"));
|
|
37
|
+
const path = __importStar(require("node:path"));
|
|
38
|
+
const commander_1 = require("commander");
|
|
39
|
+
const lib_1 = require("./lib");
|
|
40
|
+
const package_json_1 = require("../package.json");
|
|
41
|
+
const main = async () => {
|
|
42
|
+
try {
|
|
43
|
+
// Set up command-line parser
|
|
44
|
+
const program = new commander_1.Command();
|
|
45
|
+
program
|
|
46
|
+
.version(package_json_1.version)
|
|
47
|
+
.description("HTML processing tool to add subresource integrity hashes")
|
|
48
|
+
.requiredOption("-i, --input <file>", "Input HTML file")
|
|
49
|
+
.option("-o, --output <file>", "Optional output HTML file")
|
|
50
|
+
.option("-b, --base-url <url>", "Optional base URL")
|
|
51
|
+
.option("-n, --no-remote", "Optional flag, no remote sri files allowed")
|
|
52
|
+
.option("-v, --verify", "Optional flag, verify hashes in input");
|
|
53
|
+
program.parse(process.argv);
|
|
54
|
+
const options = program.opts();
|
|
55
|
+
// Extract values
|
|
56
|
+
const inputPath = options.input;
|
|
57
|
+
const outputPath = options.output;
|
|
58
|
+
const baseUrl = options.baseUrl;
|
|
59
|
+
const noRemote = options.noRemote || false;
|
|
60
|
+
const verify = options.verify || false;
|
|
61
|
+
// Read HTML file
|
|
62
|
+
const htmlContent = fs.readFileSync(inputPath, "utf-8");
|
|
63
|
+
const updatedHtml = await (0, lib_1.toHtmlWithSri)(htmlContent, path.dirname(inputPath), baseUrl, noRemote, verify);
|
|
64
|
+
(0, lib_1.handleUpdatedHtml)(process.stdout, outputPath, updatedHtml);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
console.error(`Error: ${error}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
main().catch((error) => {
|
|
72
|
+
console.error(`Error: ${error}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
declare const extractLinkRel: (scriptOrLinkTag: string) => string | undefined;
|
|
2
|
+
declare const isSriTag: (scriptOrLinkTag: string) => boolean;
|
|
3
|
+
declare const toHtmlWithSri: (htmlContent: string, baseDir: string, baseUrl?: string, noRemote?: boolean, verify?: boolean) => Promise<string>;
|
|
4
|
+
declare const getContent: (src: string, baseDir: string, baseUrl?: string, noRemote?: boolean) => Promise<Buffer>;
|
|
5
|
+
declare const fetchRemoteContent: (src: string) => Promise<Buffer>;
|
|
6
|
+
declare const readLocalContent: (src: string, baseDir: string, baseUrl: string) => Buffer;
|
|
7
|
+
declare const calculateSha384: (content: Buffer) => string;
|
|
8
|
+
declare const handleUpdatedHtml: (stdout: NodeJS.WriteStream, outputPath?: string, updatedHtml?: string) => void;
|
|
9
|
+
declare const toSriScriptTag: (tag: string, integrity: string) => string;
|
|
10
|
+
declare const ensureCrossoriginAnonymous: (tag: string) => string;
|
|
11
|
+
export { extractLinkRel, isSriTag, toHtmlWithSri, getContent, readLocalContent, fetchRemoteContent, calculateSha384, ensureCrossoriginAnonymous, toSriScriptTag, handleUpdatedHtml, };
|
package/dist/src/lib.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.handleUpdatedHtml = exports.toSriScriptTag = exports.ensureCrossoriginAnonymous = exports.calculateSha384 = exports.fetchRemoteContent = exports.readLocalContent = exports.getContent = exports.toHtmlWithSri = exports.isSriTag = exports.extractLinkRel = void 0;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const path = __importStar(require("node:path"));
|
|
39
|
+
const node_crypto_1 = require("node:crypto");
|
|
40
|
+
const extractLinkRel = (scriptOrLinkTag) => {
|
|
41
|
+
const re = /<link\s+[^>]*rel="([^"]*)"/;
|
|
42
|
+
const match = scriptOrLinkTag.match(re);
|
|
43
|
+
return match ? match[1] : undefined;
|
|
44
|
+
};
|
|
45
|
+
exports.extractLinkRel = extractLinkRel;
|
|
46
|
+
const isSriTag = (scriptOrLinkTag) => {
|
|
47
|
+
const rel = extractLinkRel(scriptOrLinkTag);
|
|
48
|
+
if (rel) {
|
|
49
|
+
// Check link tags
|
|
50
|
+
// Split by whitespace to handle multiple rel values
|
|
51
|
+
const relValues = rel.split(/\s+/);
|
|
52
|
+
// Check for critical resource types
|
|
53
|
+
const criticalTypes = [
|
|
54
|
+
"style",
|
|
55
|
+
"stylesheet",
|
|
56
|
+
"preload",
|
|
57
|
+
"modulepreload",
|
|
58
|
+
];
|
|
59
|
+
// If preload, check if it's preloading a style or script
|
|
60
|
+
if (relValues.includes("preload")) {
|
|
61
|
+
// Extract as attribute
|
|
62
|
+
const asPattern = /as="([^"]*)"/;
|
|
63
|
+
const asMatch = scriptOrLinkTag.match(asPattern);
|
|
64
|
+
if (asMatch) {
|
|
65
|
+
const asValue = asMatch[1];
|
|
66
|
+
return asValue === "style" || asValue === "script";
|
|
67
|
+
}
|
|
68
|
+
if (relValues.includes("stylesheet")) {
|
|
69
|
+
// handle case where rel contains both preload and stylesheet
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (relValues.includes("style")) {
|
|
73
|
+
// handle case where rel contains both preload and style
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
// Do not calculate sri for other preload link tags
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
// Check if link tag has sri rel
|
|
80
|
+
for (const relValue of relValues) {
|
|
81
|
+
if (criticalTypes.includes(relValue)) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Ignore other types of link tags
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
// Check if it's a script tag
|
|
89
|
+
if (scriptOrLinkTag.startsWith("<script")) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
// Not a script tag
|
|
93
|
+
return false;
|
|
94
|
+
};
|
|
95
|
+
exports.isSriTag = isSriTag;
|
|
96
|
+
const toHtmlWithSri = async (htmlContent, baseDir, baseUrl, noRemote, verify) => {
|
|
97
|
+
// Find all script and link tags that should have integrity hashes
|
|
98
|
+
const re = /<(script|link)\s+[^>]*(?:src|href)="([^"]+)"[^>]*>/g;
|
|
99
|
+
let updatedHtml = htmlContent;
|
|
100
|
+
const matches = htmlContent.matchAll(re);
|
|
101
|
+
for (const [scriptOrLinkTag, _, src] of matches) {
|
|
102
|
+
// Skip non supported resources
|
|
103
|
+
if (!isSriTag(scriptOrLinkTag)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
// Get content of script or link
|
|
107
|
+
try {
|
|
108
|
+
const content = await getContent(src, baseDir, baseUrl, noRemote);
|
|
109
|
+
// Calculate SHA-384 hash and create integrity attribute value
|
|
110
|
+
const hashHex = calculateSha384(content);
|
|
111
|
+
const integrity = `sha384-${hashHex}`;
|
|
112
|
+
if (verify) {
|
|
113
|
+
const oldHash = getIntegrityFromTag(scriptOrLinkTag);
|
|
114
|
+
if (!oldHash) {
|
|
115
|
+
throw new Error(`Missing hash for ${src}, expected ${integrity}`);
|
|
116
|
+
}
|
|
117
|
+
if (oldHash !== integrity) {
|
|
118
|
+
throw new Error(`Invalid hash ${oldHash} for ${src}, expected ${integrity}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Create new script tag with integrity
|
|
122
|
+
const sriScriptTag = toSriScriptTag(scriptOrLinkTag, integrity);
|
|
123
|
+
// Replace in HTML
|
|
124
|
+
updatedHtml = updatedHtml.replace(scriptOrLinkTag, sriScriptTag);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error(`Warning: Failed to process ${src}: ${error}`);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return updatedHtml;
|
|
132
|
+
};
|
|
133
|
+
exports.toHtmlWithSri = toHtmlWithSri;
|
|
134
|
+
const getIntegrityFromTag = (tag) => {
|
|
135
|
+
const integrityPattern = /integrity="([^"]*)"/;
|
|
136
|
+
const match = tag.match(integrityPattern);
|
|
137
|
+
return match ? match[1] : undefined;
|
|
138
|
+
};
|
|
139
|
+
const getContent = async (src, baseDir, baseUrl, noRemote) => {
|
|
140
|
+
// Handle remote vs local content
|
|
141
|
+
if (src.startsWith("http://") || src.startsWith("https://")) {
|
|
142
|
+
// If remote_base_url is provided and src starts with it, treat it as local content
|
|
143
|
+
if (baseUrl && src.startsWith(baseUrl)) {
|
|
144
|
+
return readLocalContent(src, baseDir, baseUrl);
|
|
145
|
+
}
|
|
146
|
+
if (noRemote) {
|
|
147
|
+
throw new Error("Remote sri resources not allowed");
|
|
148
|
+
}
|
|
149
|
+
return fetchRemoteContent(src);
|
|
150
|
+
}
|
|
151
|
+
// For relative src, read from file
|
|
152
|
+
return readLocalContent(src, baseDir, baseUrl || "");
|
|
153
|
+
};
|
|
154
|
+
exports.getContent = getContent;
|
|
155
|
+
const fetchRemoteContent = async (src) => {
|
|
156
|
+
try {
|
|
157
|
+
// Using native fetch available in Node.js 18+
|
|
158
|
+
const response = await fetch(src);
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
throw new Error(`Failed to fetch: ${response.statusText}`);
|
|
161
|
+
}
|
|
162
|
+
// Check content type
|
|
163
|
+
const contentType = response.headers.get("content-type") || "";
|
|
164
|
+
// Verify content type is appropriate for scripts or stylesheets
|
|
165
|
+
const validTypes = [
|
|
166
|
+
"text/javascript",
|
|
167
|
+
"application/javascript",
|
|
168
|
+
"application/x-javascript",
|
|
169
|
+
"text/css",
|
|
170
|
+
"text/plain",
|
|
171
|
+
];
|
|
172
|
+
if (!validTypes.some((type) => contentType.includes(type))) {
|
|
173
|
+
throw new Error(`Unexpected content type '${contentType}' for resource '${src}'`);
|
|
174
|
+
}
|
|
175
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
176
|
+
return Buffer.from(arrayBuffer);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
throw new Error(`Failed to fetch remote content from '${src}': ${error}`);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
exports.fetchRemoteContent = fetchRemoteContent;
|
|
183
|
+
const addTrailingSlashIfNotEmpty = (baseUrl) => {
|
|
184
|
+
if (baseUrl === "")
|
|
185
|
+
return baseUrl;
|
|
186
|
+
return baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
|
|
187
|
+
};
|
|
188
|
+
const toLocalPath = (src, baseDir, baseUrl) => {
|
|
189
|
+
const baseWithTrailingSlash = addTrailingSlashIfNotEmpty(baseUrl);
|
|
190
|
+
let localPath = src;
|
|
191
|
+
if (src.startsWith(baseWithTrailingSlash)) {
|
|
192
|
+
localPath = src.substring(baseWithTrailingSlash.length);
|
|
193
|
+
}
|
|
194
|
+
else if (src.startsWith(baseUrl)) {
|
|
195
|
+
localPath = src.substring(baseUrl.length);
|
|
196
|
+
}
|
|
197
|
+
// Remove any slash from start at file path before reading local file
|
|
198
|
+
const localPathWithoutStartingSlash = localPath.startsWith("/")
|
|
199
|
+
? localPath.substring(1)
|
|
200
|
+
: localPath;
|
|
201
|
+
return path.join(baseDir, localPathWithoutStartingSlash);
|
|
202
|
+
};
|
|
203
|
+
const readLocalContent = (src, baseDir, baseUrl) => {
|
|
204
|
+
const filePath = toLocalPath(src, baseDir, baseUrl);
|
|
205
|
+
try {
|
|
206
|
+
return fs.readFileSync(filePath);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
throw new Error(`Failed to read file at path '${filePath}': ${error}`);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
exports.readLocalContent = readLocalContent;
|
|
213
|
+
const calculateSha384 = (content) => {
|
|
214
|
+
const hash = (0, node_crypto_1.createHash)("sha384").update(content).digest();
|
|
215
|
+
return hash.toString("base64");
|
|
216
|
+
};
|
|
217
|
+
exports.calculateSha384 = calculateSha384;
|
|
218
|
+
const handleUpdatedHtml = (stdout, outputPath, updatedHtml) => {
|
|
219
|
+
if (!updatedHtml)
|
|
220
|
+
return;
|
|
221
|
+
if (outputPath) {
|
|
222
|
+
fs.writeFileSync(outputPath, updatedHtml);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
stdout.write(`${updatedHtml}\n`);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
exports.handleUpdatedHtml = handleUpdatedHtml;
|
|
229
|
+
const toSriScriptTag = (tag, integrity) => {
|
|
230
|
+
// First handle the integrity attribute
|
|
231
|
+
let newTag = tag;
|
|
232
|
+
if (tag.includes("integrity=")) {
|
|
233
|
+
// Replace existing integrity attribute
|
|
234
|
+
const reIntegrity = /integrity="[^"]*"/g;
|
|
235
|
+
newTag = newTag.replace(reIntegrity, `integrity="${integrity}"`);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
// Add integrity attribute
|
|
239
|
+
if (tag.endsWith("/>")) {
|
|
240
|
+
newTag = newTag.replace(/\/>$/, ` integrity="${integrity}"/>`);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
newTag = newTag.replace(/>$/, ` integrity="${integrity}">`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Ensure crossorigin attribute is set
|
|
247
|
+
return ensureCrossoriginAnonymous(newTag);
|
|
248
|
+
};
|
|
249
|
+
exports.toSriScriptTag = toSriScriptTag;
|
|
250
|
+
const ensureCrossoriginAnonymous = (tag) => {
|
|
251
|
+
// Replace if exists
|
|
252
|
+
if (tag.includes("crossorigin=")) {
|
|
253
|
+
const reCrossorigin = /crossorigin="[^"]*"/g;
|
|
254
|
+
return tag.replace(reCrossorigin, 'crossorigin="anonymous"');
|
|
255
|
+
}
|
|
256
|
+
// Add crossorigin="anonymous" attribute if missing from tag
|
|
257
|
+
if (tag.endsWith("/>")) {
|
|
258
|
+
return tag.replace(/\/>$/, ' crossorigin="anonymous"/>');
|
|
259
|
+
}
|
|
260
|
+
return tag.replace(/>$/, ' crossorigin="anonymous">');
|
|
261
|
+
};
|
|
262
|
+
exports.ensureCrossoriginAnonymous = ensureCrossoriginAnonymous;
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dintero/sri-to-dist",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "HTML tool for adding subresource integrity hashes",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"sri-to-dist": "bin/sri-to-dist.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"prepare": "npm run build",
|
|
17
|
+
"start": "node bin/sri-to-dist.js",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"test:watch": "vitest",
|
|
20
|
+
"lint": "biome lint .",
|
|
21
|
+
"format": "biome format . --write",
|
|
22
|
+
"prepublishOnly": "yarn run build"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"subresource",
|
|
26
|
+
"integrity",
|
|
27
|
+
"sri",
|
|
28
|
+
"html",
|
|
29
|
+
"security"
|
|
30
|
+
],
|
|
31
|
+
"homepage": "https://github.com/Dintero/sri-to-dist?tab=readme-ov-file#sri-to-dist",
|
|
32
|
+
"author": "Sven Nicolai Viig <sven@dintero.com> (http://dintero.com)",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@biomejs/biome": "1.9.4",
|
|
36
|
+
"@types/node": "22.14.0",
|
|
37
|
+
"@types/temp": "0.9.4",
|
|
38
|
+
"semantic-release": "24.2.3",
|
|
39
|
+
"temp": "0.9.4",
|
|
40
|
+
"typescript": "5.8.2",
|
|
41
|
+
"vitest": "3.1.1"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"commander": "13.1.0"
|
|
45
|
+
},
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/Dintero/sri-to-dist/issues"
|
|
48
|
+
},
|
|
49
|
+
"private": false,
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "https://github.com/Dintero/sri-to-dist.git"
|
|
56
|
+
}
|
|
57
|
+
}
|