vite-plugin-sri4 4.1.0 → 5.0.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/LICENSE +21 -0
- package/README.md +124 -19
- package/dist/index.cjs +367 -137
- package/dist/index.js +367 -137
- package/package.json +12 -8
- package/types/index.d.ts +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Zac
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, 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,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -13,7 +13,9 @@ A Vite plugin to generate Subresource Integrity (SRI) hashes for your assets dur
|
|
|
13
13
|
- [Usage](#usage)
|
|
14
14
|
- [Plugin Options](#plugin-options)
|
|
15
15
|
- [Dynamic Routes](#dynamic-routes)
|
|
16
|
+
- [External Resources](#external-resources)
|
|
16
17
|
- [When SRI Actually Helps](#when-sri-actually-helps)
|
|
18
|
+
- [How It Attaches Hashes](#how-it-attaches-hashes)
|
|
17
19
|
- [Example Project](#example-project)
|
|
18
20
|
- [Best Practices](#best-practices)
|
|
19
21
|
- [Troubleshooting](#troubleshooting)
|
|
@@ -25,8 +27,10 @@ A Vite plugin to generate Subresource Integrity (SRI) hashes for your assets dur
|
|
|
25
27
|
|
|
26
28
|
- **Automatic SRI Generation:** Computes SRI hashes for assets (chunks and files) using a configurable algorithm (default is `sha384`).
|
|
27
29
|
- **HTML Injection:** Automatically injects `integrity` and `crossorigin` attributes into `<script>` and `<link>` tags in your HTML.
|
|
28
|
-
- **
|
|
29
|
-
- **Bypass Domains:**
|
|
30
|
+
- **External Resource Gating:** A resource on someone else's origin is hashed only when it is reachable, answers `Access-Control-Allow-Origin: *`, and its origin declares the URL immutable. Anything else is left alone with a warning naming the reason — a hash pins one snapshot of bytes, so it is only correct on a URL whose bytes never change. See [External resources](#external-resources).
|
|
31
|
+
- **Bypass and Trust Domains:** `bypassDomains` to leave a host alone, `trustDomains` to hash one whose headers do not declare it stable, plus a `skip-sri` attribute to opt out a single tag.
|
|
32
|
+
- **Public Directory Support:** Assets served verbatim from `publicDir` are hashed from disk, not just bundle outputs.
|
|
33
|
+
- **Zero Dependencies:** No runtime dependencies, and TypeScript definitions are included.
|
|
30
34
|
- **Missing Asset Handling:** Configurable warning suppression for missing assets.
|
|
31
35
|
- **Robust Content Support:** Handles various content types including strings, Buffer, and Uint8Array.
|
|
32
36
|
- **Dynamic Routes:** Optional import map integrity and an SRI manifest cover `import()`-loaded chunks and SSR builds, which have no build-time HTML tag to rewrite.
|
|
@@ -50,8 +54,10 @@ import sri from 'vite-plugin-sri4';
|
|
|
50
54
|
export default defineConfig({
|
|
51
55
|
plugins: [
|
|
52
56
|
sri({
|
|
53
|
-
// Optional.
|
|
57
|
+
// Optional. 'sha256' | 'sha384' | 'sha512'. Defaults to 'sha384'.
|
|
54
58
|
hashAlgorithm: 'sha384',
|
|
59
|
+
// Optional. 'anonymous' | 'use-credentials'. Defaults to 'anonymous'.
|
|
60
|
+
crossorigin: 'anonymous',
|
|
55
61
|
// Optional. Domains to bypass SRI injection.
|
|
56
62
|
bypassDomains: ['example.com'],
|
|
57
63
|
// Optional. Suppress warnings for missing assets.
|
|
@@ -85,11 +91,15 @@ Output:
|
|
|
85
91
|
## Plugin Options
|
|
86
92
|
|
|
87
93
|
* `hashAlgorithm` (string):
|
|
88
|
-
The hash algorithm used for computing SRI.
|
|
94
|
+
The hash algorithm used for computing SRI. One of `sha256`, `sha384` (default) or `sha512` — the only three the SRI spec defines. Anything else fails at startup rather than producing an attribute browsers silently reject.
|
|
95
|
+
* `crossorigin` (string):
|
|
96
|
+
Value for the injected `crossorigin` attribute: `anonymous` (default) or `use-credentials`. Use the latter for a CDN that requires cookies or HTTP auth. Tags that already declare a `crossorigin` are left alone.
|
|
89
97
|
* `bypassDomains` (Array<string>):
|
|
90
|
-
|
|
98
|
+
Hostnames to leave untouched, subdomains included. Use it to silence the warning for a host you have decided not to protect. See [External resources](#external-resources).
|
|
99
|
+
* `trustDomains` (Array<string>):
|
|
100
|
+
Hostnames whose bytes you vouch for, subdomains included. An external resource is only hashed when its origin declares the URL immutable; a host listed here is hashed regardless. For a stable host that does not set the header — not for forcing SRI onto a vendor's rolling URL. See [External resources](#external-resources).
|
|
91
101
|
* `ignoreMissingAsset` (boolean):
|
|
92
|
-
When true,
|
|
102
|
+
When true, warns instead of failing the build for assets found in neither the bundle nor `publicDir`. Default is `false`, which fails the build rather than shipping a tag with no integrity.
|
|
93
103
|
* `logLevel` (string):
|
|
94
104
|
Log verbosity. One of `silent`, `error`, `warn`, `info`, `debug`. Default is `warn`. Use `debug` to see per-resource decisions during the build.
|
|
95
105
|
* `importmap` (boolean):
|
|
@@ -128,12 +138,102 @@ This is the only mechanism available when the build produces no HTML asset.
|
|
|
128
138
|
|
|
129
139
|
Hashes are computed during the build. A plugin that mutates chunk contents after this one (`@vitejs/plugin-legacy`, compression plugins that rewrite in place) would invalidate them, so the plugin re-hashes every file it touched in `writeBundle` and fails the build if anything drifted. You get a build error rather than a page that only breaks in the browser.
|
|
130
140
|
|
|
141
|
+
## External resources
|
|
142
|
+
|
|
143
|
+
The `writeBundle` drift check above covers **your own build outputs only**. Everything you build is hashed locally — bundle chunks and assets from their bytes, `public/` files from disk, and, with an absolute `base`, your own CDN URLs from the bundle rather than the network — so none of it depends on a server being reachable or honest at build time.
|
|
144
|
+
|
|
145
|
+
An external URL pointing at someone else's origin is different. It is fetched **once, at build time, from your build machine**, and the hash is taken from that copy. An `integrity` attribute pins those bytes forever, so it is only correct on a URL whose bytes never change — and the origin is the only party that knows whether that is true.
|
|
146
|
+
|
|
147
|
+
So the plugin asks it. An external resource is hashed only when **all three** hold:
|
|
148
|
+
|
|
149
|
+
1. **`HEAD` succeeds.** Otherwise there is nothing to check.
|
|
150
|
+
2. **`Access-Control-Allow-Origin: *`.** Injecting `integrity` also injects `crossorigin`, so a response scoped to one specific origin — or to none — would turn a working resource into a blocked one.
|
|
151
|
+
3. **The origin declares the URL immutable**: `Cache-Control: immutable`, or a `max-age` of a year or more — and nothing in the same header contradicting it. `private`, `no-store` and `no-cache` each veto it: freshness and shareability are orthogonal, so a per-client response can carry a long `max-age`, and `no-cache, max-age=<long>` is a real CDN spelling of "cache it, but revalidate every time". Or the host is in `trustDomains`.
|
|
152
|
+
|
|
153
|
+
Anything else is left alone, with a warning naming the URL and the reason. Nothing ships without integrity silently.
|
|
154
|
+
|
|
155
|
+
### Why immutability, and not a list of bad origins
|
|
156
|
+
|
|
157
|
+
Because the list is never finished. Version-pinned URLs and rolling ones are two clean clusters, and the CDNs drew the line themselves:
|
|
158
|
+
|
|
159
|
+
| URL | `Cache-Control` | |
|
|
160
|
+
|---|---|---|
|
|
161
|
+
| `cdnjs …/jquery/3.7.1/jquery.min.js` | `max-age=30672000, immutable` | hashed |
|
|
162
|
+
| `jsdelivr …/bootstrap@5.3.3/…` | `max-age=31536000, immutable` | hashed |
|
|
163
|
+
| `unpkg …/htmx.org@1.9.12/…` | `max-age=31536000` | hashed |
|
|
164
|
+
| `code.jquery.com/jquery-3.7.1.min.js` | `max-age=31536000` | hashed |
|
|
165
|
+
| `jsdelivr …/vue@3/…` (floating) | `max-age=604800` | skipped |
|
|
166
|
+
| `fonts.googleapis.com/css2?…` | `private, max-age=86400` | skipped |
|
|
167
|
+
| `plausible.io/js/script.js` | `public, max-age=86400` | skipped |
|
|
168
|
+
| `cdn.tailwindcss.com` | `max-age=14400` | skipped |
|
|
169
|
+
| `connect.facebook.net/en_US/sdk.js` | `public, max-age=1200` | skipped |
|
|
170
|
+
| `js.stripe.com/v3/` | `max-age=120` | skipped |
|
|
171
|
+
| `unpkg …/react@18/…` (floating) | `max-age=60` | skipped |
|
|
172
|
+
|
|
173
|
+
Nothing lands between 604800 and 30672000, so the threshold separates two clusters rather than splitting a spectrum.
|
|
174
|
+
|
|
175
|
+
A blacklist would have to catch every one of the bottom rows individually, and the ones that are ordinary `public` responses — `cdn.tailwindcss.com`, `plausible.io` — look exactly like a resource you *should* hash. Each gap ships a build that works today and breaks whenever that vendor deploys. The whitelist fails the other way: a resource you could have protected ships unprotected, and says so in the log.
|
|
176
|
+
|
|
177
|
+
`Vary` is deliberately not used. Google Fonts varies on `User-Agent` without declaring it there (`vary: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site`), so a `Vary`-based check lets exactly that resource through.
|
|
178
|
+
|
|
179
|
+
### Getting a resource hashed
|
|
180
|
+
|
|
181
|
+
**Pin a version in the URL.** `unpkg.com/react@18.3.1/…` answers `max-age=31536000`; `unpkg.com/react@18/…` answers `max-age=60`. Same for jsdelivr. This is the fix, not a workaround — a floating URL and an integrity attribute are contradictory by construction.
|
|
182
|
+
|
|
183
|
+
**Or vouch for the host** when you know it is stable and it just does not say so:
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
sri({ trustDomains: ['assets.internal.example'] })
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Do not point `trustDomains` at a vendor's rolling URL. Stripe, for one, documents that `js.stripe.com/v3/` must not be pinned; forcing a hash onto it produces a page that works until their next deploy.
|
|
190
|
+
|
|
191
|
+
**Or accept it and silence the warning** with `bypassDomains`. Third-party analytics and widget scripts are usually this case — they are built to auto-update, and there is nothing to pin:
|
|
192
|
+
|
|
193
|
+
```js
|
|
194
|
+
sri({ bypassDomains: ['www.googletagmanager.com', 'connect.facebook.net'] })
|
|
195
|
+
```
|
|
196
|
+
|
|
131
197
|
## When SRI Actually Helps
|
|
132
198
|
|
|
133
199
|
SRI is worth the most when your HTML and your assets have **different trust boundaries** - typically HTML served from your own origin and JS/CSS served from a CDN (`base: 'https://cdn.example.com/'`). If the CDN is compromised or a cache is poisoned, the integrity attribute in your origin-served HTML is what stops the browser from running the tampered file. That is the case this plugin is built for.
|
|
134
200
|
|
|
135
201
|
If everything is served from a single origin, SRI buys much less than it appears to: an attacker who can rewrite `/assets/index-abc123.js` on your server can usually rewrite the `index.html` carrying its hash just as easily. It is not useless - it narrows some deploy and cache-layer mistakes - but for same-origin builds, a Content Security Policy and Vite's default hashed, immutable filenames do more for you than SRI does. Enable it because it is cheap, not because it closes the hole you think it closes.
|
|
136
202
|
|
|
203
|
+
### Skipping a Single Tag
|
|
204
|
+
|
|
205
|
+
`bypassDomains` only reaches external hosts. To exclude one specific element, add `skip-sri` to it. The attribute is stripped from the output:
|
|
206
|
+
|
|
207
|
+
```html
|
|
208
|
+
<script skip-sri src="/legacy.js"></script>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```html
|
|
212
|
+
<!-- built output -->
|
|
213
|
+
<script src="/legacy.js"></script>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## How It Attaches Hashes
|
|
217
|
+
|
|
218
|
+
There are three places a Vite plugin can compute SRI hashes, and they are not equivalent. This one matters more than it looks, so it is worth writing down.
|
|
219
|
+
|
|
220
|
+
**In `transformIndexHtml`.** The obvious choice, and the one that reads best — you get the finished HTML and the bundle on the context. It produces wrong hashes for entry chunks. Vite's import-analysis plugin substitutes `__VITE_PRELOAD__` inside its own `generateBundle`, which runs *after* `transformIndexHtml`, so an entry chunk still reads `import("./route.js"), __VITE_PRELOAD__)` at that point while the written file reads `import("./route.js"), [])`. The hash describes bytes that never ship, and the browser rejects the file with no build error at all.
|
|
221
|
+
|
|
222
|
+
**In a plain `enforce: 'post'` `generateBundle`.** Same problem. Vite places its import-analysis plugin immediately after post user plugins, so a post hook is still one step too early.
|
|
223
|
+
|
|
224
|
+
**Where this plugin does it.** During `configResolved` it moves itself after that plugin in `config.plugins`, then works in an ordinary `generateBundle`. Measured on Vite 8.2.2, hashing the entry chunk:
|
|
225
|
+
|
|
226
|
+
| Hook | Entry chunk | Matches shipped file |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| `transformIndexHtml` (post) | `io6MKsmc4G5y` | ✗ |
|
|
229
|
+
| `generateBundle` (post) | `io6MKsmc4G5y` | ✗ |
|
|
230
|
+
| after repositioning | `lvFyraHkqPN0` | ✓ |
|
|
231
|
+
| written file | `lvFyraHkqPN0` | — |
|
|
232
|
+
|
|
233
|
+
Only the entry chunk is affected, so a build without a dynamic import will not reveal the difference. As a second safeguard, every hashed file is re-hashed in `writeBundle` and the build fails if anything changed after the hash was taken.
|
|
234
|
+
|
|
235
|
+
This ordering constraint was first identified by [vite-plugin-sri3](https://github.com/yoyo930021/vite-plugin-sri3), which this plugin began as a fork of. Beyond it, this plugin adds `crossorigin` injection, a CORS pre-check with timeouts and retries for external resources, import map and manifest output for dynamically imported routes, `publicDir` resolution, and the drift check above.
|
|
236
|
+
|
|
137
237
|
## Example Project
|
|
138
238
|
|
|
139
239
|
The plugin includes an example project in the `example` directory that demonstrates its usage with a simple Vite application. To try it:
|
|
@@ -164,10 +264,11 @@ The example project shows:
|
|
|
164
264
|
- Consider `sha512` for maximum security
|
|
165
265
|
- Avoid `sha1` as it's considered cryptographically weak
|
|
166
266
|
|
|
167
|
-
2. **
|
|
168
|
-
-
|
|
169
|
-
-
|
|
170
|
-
- Use `bypassDomains` for
|
|
267
|
+
2. **External Resources**
|
|
268
|
+
- Pin a version in the URL. `unpkg.com/react@18.3.1/…` answers `max-age=31536000` and gets a hash; `unpkg.com/react@18/…` answers `max-age=60` and does not
|
|
269
|
+
- Serve your own assets with `Access-Control-Allow-Origin: *` and an immutable `Cache-Control`
|
|
270
|
+
- Use `bypassDomains` for a host you have decided not to protect — a vendor's auto-updating widget or analytics script
|
|
271
|
+
- Use `trustDomains` only for a host you control that is stable but does not say so in its headers
|
|
171
272
|
|
|
172
273
|
3. **Performance Optimization**
|
|
173
274
|
- Enable `ignoreMissingAsset` in development for faster builds
|
|
@@ -182,17 +283,21 @@ The example project shows:
|
|
|
182
283
|
|
|
183
284
|
### Common Issues
|
|
184
285
|
|
|
185
|
-
1. **
|
|
186
|
-
-
|
|
187
|
-
-
|
|
188
|
-
-
|
|
286
|
+
1. **An external resource has no integrity**
|
|
287
|
+
- Read the build log. Every skip warns and names its reason
|
|
288
|
+
- `does not declare this URL immutable` — the origin's `Cache-Control` is short, or carries `private` / `no-cache` / `no-store`. Pin a version in the URL, or see [Getting a resource hashed](#getting-a-resource-hashed)
|
|
289
|
+
- `Access-Control-Allow-Origin is absent` / `not "*"` — nothing to do at build time; `bypassDomains` silences it
|
|
290
|
+
|
|
291
|
+
2. **A local asset has no integrity**
|
|
292
|
+
- Check the file is in your build output, or in `publicDir`
|
|
293
|
+
- Verify the path in the tag matches, including `base`
|
|
294
|
+
- Enable debug mode to see per-resource decisions
|
|
189
295
|
|
|
190
|
-
|
|
191
|
-
-
|
|
192
|
-
-
|
|
193
|
-
- Check network tab for CORS headers
|
|
296
|
+
3. **The browser blocks a resource that has integrity**
|
|
297
|
+
- The bytes changed after the build. If it is your own output, a plugin ordered after this one rewrote it — the `writeBundle` drift check should have failed the build, so check the plugin order
|
|
298
|
+
- If it is external, the URL is not as immutable as its headers claim. Move it to `bypassDomains`
|
|
194
299
|
|
|
195
|
-
|
|
300
|
+
4. **Build Performance**
|
|
196
301
|
- Use `ignoreMissingAsset` if you have many external resources
|
|
197
302
|
- Disable debug mode in production
|
|
198
303
|
- Consider using a CDN for external resources
|