encoded_ids 1.0.0 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 31d25d05fa2a21196dee435efb25c144e2cf90ee3837c8da263f49c22f1e8825
4
- data.tar.gz: 54af5781a3455a70b96b5033e6dc9e58b1ea0c8cc3b64a0afc4b5dbe83fd5c35
3
+ metadata.gz: cc01cbbed877c526bd59169c2f74f183ef909fb2da4beecb9354ac0b48d0723a
4
+ data.tar.gz: ae607c87436672951ebed59c753a7e4c01ebd0f249a5a1d6c196c9b5ff6cdcb3
5
5
  SHA512:
6
- metadata.gz: bebf4a30fd0307ad7514e57088cca68bfab718789ef7d5d13506bd6bf312caf3d3f02442e1d87186c3fc5e3c5c9dbbf2dbf84fd71188537af41472a1048a3583
7
- data.tar.gz: b83f9254cae57f08270f9c4da0db06fef2883b07641ad20d0fe6f00b6e25d60b2a3ca1737d2cd993b84ea144140de382268f29bfd5af9bfdcc3bf8738dbbdd6e
6
+ metadata.gz: 3a203a1bf4c38e6652ad256aa668d319991be746b0b20e187ae3d9de26851650da8e7fb20dca7aecd0a4ee019d23be1ce402f8a83d9e5c9d3ea9fd66364be1ae
7
+ data.tar.gz: d9cea9e34f8b0e2816b0a509794b613fc47ae3159eb5c6add6f7b5e2fc789da51f7c88edb55b7ab52f65bb2d69c2204be5fc6c14fc0af31b8005c8af5bb0077d
data/CHANGELOG.md CHANGED
@@ -5,12 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.1.0] - 2026-02-12
9
+
10
+ ### Added
11
+ - Per-model salt override support via `salt:` parameter in `set_public_id_prefix`
12
+ - Environment variable support (`ENV["HASHID_SALT"]`) in salt fallback chain
13
+ - Development warning when no custom salt is configured
14
+ - Enhanced security documentation in README
15
+ - Security examples in models.rb showing salt override patterns
16
+
17
+ ### Changed
18
+ - Improved salt fallback chain: config → credentials → ENV → secret_key_base
19
+ - Enhanced initializer example with better security guidance
20
+ - Updated documentation to emphasize importance of using a unique salt
21
+
22
+ ### Security
23
+ - **IMPORTANT**: Without a unique salt, hash IDs can be calculated by anyone who knows your database IDs
24
+ - Per-model salts allow for better security isolation between models
25
+ - Empty salt option (`salt: ""`) available for backward compatibility with legacy systems
26
+
8
27
  ## [1.0.0] - 2026-02-09
9
28
 
10
29
  ### Added
11
30
  - Initial stable release
12
- - `HashidEncodedIds` concern for integer primary keys
13
- - `UuidEncodedIds` concern for UUID primary keys
31
+ - `HashidIdentifiable` concern for integer primary keys
32
+ - `UuidIdentifiable` concern for UUID primary keys
14
33
  - Automatic `to_param` override for Rails URL generation
15
34
  - Overridden `find` method to accept both internal and public IDs
16
35
  - `find_by_public_id` and `find_by_public_id!` class methods
data/README.md CHANGED
@@ -158,7 +158,8 @@ Create an initializer at `config/initializers/encoded_ids.rb`:
158
158
  ```ruby
159
159
  EncodedIds.configure do |config|
160
160
  # Hashid configuration (for integer IDs)
161
- config.hashid_salt = Rails.application.credentials.dig(:hashid, :salt)
161
+ # SECURITY IMPORTANT: Set a unique salt! See Security section below.
162
+ config.hashid_salt = Rails.application.credentials.dig(:hashid, :salt) || ENV["HASHID_SALT"]
162
163
  config.hashid_min_length = 8
163
164
  config.hashid_alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
164
165
 
@@ -175,15 +176,48 @@ EncodedIds.configure do |config|
175
176
  end
176
177
  ```
177
178
 
178
- **Important:** For production, set a unique `hashid_salt` in your credentials:
179
+ ### Security: Configuring Your Salt
180
+
181
+ **IMPORTANT:** Without a unique salt, hash IDs can be calculated by anyone who knows your database IDs, making them only marginally better than exposing raw IDs.
182
+
183
+ The gem uses a fallback chain for the salt:
184
+ 1. `EncodedIds.configuration.hashid_salt` (set in initializer)
185
+ 2. `Rails.application.credentials.dig(:hashid, :salt)` (recommended)
186
+ 3. `ENV["HASHID_SALT"]`
187
+ 4. `Rails.application.secret_key_base` (not recommended - shows warning in development)
188
+
189
+ **Recommended setup:**
179
190
 
180
191
  ```bash
192
+ # Generate a unique salt
181
193
  rails credentials:edit
182
194
  ```
183
195
 
196
+ Add to your credentials:
184
197
  ```yaml
185
198
  hashid:
186
- salt: your-unique-salt-here # Generate with: SecureRandom.hex(32)
199
+ salt: <paste output of: SecureRandom.hex(32)> # e.g., "a1b2c3d4e5f6..."
200
+ ```
201
+
202
+ The initializer will automatically pick it up:
203
+ ```ruby
204
+ config.hashid_salt = Rails.application.credentials.dig(:hashid, :salt)
205
+ ```
206
+
207
+ **Per-model salt override:**
208
+
209
+ For models that need different salts (e.g., to maintain compatibility with legacy IDs):
210
+
211
+ ```ruby
212
+ class LegacyUser < ApplicationRecord
213
+ include EncodedIds::HashidIdentifiable
214
+ set_public_id_prefix :usr, salt: "" # Empty salt for backward compatibility
215
+ end
216
+
217
+ class SecureDocument < ApplicationRecord
218
+ include EncodedIds::HashidIdentifiable
219
+ set_public_id_prefix :doc, salt: Rails.application.credentials.dig(:documents, :salt)
220
+ end
187
221
  ```
188
222
 
189
223
  ## How It Works
@@ -7,11 +7,25 @@
7
7
 
8
8
  EncodedIds.configure do |config|
9
9
  # Hashid configuration (for integer IDs)
10
- # IMPORTANT: Set a unique salt in production via credentials
11
- # rails credentials:edit
12
- # Add: hashid: { salt: "your-unique-salt-here" }
13
- config.hashid_salt = Rails.application.credentials.dig(:hashid, :salt)
10
+ #
11
+ # SECURITY IMPORTANT: Set a unique salt for your application!
12
+ # Without a salt, hash IDs can be calculated by anyone who knows your database IDs.
13
+ #
14
+ # Method 1 (Recommended): Store in encrypted credentials
15
+ # rails credentials:edit
16
+ # Add: hashid: { salt: "your-unique-salt-here" }
17
+ # Generate with: SecureRandom.hex(32)
18
+ #
19
+ # Method 2: Use environment variable
20
+ # config.hashid_salt = ENV["HASHID_SALT"]
21
+ #
22
+ # Method 3: Mix of both (credentials preferred, ENV as fallback)
23
+ config.hashid_salt = Rails.application.credentials.dig(:hashid, :salt) || ENV["HASHID_SALT"]
24
+
25
+ # Minimum length of the hash portion (before prefix)
14
26
  config.hashid_min_length = 8
27
+
28
+ # Character set for hash generation (lowercase + numbers by default)
15
29
  config.hashid_alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"
16
30
 
17
31
  # Base62 alphabet (for UUID encoding)
data/examples/models.rb CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  # Integer primary key with simple prefix
6
6
  class User < ApplicationRecord
7
- include EncodedIds::HashidEncodedIds
7
+ include EncodedIds::HashidIdentifiable
8
8
  set_public_id_prefix :usr
9
9
  end
10
10
  # user.public_id => "usr_k5qx9z"
@@ -12,14 +12,14 @@ end
12
12
 
13
13
  # Integer primary key with longer hash for high-volume tables
14
14
  class Event < ApplicationRecord
15
- include EncodedIds::HashidEncodedIds
15
+ include EncodedIds::HashidIdentifiable
16
16
  set_public_id_prefix :evt, min_hash_length: 12
17
17
  end
18
18
  # event.public_id => "evt_x5qp9z2m8n4k"
19
19
 
20
20
  # Integer primary key with compositional prefix
21
21
  class Intel::Tool::PhoneNumber < ApplicationRecord
22
- include EncodedIds::HashidEncodedIds
22
+ include EncodedIds::HashidIdentifiable
23
23
  add_public_id_segment :int
24
24
  add_public_id_segment :tool
25
25
  add_public_id_segment :phn
@@ -28,22 +28,36 @@ end
28
28
 
29
29
  # UUID primary key
30
30
  class Organization < ApplicationRecord
31
- include EncodedIds::UuidEncodedIds
31
+ include EncodedIds::UuidIdentifiable
32
32
  set_public_id_prefix "org"
33
33
  end
34
34
  # org.public_id => "org_4k8xJm2pN9qW"
35
35
 
36
36
  # UUID primary key with different prefix
37
37
  class Team < ApplicationRecord
38
- include EncodedIds::UuidEncodedIds
38
+ include EncodedIds::UuidIdentifiable
39
39
  set_public_id_prefix "team"
40
40
  end
41
41
  # team.public_id => "team_7n2kLp4xMq8R"
42
42
 
43
43
  # Override per-model to include prefix in routes (Stripe style)
44
44
  class ApiKey < ApplicationRecord
45
- include EncodedIds::HashidEncodedIds
45
+ include EncodedIds::HashidIdentifiable
46
46
  set_public_id_prefix :key, use_prefix_in_routes: true
47
47
  end
48
48
  # api_key.public_id => "key_abc123"
49
49
  # api_key.to_param => "key_abc123" (includes prefix)
50
+
51
+ # Security: Custom salt for specific model
52
+ class SecureDocument < ApplicationRecord
53
+ include EncodedIds::HashidIdentifiable
54
+ set_public_id_prefix :doc, salt: Rails.application.credentials.dig(:documents, :salt)
55
+ end
56
+ # Uses a different salt than the global default for added security
57
+
58
+ # Security: Empty salt for backward compatibility with legacy IDs
59
+ class LegacyUser < ApplicationRecord
60
+ include EncodedIds::HashidIdentifiable
61
+ set_public_id_prefix :leg, salt: ""
62
+ end
63
+ # Maintains compatibility with existing hash IDs generated without a salt
@@ -54,13 +54,19 @@ module EncodedIds
54
54
 
55
55
  module ClassMethods
56
56
  # Simple approach: set the full prefix directly
57
- def set_public_id_prefix(prefix, min_hash_length: 8, use_prefix_in_routes: nil)
57
+ # Options:
58
+ # - min_hash_length: Minimum length of the encoded hash (default: 8)
59
+ # - use_prefix_in_routes: Include prefix in to_param URLs (default: nil, inherits from config)
60
+ # - salt: Custom salt for this model (default: nil, uses global config)
61
+ def set_public_id_prefix(prefix, min_hash_length: 8, use_prefix_in_routes: nil, salt: nil)
58
62
  self.public_id_prefix = prefix.to_s.downcase
59
63
  self.hashid_min_length = min_hash_length
60
64
  self.use_prefix_in_routes = use_prefix_in_routes
61
65
 
62
- # Configure hashid-rails for this model with custom length
63
- hashid_config(min_hash_length: min_hash_length)
66
+ # Configure hashid-rails for this model with custom length and optional salt
67
+ config_options = { min_hash_length: min_hash_length }
68
+ config_options[:salt] = salt if salt
69
+ hashid_config(config_options)
64
70
  end
65
71
 
66
72
  # Compositional approach: add segments that get joined with underscores
@@ -5,9 +5,41 @@ module EncodedIds
5
5
  initializer "encoded_ids.configure_hashid_rails" do
6
6
  # Configure hashid-rails with gem settings
7
7
  Hashid::Rails.configure do |config|
8
- config.salt = EncodedIds.configuration.hashid_salt ||
9
- Rails.application.credentials.dig(:hashid, :salt) ||
10
- Rails.application.secret_key_base
8
+ # Project-wide salt fallback chain:
9
+ # 1. Explicitly configured via EncodedIds.configure
10
+ # 2. Rails credentials hashid.salt
11
+ # 3. ENV["HASHID_SALT"]
12
+ # 4. Rails secret_key_base (not recommended for production)
13
+ salt = EncodedIds.configuration.hashid_salt ||
14
+ Rails.application.credentials.dig(:hashid, :salt) ||
15
+ ENV["HASHID_SALT"] ||
16
+ Rails.application.secret_key_base
17
+
18
+ # Warn in development if using secret_key_base as salt
19
+ if Rails.env.development? &&
20
+ EncodedIds.configuration.hashid_salt.nil? &&
21
+ Rails.application.credentials.dig(:hashid, :salt).nil? &&
22
+ ENV["HASHID_SALT"].nil?
23
+ Rails.logger.warn <<~WARNING
24
+ [EncodedIds] No hashid_salt configured. Using secret_key_base as fallback.
25
+
26
+ For better security, set a unique salt in config/credentials.yml.enc:
27
+ hashid:
28
+ salt: #{SecureRandom.hex(32)}
29
+
30
+ Or via environment variable:
31
+ HASHID_SALT=#{SecureRandom.hex(32)}
32
+
33
+ Or in config/initializers/encoded_ids.rb:
34
+ EncodedIds.configure do |config|
35
+ config.hashid_salt = Rails.application.credentials.dig(:hashid, :salt) || ENV["HASHID_SALT"]
36
+ end
37
+
38
+ Using the default salt makes hash IDs predictable if someone knows your model IDs.
39
+ WARNING
40
+ end
41
+
42
+ config.salt = salt
11
43
  config.min_hash_length = EncodedIds.configuration.hashid_min_length
12
44
  config.alphabet = EncodedIds.configuration.hashid_alphabet
13
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EncodedIds
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: encoded_ids
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jasper Mayone