Skip to content

Commit b51f709

Browse files
authored
Merge pull request #312 from rails/rm-opt-in-integrity
Make integrity calculation opt-in
2 parents ae67187 + 41339ab commit b51f709

File tree

4 files changed

+70
-12
lines changed

4 files changed

+70
-12
lines changed

README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,13 @@ If you want to import local js module files from `app/javascript/src` or other s
8181
pin_all_from 'app/javascript/src', under: 'src', to: 'src'
8282

8383
# With automatic integrity calculation for enhanced security
84+
enable_integrity!
8485
pin_all_from 'app/javascript/controllers', under: 'controllers', integrity: true
8586
```
8687

8788
The `:to` parameter is only required if you want to change the destination logical import name. If you drop the :to option, you must place the :under option directly after the first parameter.
8889

89-
The `integrity: true` option automatically calculates integrity hashes for all files in the directory, providing security benefits without manual hash management.
90+
The `enable_integrity!` call enables integrity calculation globally, and `integrity: true` automatically calculates integrity hashes for all files in the directory, providing security benefits without manual hash management.
9091

9192
Allows you to:
9293

@@ -142,12 +143,15 @@ For enhanced security, importmap-rails supports [Subresource Integrity (SRI)](ht
142143

143144
### Automatic integrity for local assets
144145

145-
Starting with importmap-rails, **`integrity: true` is the default** for all pins. This automatically calculates integrity hashes for local assets served by the Rails asset pipeline:
146+
To enable automatic integrity calculation for local assets served by the Rails asset pipeline, you must first call `enable_integrity!` in your importmap configuration:
146147

147148
```ruby
148149
# config/importmap.rb
149150

150-
# These all use integrity: true by default
151+
# Enable integrity calculation globally
152+
enable_integrity!
153+
154+
# With integrity enabled, these will auto-calculate integrity hashes
151155
pin "application" # Auto-calculated integrity
152156
pin "admin", to: "admin.js" # Auto-calculated integrity
153157
pin_all_from "app/javascript/controllers", under: "controllers" # Auto-calculated integrity
@@ -163,7 +167,7 @@ This is particularly useful for:
163167
* **Bulk operations** with `pin_all_from` where calculating hashes manually would be tedious
164168
* **Development workflow** where asset contents change frequently
165169

166-
This behavior can be disabled by setting `integrity: false` or `integrity: nil`
170+
**Note:** Integrity calculation is opt-in and must be enabled with `enable_integrity!`. This behavior can be further controlled by setting `integrity: false` or `integrity: nil` on individual pins.
167171

168172
**Important for Propshaft users:** SRI support requires Propshaft 1.2+ and you must configure the integrity hash algorithm in your application:
169173

@@ -174,7 +178,7 @@ config.assets.integrity_hash_algorithm = 'sha256' # or 'sha384', 'sha512'
174178

175179
Without this configuration, integrity will be disabled by default when using Propshaft. Sprockets includes integrity support out of the box.
176180

177-
**Example output with `integrity: true`:**
181+
**Example output with `enable_integrity!` and `integrity: true`:**
178182
```json
179183
{
180184
"imports": {

lib/importmap/map.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ def self.pin_line_regexp_for(package) # :nodoc:
1212
class InvalidFile < StandardError; end
1313

1414
def initialize
15+
@integrity = false
1516
@packages, @directories = {}, {}
1617
@cache = {}
1718
end
@@ -31,6 +32,43 @@ def draw(path = nil, &block)
3132
self
3233
end
3334

35+
# Enables automatic integrity hash calculation for all pinned modules.
36+
#
37+
# When enabled, integrity values are included in the importmap JSON for all
38+
# pinned modules. For local assets served by the Rails asset pipeline,
39+
# integrity hashes are automatically calculated when +integrity: true+ is
40+
# specified. For modules with explicit integrity values, those values are
41+
# included as provided. This provides Subresource Integrity (SRI) protection
42+
# to ensure JavaScript modules haven't been tampered with.
43+
#
44+
# Clears the importmap cache when called to ensure fresh integrity hashes
45+
# are generated.
46+
#
47+
# ==== Examples
48+
#
49+
# # config/importmap.rb
50+
# enable_integrity!
51+
#
52+
# # These will now auto-calculate integrity hashes
53+
# pin "application" # integrity: true by default
54+
# pin "admin", to: "admin.js" # integrity: true by default
55+
# pin_all_from "app/javascript/lib" # integrity: true by default
56+
#
57+
# # Manual control still works
58+
# pin "no_integrity", integrity: false
59+
# pin "custom_hash", integrity: "sha384-abc123..."
60+
#
61+
# ==== Notes
62+
#
63+
# * Integrity calculation is disabled by default and must be explicitly enabled
64+
# * Requires asset pipeline support for integrity calculation (Sprockets or Propshaft 1.2+)
65+
# * For Propshaft, you must configure +config.assets.integrity_hash_algorithm+
66+
# * External CDN packages should provide their own integrity hashes
67+
def enable_integrity!
68+
clear_cache
69+
@integrity = true
70+
end
71+
3472
def pin(name, to: nil, preload: true, integrity: true)
3573
clear_cache
3674
@packages[name] = MappedFile.new(name: name, path: to || "#{name}.js", preload: preload, integrity: integrity)
@@ -216,6 +254,8 @@ def build_integrity_hash(packages, resolver:)
216254
end
217255

218256
def resolve_integrity_value(integrity, path, resolver:)
257+
return unless @integrity
258+
219259
case integrity
220260
when true
221261
resolver.asset_integrity(path) if resolver.respond_to?(:asset_integrity)

test/dummy/config/importmap.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
enable_integrity!
2+
13
pin_all_from "app/assets/javascripts"
24

35
pin "md5", to: "https://cdn.skypack.dev/md5", preload: true, integrity: false

test/importmap_test.rb

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ class ImportmapTest < ActiveSupport::TestCase
55
def setup
66
@importmap = Importmap::Map.new.tap do |map|
77
map.draw do
8+
enable_integrity!
9+
810
pin "application", preload: false, integrity: false
911
pin "editor", to: "rich_text.js", preload: false, integrity: "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb"
1012
pin "not_there", to: "nowhere.js", preload: false, integrity: "sha384-somefakehash"
@@ -39,11 +41,21 @@ def setup
3941
assert_not_includes generate_importmap_json["integrity"].values, "sha384-somefakehash"
4042
end
4143

42-
test "integrity is default" do
44+
test "integrity is not on by default" do
4345
@importmap = Importmap::Map.new.tap do |map|
4446
map.pin "application", preload: false
4547
end
4648

49+
json = generate_importmap_json
50+
assert_not json.key?("integrity")
51+
end
52+
53+
test "enable_integrity! change the map to generate integrity attribute" do
54+
@importmap = Importmap::Map.new.tap do |map|
55+
map.enable_integrity!
56+
map.pin "application", preload: false
57+
end
58+
4759
json = generate_importmap_json
4860
assert json.key?("integrity")
4961
application_path = json["imports"]["application"]
@@ -68,7 +80,9 @@ def setup
6880

6981
test "integrity: 'custom-hash' uses the provided string" do
7082
custom_hash = "sha384-customhash123"
83+
7184
@importmap = Importmap::Map.new.tap do |map|
85+
map.enable_integrity!
7286
map.pin "application", preload: false, integrity: custom_hash
7387
end
7488

@@ -122,6 +136,7 @@ def setup
122136

123137
test "importmap json includes integrity hashes from integrity: true" do
124138
importmap = Importmap::Map.new.tap do |map|
139+
map.enable_integrity!
125140
map.pin "application", integrity: true
126141
end
127142

@@ -270,13 +285,11 @@ def setup
270285
assert_equal "https://cdn.skypack.dev/tinymce", tinymce.path
271286
assert_equal 'alternate', tinymce.preload
272287

273-
# Should include packages for multiple entry points (chartkick preloads for both 'application' and 'alternate')
274288
chartkick = packages["https://cdn.skypack.dev/chartkick"]
275289
assert chartkick, "Should include chartkick package"
276290
assert_equal "chartkick", chartkick.name
277291
assert_equal ['application', 'alternate'], chartkick.preload
278292

279-
# Should include always-preloaded packages
280293
md5 = packages["https://cdn.skypack.dev/md5"]
281294
assert md5, "Should include md5 package (always preloaded)"
282295

@@ -290,15 +303,12 @@ def setup
290303
leaflet = packages["https://cdn.skypack.dev/leaflet"]
291304
assert leaflet, "Should include leaflet package for application entry point"
292305

293-
# Should include packages for 'alternate' entry point
294306
tinymce = packages["https://cdn.skypack.dev/tinymce"]
295307
assert tinymce, "Should include tinymce package for alternate entry point"
296308

297-
# Should include packages for multiple entry points
298309
chartkick = packages["https://cdn.skypack.dev/chartkick"]
299310
assert chartkick, "Should include chartkick package for both entry points"
300311

301-
# Should include always-preloaded packages
302312
md5 = packages["https://cdn.skypack.dev/md5"]
303313
assert md5, "Should include md5 package (always preloaded)"
304314

@@ -307,8 +317,8 @@ def setup
307317
end
308318

309319
test "preloaded_module_packages includes package integrity when present" do
310-
# Create a new importmap with a preloaded package that has integrity
311320
importmap = Importmap::Map.new.tap do |map|
321+
map.enable_integrity!
312322
map.pin "editor", to: "rich_text.js", preload: true, integrity: "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb"
313323
end
314324

@@ -322,6 +332,7 @@ def setup
322332

323333
test "pin with integrity: true should calculate integrity dynamically" do
324334
importmap = Importmap::Map.new.tap do |map|
335+
map.enable_integrity!
325336
map.pin "editor", to: "rich_text.js", preload: true, integrity: "sha384-OLBgp1GsljhM2TJ+sbHjaiH9txEUvgdDTAzHv2P24donTt6/529l+9Ua0vFImLlb"
326337
end
327338

@@ -370,6 +381,7 @@ def setup
370381

371382
test "pin_all_from with integrity: true should calculate integrity dynamically" do
372383
importmap = Importmap::Map.new.tap do |map|
384+
map.enable_integrity!
373385
map.pin_all_from "app/javascript/controllers", under: "controllers", integrity: true
374386
end
375387

0 commit comments

Comments
 (0)