encoded_id 1.0.0.rc6 → 1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -4
- data/README.md +44 -33
- data/context/encoded_id.md +229 -75
- data/lib/encoded_id/alphabet.rb +2 -0
- data/lib/encoded_id/blocklist.rb +17 -7
- data/lib/encoded_id/encoders/base_configuration.rb +145 -0
- data/lib/encoded_id/encoders/{hash_id.rb → hashid.rb} +53 -57
- data/lib/encoded_id/encoders/hashid_configuration.rb +33 -0
- data/lib/encoded_id/encoders/{hash_id_consistent_shuffle.rb → hashid_consistent_shuffle.rb} +4 -4
- data/lib/encoded_id/encoders/{hash_id_ordinal_alphabet_separator_guards.rb → hashid_ordinal_alphabet_separator_guards.rb} +28 -54
- data/lib/encoded_id/encoders/{hash_id_salt.rb → hashid_salt.rb} +3 -3
- data/lib/encoded_id/encoders/my_sqids.rb +5 -16
- data/lib/encoded_id/encoders/sqids.rb +25 -11
- data/lib/encoded_id/encoders/sqids_configuration.rb +17 -0
- data/lib/encoded_id/encoders/sqids_with_blocklist_mode.rb +52 -0
- data/lib/encoded_id/hex_representation.rb +6 -9
- data/lib/encoded_id/reversible_id.rb +56 -138
- data/lib/encoded_id/version.rb +1 -2
- data/lib/encoded_id.rb +16 -19
- metadata +27 -10
- data/lib/encoded_id/encoders/base.rb +0 -71
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ccfa7987d3a108bfce0268467b8ae40793226e98f01fa2c331bc1f642026f47b
|
|
4
|
+
data.tar.gz: b4cb2429a8031f0e576f242550397b4683e302a184d57f0c85b870da502e181e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db4ae0c6d8477ea3ab5ada1bcbd959b18420c1aa1be54ec747898d298f16e6dd6a2090ff4a18008fe1711d3641c158bf9a8c1da23a33663be7fbc17a68079409
|
|
7
|
+
data.tar.gz: 4e731df7f992cf6935012cabffc7c6d4923de5db46059c123780d0f6a1dfbe329844ddf074580c67ec1a16101afb9c315175049045bcf5375e6666ae2707fa0b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
- nothing yet
|
|
4
|
+
|
|
5
|
+
## [1.0.0] - 2025-11-21
|
|
6
|
+
|
|
7
|
+
- First stable release!
|
|
8
|
+
|
|
9
|
+
**Important!!: `:sqids` are not compatible with `:hashids`, DO NOT CHANGE FROM ONE TO THE OTHER AFTER GOING LIVE.**
|
|
10
|
+
|
|
11
|
+
### Fixed (Rails integration)
|
|
12
|
+
|
|
13
|
+
- Ensure finder methods correctly override their ActiveRecord counterparts
|
|
14
|
+
- Warn when ActiveRecord integration used in model that doesn't use `id` as primary key
|
|
15
|
+
|
|
16
|
+
## [1.0.0.rc7] - 2025-11-19
|
|
4
17
|
|
|
5
18
|
### Breaking changes
|
|
6
19
|
|
|
7
|
-
- `ReversibleId` now no longer downcases the encodedid input string by default on decode, ie the `decode` option `downcase` is now `false`. In a future release the `downcase` option will be removed.
|
|
8
|
-
- The default encoding engine is now `:sqids` to reflect the official "deprecated" status of `Hashid`s (see https://sqids.org/faq#why-hashids)
|
|
20
|
+
- `ReversibleId` now no longer downcases the encodedid input string by default on decode, ie the `decode` option `downcase` is now `false`. This can be configured via the Rails configuration `downcase_on_decode` option. In a future release the `downcase` option will be removed.
|
|
21
|
+
- The default encoding engine is now `:sqids` to reflect the official "deprecated" status of `Hashid`s (see https://sqids.org/faq#why-hashids)
|
|
9
22
|
- Ruby < 3.2 support dropped. The minimum supported Ruby version is now 3.2.0
|
|
23
|
+
- `Encoders` classes no longer expose `encode_hex` and `decode_hex` as they worked differently to the similarly named methods on ReversibleId
|
|
24
|
+
- Rails generator now needs you to specify which encoding algorithm you are using
|
|
10
25
|
|
|
11
|
-
|
|
26
|
+
### Added (Rails integration)
|
|
27
|
+
|
|
28
|
+
- `encoded_id_options` class method to override `encoded_id` configuration on a model by model basis
|
|
29
|
+
- Blocklist application "modes". User can decide if the blocklist checks should be applied in situations
|
|
30
|
+
where very long encoded IDs are likely and hence block word collisions are undesirably likely.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- Blocklists are now pre-filtered to the encoder alphabet to avoid testing words which cannot be created by that alphabet anyway.
|
|
35
|
+
- Updates to inline RBS type signatures
|
|
36
|
+
- Documentation updates
|
|
12
37
|
|
|
13
38
|
## [1.0.0.rc6] - 2025-11-17
|
|
14
39
|
|
data/README.md
CHANGED
|
@@ -7,28 +7,58 @@
|
|
|
7
7
|
|
|
8
8
|
`encoded_id-rails` is a Rails integration that provides additional features for using `encoded_id` with ActiveRecord models.
|
|
9
9
|
|
|
10
|
+
It's one of the most, if not _the_ most, feature complete gem of its kind!
|
|
11
|
+
|
|
10
12
|
👉 **Full documentation available at [encoded-id.onrender.com](https://encoded-id.onrender.com)**
|
|
11
13
|
|
|
14
|
+
## Key Features
|
|
15
|
+
|
|
16
|
+
* 🔄 **Reversible** - Encoded IDs can be decoded back to the original values
|
|
17
|
+
* 👥 **Multiple IDs** - Encode multiple numeric IDs in one string
|
|
18
|
+
* 🚀 **Choose your encoding** - Supports `Sqids` (default) and `Hashids`, or use your own custom encoder
|
|
19
|
+
* 👓 **Human-readable** - Character grouping & character mappings of easily confused characters for better readability
|
|
20
|
+
* 🔡 **Custom alphabets** - Use your preferred character set, or a provided default
|
|
21
|
+
* 🚗 **Performance** - Uses an optimized `Hashids` encoder (compared to `hashids` gem) for better performance and less memory usage, and have pushed performance improvements to `Sqids` as well
|
|
22
|
+
* 🤬 **Blocklist filtering** - Built-in word blocklist support with configurable modes and optional default lists
|
|
23
|
+
|
|
24
|
+
### Rails Integration Features
|
|
25
|
+
|
|
26
|
+
* 🏷️ **ActiveRecord integration** - Use with ActiveRecord models
|
|
27
|
+
* 🔑 **Per-model configuration** - Use custom salt and encoding settings per model
|
|
28
|
+
* 💅 **Slugged IDs** - URL-friendly slugs like `my-product--p5w9-z27j`
|
|
29
|
+
* 🔖 **Annotated IDs** - Model type indicators like `user_p5w9-z27j` (default behavior)
|
|
30
|
+
* 🔍 **Finder methods** - `find_by_encoded_id`, `find_by_encoded_id!`, `find_all_by_encoded_id`, `where_encoded_id`
|
|
31
|
+
* 🛣️ **URL params** - `to_param` automatically uses encoded IDs
|
|
32
|
+
* 🔒 **Safe defaults** - Limits on encoded ID lengths to prevent CPU and memory-intensive encode/decodes when used in URLs
|
|
33
|
+
* 💾 **Persistence** - Optional database persistence for efficient lookups
|
|
34
|
+
|
|
35
|
+
|
|
12
36
|
## Quick Example
|
|
13
37
|
|
|
14
38
|
```ruby
|
|
15
|
-
|
|
39
|
+
# Using Hashids encoder (requires salt)
|
|
40
|
+
coder = ::EncodedId::ReversibleId.hashid(salt: "my-salt")
|
|
16
41
|
coder.encode(123)
|
|
17
|
-
# => "
|
|
42
|
+
# => "m3pm-8anj"
|
|
18
43
|
|
|
19
44
|
# The encoded strings are reversible
|
|
20
|
-
coder.decode("
|
|
45
|
+
coder.decode("m3pm-8anj")
|
|
21
46
|
# => [123]
|
|
22
47
|
|
|
23
48
|
# Supports encoding multiple IDs at once
|
|
24
49
|
coder.encode([78, 45])
|
|
25
|
-
# => "
|
|
50
|
+
# => "ny9y-sd7p"
|
|
51
|
+
|
|
52
|
+
# Using Sqids encoder (default, no salt required)
|
|
53
|
+
sqids_coder = ::EncodedId::ReversibleId.sqids
|
|
54
|
+
sqids_coder.encode(123)
|
|
55
|
+
# => (encoded value varies)
|
|
26
56
|
|
|
27
57
|
# Can also be used with ActiveRecord models
|
|
28
58
|
class User < ApplicationRecord
|
|
29
59
|
include EncodedId::Rails::Model
|
|
30
|
-
|
|
31
|
-
# Optional slug for
|
|
60
|
+
|
|
61
|
+
# Optional: define method to provide slug for encoded ID
|
|
32
62
|
def name_for_encoded_id_slug
|
|
33
63
|
full_name
|
|
34
64
|
end
|
|
@@ -40,28 +70,6 @@ user.encoded_id # => "user_p5w9-z27j"
|
|
|
40
70
|
user.slugged_encoded_id # => "bob-smith--user_p5w9-z27j"
|
|
41
71
|
```
|
|
42
72
|
|
|
43
|
-
## Key Features
|
|
44
|
-
|
|
45
|
-
* 🔄 **Reversible** - Encoded IDs can be decoded back to the original values
|
|
46
|
-
* 👥 **Multiple IDs** - Encode multiple numeric IDs in one string
|
|
47
|
-
* 🚀 **Choose your encoding** - Supports `Hashids` and `Sqids` out of the box, or use your own custom encoder
|
|
48
|
-
* 👓 **Human-readable** - Character grouping & character mappings of easily confused characters for better readability
|
|
49
|
-
* 🔡 **Custom alphabets** - Use your preferred character set, or a provided default
|
|
50
|
-
* 🚗 **Performance** - Uses an optimized `Hashids` encoder (compared to `hashids` gem) for better performance and less memory usage, and have pushed performance improvements to `Sqids` as well
|
|
51
|
-
* 🤬 **Profanity blocking** - Built-in word blocklist support and optional default lists
|
|
52
|
-
|
|
53
|
-
### Rails Integration Features
|
|
54
|
-
|
|
55
|
-
* 🏷️ **ActiveRecord integration** - Use with ActiveRecord models
|
|
56
|
-
* 🔑 **Per-model salt** - Use a custom salt for encoding per model
|
|
57
|
-
* 💅 **Slugged IDs** - URL-friendly slugs like `my-product--p5w9-z27j`
|
|
58
|
-
* 🔖 **Annotated IDs** - Model type indicators like `user_p5w9-z27j`
|
|
59
|
-
* 🔍 **Finder methods** - Find records using encoded IDs
|
|
60
|
-
* 🛣️ **URL params** - `to_param` with encoded IDs
|
|
61
|
-
* 🔒 **Safe defaults**: Limits on encoded ID lengths to prevent CPU and memory-intensive encode/decodes eg when used in URLs
|
|
62
|
-
* 💾 **Persistence** - Optional database persistence for efficient lookups
|
|
63
|
-
|
|
64
|
-
|
|
65
73
|
## Standalone Gem
|
|
66
74
|
|
|
67
75
|
|
|
@@ -91,6 +99,10 @@ See the [Rails Integration](https://encoded-id.onrender.com/docs/encoded_id_rail
|
|
|
91
99
|
|
|
92
100
|
**Encoded IDs are not secure**. They are meant to provide obfuscation, not encryption. Do not use them as a security mechanism.
|
|
93
101
|
|
|
102
|
+
As of version 1.0.0, `Sqids` is the default encoder. `Hashids` is still supported but is officially deprecated by the Hashids project in favor of Sqids.
|
|
103
|
+
|
|
104
|
+
Read more about the security implications: [Hashids expose salt value](https://www.sjoerdlangkemper.nl/2023/11/25/hashids-expose-salt-value/) (note: this specifically applies to Hashids encoder)
|
|
105
|
+
|
|
94
106
|
## Compare to Alternate Gems
|
|
95
107
|
|
|
96
108
|
- [prefixed_ids](https://github.com/excid3/prefixed_ids)
|
|
@@ -105,11 +117,10 @@ For a detailed comparison, see the [Compared to Other Gems](https://encoded-id.o
|
|
|
105
117
|
|
|
106
118
|
Visit [encoded-id.onrender.com](https://encoded-id.onrender.com) for comprehensive documentation including:
|
|
107
119
|
|
|
108
|
-
- [EncodedId Core
|
|
109
|
-
- [Rails
|
|
110
|
-
- [
|
|
111
|
-
- [
|
|
112
|
-
- [Advanced Topics](https://encoded-id.onrender.com/docs/advanced-topics)
|
|
120
|
+
- [EncodedId Core Guide](https://encoded-id.onrender.com/docs/encoded_id/) - Installation, configuration, examples, and advanced topics
|
|
121
|
+
- [EncodedId Rails Guide](https://encoded-id.onrender.com/docs/encoded_id_rails/) - Rails integration, configuration, and examples
|
|
122
|
+
- [EncodedId Core API Reference](https://encoded-id.onrender.com/docs/encoded_id/api)
|
|
123
|
+
- [Rails Integration API Reference](https://encoded-id.onrender.com/docs/encoded_id_rails/api)
|
|
113
124
|
|
|
114
125
|
## Development
|
|
115
126
|
|
data/context/encoded_id.md
CHANGED
|
@@ -14,6 +14,25 @@
|
|
|
14
14
|
- **Performance Optimized**: Uses an optimized HashIds implementation for better performance
|
|
15
15
|
- **Profanity Protection**: Built-in blocklist support to prevent offensive words in generated IDs
|
|
16
16
|
- **Customizable**: Configurable alphabets, lengths, and formatting options
|
|
17
|
+
- **Blocklist Modes**: Three modes for controlling blocklist checking performance
|
|
18
|
+
|
|
19
|
+
## Quick Reference
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# Sqids encoder (default, no salt required)
|
|
23
|
+
coder = EncodedId::ReversibleId.sqids(min_length: 10)
|
|
24
|
+
id = coder.encode(123) # => "p5w9-z27j-k8"
|
|
25
|
+
nums = coder.decode(id) # => [123]
|
|
26
|
+
|
|
27
|
+
# Hashids encoder (requires salt)
|
|
28
|
+
coder = EncodedId::ReversibleId.hashid(salt: "my-salt", min_length: 8)
|
|
29
|
+
id = coder.encode([78, 45]) # => "z2j7-0dmw"
|
|
30
|
+
nums = coder.decode(id) # => [78, 45]
|
|
31
|
+
|
|
32
|
+
# UUID encoding (experimental)
|
|
33
|
+
hex_id = coder.encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
|
|
34
|
+
uuid = coder.decode_hex(hex_id).first
|
|
35
|
+
```
|
|
17
36
|
|
|
18
37
|
## Core API
|
|
19
38
|
|
|
@@ -21,30 +40,65 @@
|
|
|
21
40
|
|
|
22
41
|
The main class for encoding and decoding IDs.
|
|
23
42
|
|
|
24
|
-
####
|
|
43
|
+
#### Factory Methods (Recommended)
|
|
44
|
+
|
|
45
|
+
Factory methods provide the cleanest way to create encoders:
|
|
25
46
|
|
|
26
47
|
```ruby
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
48
|
+
# Sqids encoder (default, no salt required)
|
|
49
|
+
coder = EncodedId::ReversibleId.sqids(
|
|
50
|
+
min_length: 10,
|
|
51
|
+
blocklist: ["bad", "words"]
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Hashids encoder (requires salt)
|
|
55
|
+
coder = EncodedId::ReversibleId.hashid(
|
|
56
|
+
salt: "my-salt",
|
|
57
|
+
min_length: 8,
|
|
58
|
+
blocklist: ["bad", "words"]
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Both factory methods accept all configuration options described below.
|
|
63
|
+
|
|
64
|
+
#### Constructor (Alternative)
|
|
65
|
+
|
|
66
|
+
You can also use the constructor with explicit configuration objects:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Using Sqids configuration
|
|
70
|
+
config = EncodedId::Encoders::SqidsConfiguration.new(
|
|
71
|
+
min_length: 8, # Minimum length of encoded string
|
|
30
72
|
split_at: 4, # Split encoded string every X characters
|
|
31
73
|
split_with: "-", # Character to split with
|
|
32
74
|
alphabet: EncodedId::Alphabet.modified_crockford,
|
|
33
75
|
hex_digit_encoding_group_size: 4,
|
|
34
76
|
max_length: 128, # Maximum length limit
|
|
35
77
|
max_inputs_per_id: 32, # Maximum IDs to encode together
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
blocklist: nil, # Words to prevent in IDs
|
|
79
|
+
blocklist_mode: :length_threshold, # :always, :length_threshold, or :raise_if_likely
|
|
80
|
+
blocklist_max_length: 32 # Max length for :length_threshold mode
|
|
81
|
+
)
|
|
82
|
+
coder = EncodedId::ReversibleId.new(config)
|
|
83
|
+
|
|
84
|
+
# Using Hashids configuration (requires salt)
|
|
85
|
+
config = EncodedId::Encoders::HashidConfiguration.new(
|
|
86
|
+
salt: "my-salt", # Required for Hashids (min 4 chars)
|
|
87
|
+
min_length: 8,
|
|
88
|
+
# ... other options same as above
|
|
38
89
|
)
|
|
90
|
+
coder = EncodedId::ReversibleId.new(config)
|
|
39
91
|
```
|
|
40
92
|
|
|
93
|
+
**Note**: As of v1.0.0, the default encoder is `:sqids`. For backwards compatibility with pre-v1 versions, use `ReversibleId.hashid()`.
|
|
94
|
+
|
|
41
95
|
#### Key Methods
|
|
42
96
|
|
|
43
97
|
##### encode(values)
|
|
44
98
|
Encodes one or more integer IDs into an obfuscated string.
|
|
45
99
|
|
|
46
100
|
```ruby
|
|
47
|
-
coder = EncodedId::ReversibleId.
|
|
101
|
+
coder = EncodedId::ReversibleId.sqids
|
|
48
102
|
|
|
49
103
|
# Single ID
|
|
50
104
|
coder.encode(123) # => "p5w9-z27j"
|
|
@@ -53,17 +107,22 @@ coder.encode(123) # => "p5w9-z27j"
|
|
|
53
107
|
coder.encode([78, 45]) # => "z2j7-0dmw"
|
|
54
108
|
```
|
|
55
109
|
|
|
56
|
-
##### decode(encoded_id, downcase:
|
|
110
|
+
##### decode(encoded_id, downcase: false)
|
|
57
111
|
Decodes an encoded string back to original IDs.
|
|
58
112
|
|
|
59
113
|
```ruby
|
|
60
114
|
coder.decode("p5w9-z27j") # => [123]
|
|
61
115
|
coder.decode("z2j7-0dmw") # => [78, 45]
|
|
62
116
|
|
|
63
|
-
#
|
|
64
|
-
coder.decode("p5w9-z27J") # => [
|
|
117
|
+
# Case-sensitive by default (v1.0.0+)
|
|
118
|
+
coder.decode("p5w9-z27J") # => [] (case doesn't match)
|
|
119
|
+
|
|
120
|
+
# For case-insensitive matching (pre-v1 behavior)
|
|
121
|
+
coder.decode("p5w9-z27J", downcase: true) # => [123]
|
|
65
122
|
```
|
|
66
123
|
|
|
124
|
+
**Note**: As of v1.0.0, decoding is case-sensitive by default (`downcase: false`). Set `downcase: true` for backwards compatibility.
|
|
125
|
+
|
|
67
126
|
##### encode_hex(hex_strings) (Experimental)
|
|
68
127
|
Encodes hexadecimal strings (like UUIDs).
|
|
69
128
|
|
|
@@ -73,19 +132,19 @@ coder.encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
|
|
|
73
132
|
# => "5jjy-c8d9-hxp2-qsve-rgh9-rxnt-7nb5-tve7-bf84-vr"
|
|
74
133
|
|
|
75
134
|
# With larger group size for shorter output
|
|
76
|
-
coder = EncodedId::ReversibleId.
|
|
77
|
-
salt: "my-salt",
|
|
78
|
-
hex_digit_encoding_group_size: 32
|
|
79
|
-
)
|
|
135
|
+
coder = EncodedId::ReversibleId.sqids(hex_digit_encoding_group_size: 32)
|
|
80
136
|
coder.encode_hex("9a566b8b-8618-42ab-8db7-a5a0276401fd")
|
|
81
137
|
# => "vr7m-qra8-m5y6-dkgj-5rqr-q44e-gp4a-52"
|
|
82
138
|
```
|
|
83
139
|
|
|
84
|
-
##### decode_hex(encoded_id, downcase:
|
|
140
|
+
##### decode_hex(encoded_id, downcase: false) (Experimental)
|
|
85
141
|
Decodes back to hexadecimal strings.
|
|
86
142
|
|
|
87
143
|
```ruby
|
|
88
144
|
coder.decode_hex("w72a-y0az") # => ["10f8c"]
|
|
145
|
+
|
|
146
|
+
# For case-insensitive decoding (pre-v1 behavior)
|
|
147
|
+
coder.decode_hex("W72A-Y0AZ", downcase: true) # => ["10f8c"]
|
|
89
148
|
```
|
|
90
149
|
|
|
91
150
|
### EncodedId::Alphabet
|
|
@@ -116,62 +175,127 @@ alphabet = EncodedId::Alphabet.new(
|
|
|
116
175
|
|
|
117
176
|
# Greek alphabet example
|
|
118
177
|
alphabet = EncodedId::Alphabet.new("αβγδεζηθικλμνξοπρστυφχψω")
|
|
119
|
-
coder = EncodedId::ReversibleId.
|
|
178
|
+
coder = EncodedId::ReversibleId.sqids(alphabet: alphabet)
|
|
120
179
|
coder.encode(123) # => "θεαψ-ζκυο"
|
|
121
180
|
```
|
|
122
181
|
|
|
182
|
+
### EncodedId::Blocklist
|
|
183
|
+
|
|
184
|
+
Class for managing profanity/word blocklists.
|
|
185
|
+
|
|
186
|
+
#### Predefined Blocklists
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
# Empty blocklist (no filtering)
|
|
190
|
+
EncodedId::Blocklist.empty
|
|
191
|
+
|
|
192
|
+
# Minimal blocklist (~50 common profane words)
|
|
193
|
+
EncodedId::Blocklist.minimal
|
|
194
|
+
|
|
195
|
+
# Full Sqids default blocklist (comprehensive)
|
|
196
|
+
EncodedId::Blocklist.sqids_blocklist
|
|
197
|
+
|
|
198
|
+
# Use in configuration
|
|
199
|
+
coder = EncodedId::ReversibleId.sqids(
|
|
200
|
+
blocklist: EncodedId::Blocklist.minimal
|
|
201
|
+
)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
#### Custom Blocklists
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
# From array
|
|
208
|
+
blocklist = EncodedId::Blocklist.new(["bad", "offensive", "words"])
|
|
209
|
+
|
|
210
|
+
# Merge blocklists
|
|
211
|
+
combined = EncodedId::Blocklist.minimal.merge(
|
|
212
|
+
EncodedId::Blocklist.new(["custom", "words"])
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
# Filter for specific alphabet (automatic with configuration)
|
|
216
|
+
filtered = blocklist.filter_for_alphabet(EncodedId::Alphabet.modified_crockford)
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Note**: Blocklists are automatically filtered to only include words possible with your configured alphabet. This optimization improves performance.
|
|
220
|
+
|
|
123
221
|
## Configuration Options
|
|
124
222
|
|
|
125
223
|
### Basic Options
|
|
126
224
|
|
|
127
|
-
- **
|
|
128
|
-
- **length**: Minimum length of encoded string (default: 8)
|
|
225
|
+
- **min_length**: Minimum length of encoded string (default: 8)
|
|
129
226
|
- **max_length**: Maximum allowed length (default: 128) to prevent DoS attacks
|
|
130
227
|
- **max_inputs_per_id**: Maximum IDs encodable together (default: 32)
|
|
228
|
+
- **hex_digit_encoding_group_size**: Group size for hex encoding (default: 4)
|
|
131
229
|
|
|
132
230
|
### Encoder Selection
|
|
133
231
|
|
|
134
232
|
```ruby
|
|
135
|
-
#
|
|
136
|
-
coder = EncodedId::ReversibleId.
|
|
233
|
+
# Sqids encoder (default, no salt required)
|
|
234
|
+
coder = EncodedId::ReversibleId.sqids
|
|
137
235
|
|
|
138
|
-
#
|
|
139
|
-
coder = EncodedId::ReversibleId.
|
|
236
|
+
# Hashids encoder (requires salt - minimum 4 characters)
|
|
237
|
+
coder = EncodedId::ReversibleId.hashid(salt: "my-salt-minimum-4-chars")
|
|
140
238
|
```
|
|
141
239
|
|
|
142
|
-
**Important**:
|
|
240
|
+
**Important**:
|
|
241
|
+
- As of v1.0.0, `:sqids` is the default encoder
|
|
242
|
+
- **Sqids**: No salt required, automatically avoids blocklisted words via iteration
|
|
243
|
+
- **Hashids**: Salt required (min 4 chars), raises exception if blocklisted word appears
|
|
244
|
+
- HashIds and Sqids produce different encodings and are **not compatible**
|
|
245
|
+
- Do NOT change encoders after going to production with existing encoded IDs
|
|
143
246
|
|
|
144
|
-
###
|
|
247
|
+
### Blocklist Configuration
|
|
248
|
+
|
|
249
|
+
#### Blocklist Modes
|
|
250
|
+
|
|
251
|
+
Control how blocklist checking behaves to balance performance and safety:
|
|
145
252
|
|
|
146
253
|
```ruby
|
|
147
|
-
#
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
254
|
+
# :length_threshold (default) - Check blocklist only until encoded length reaches blocklist_max_length
|
|
255
|
+
# Best for most use cases - prevents performance issues with very long IDs
|
|
256
|
+
coder = EncodedId::ReversibleId.sqids(
|
|
257
|
+
blocklist: EncodedId::Blocklist.minimal,
|
|
258
|
+
blocklist_mode: :length_threshold,
|
|
259
|
+
blocklist_max_length: 32 # Stop checking after 32 characters
|
|
152
260
|
)
|
|
153
|
-
coder.encode(123) # => "p5w.9z2.7j"
|
|
154
261
|
|
|
155
|
-
#
|
|
156
|
-
|
|
262
|
+
# :always - Always check blocklist regardless of encoded length
|
|
263
|
+
# Can be slow for long IDs or large blocklists
|
|
264
|
+
coder = EncodedId::ReversibleId.hashid(
|
|
157
265
|
salt: "my-salt",
|
|
158
|
-
|
|
266
|
+
blocklist: ["bad", "words"],
|
|
267
|
+
blocklist_mode: :always
|
|
159
268
|
)
|
|
160
|
-
|
|
269
|
+
|
|
270
|
+
# :raise_if_likely - Raise error at configuration time if settings likely cause blocklist collisions
|
|
271
|
+
# Prevents configurations that would cause performance issues
|
|
272
|
+
coder = EncodedId::ReversibleId.sqids(
|
|
273
|
+
min_length: 8,
|
|
274
|
+
blocklist: ["bad", "words"],
|
|
275
|
+
blocklist_mode: :raise_if_likely
|
|
276
|
+
)
|
|
277
|
+
# Raises InvalidConfigurationError if min_length > blocklist_max_length
|
|
161
278
|
```
|
|
162
279
|
|
|
163
|
-
|
|
280
|
+
**Blocklist Behavior by Encoder**:
|
|
281
|
+
- **Sqids**: Iteratively regenerates to avoid blocklisted words (may impact encoding performance)
|
|
282
|
+
- **Hashids**: Raises `EncodedId::BlocklistError` if a blocklisted word appears
|
|
283
|
+
|
|
284
|
+
**Recommendation**: Use `:length_threshold` mode (default) for best balance of performance and safety.
|
|
285
|
+
|
|
286
|
+
### Formatting Options
|
|
164
287
|
|
|
165
288
|
```ruby
|
|
166
|
-
#
|
|
167
|
-
coder = EncodedId::ReversibleId.
|
|
168
|
-
|
|
169
|
-
|
|
289
|
+
# Custom splitting
|
|
290
|
+
coder = EncodedId::ReversibleId.sqids(
|
|
291
|
+
split_at: 3, # Group every 3 chars
|
|
292
|
+
split_with: "." # Use dots
|
|
170
293
|
)
|
|
294
|
+
coder.encode(123) # => "p5w.9z2.7j"
|
|
171
295
|
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
#
|
|
296
|
+
# No splitting
|
|
297
|
+
coder = EncodedId::ReversibleId.sqids(split_at: nil)
|
|
298
|
+
coder.encode(123) # => "p5w9z27j"
|
|
175
299
|
```
|
|
176
300
|
|
|
177
301
|
## Exception Handling
|
|
@@ -183,14 +307,15 @@ coder = EncodedId::ReversibleId.new(
|
|
|
183
307
|
| `EncodedId::EncodedIdFormatError` | Invalid encoded ID format |
|
|
184
308
|
| `EncodedId::EncodedIdLengthError` | Encoded ID exceeds max_length |
|
|
185
309
|
| `EncodedId::InvalidInputError` | Invalid input (negative integers, too many inputs) |
|
|
186
|
-
| `EncodedId::SaltError` | Invalid salt (too short) |
|
|
310
|
+
| `EncodedId::SaltError` | Invalid salt (too short, only for Hashids) |
|
|
311
|
+
| `EncodedId::BlocklistError` | Generated ID contains blocklisted word (Hashids only) |
|
|
187
312
|
|
|
188
313
|
## Usage Examples
|
|
189
314
|
|
|
190
315
|
### Basic Usage
|
|
191
316
|
```ruby
|
|
192
|
-
# Initialize
|
|
193
|
-
coder = EncodedId::ReversibleId.
|
|
317
|
+
# Initialize with Sqids (no salt needed)
|
|
318
|
+
coder = EncodedId::ReversibleId.sqids
|
|
194
319
|
|
|
195
320
|
# Encode/decode cycle
|
|
196
321
|
encoded = coder.encode(123) # => "p5w9-z27j"
|
|
@@ -205,44 +330,79 @@ encoded = coder.encode([78, 45, 92]) # => "z2j7-0dmw-kf8p"
|
|
|
205
330
|
decoded = coder.decode(encoded) # => [78, 45, 92]
|
|
206
331
|
```
|
|
207
332
|
|
|
208
|
-
###
|
|
333
|
+
### With Hashids and Blocklist
|
|
209
334
|
```ruby
|
|
210
|
-
|
|
211
|
-
coder = EncodedId::ReversibleId.new(
|
|
335
|
+
coder = EncodedId::ReversibleId.hashid(
|
|
212
336
|
salt: "my-app-salt",
|
|
213
|
-
|
|
214
|
-
|
|
337
|
+
min_length: 12,
|
|
338
|
+
blocklist: EncodedId::Blocklist.minimal,
|
|
339
|
+
blocklist_mode: :length_threshold
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
encoded = coder.encode(123)
|
|
343
|
+
# Raises BlocklistError if result contains blocklisted word
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
### Custom Configuration
|
|
347
|
+
```ruby
|
|
348
|
+
# Highly customized Sqids instance
|
|
349
|
+
coder = EncodedId::ReversibleId.sqids(
|
|
350
|
+
min_length: 12,
|
|
215
351
|
split_at: 3,
|
|
216
352
|
split_with: ".",
|
|
217
353
|
alphabet: EncodedId::Alphabet.new("0123456789ABCDEF"),
|
|
218
|
-
blocklist: ["BAD", "FAKE"]
|
|
354
|
+
blocklist: ["BAD", "FAKE"],
|
|
355
|
+
blocklist_mode: :length_threshold,
|
|
356
|
+
blocklist_max_length: 32
|
|
219
357
|
)
|
|
220
358
|
```
|
|
221
359
|
|
|
222
360
|
### Hex Encoding (UUIDs)
|
|
223
361
|
```ruby
|
|
224
362
|
# For encoding UUIDs efficiently
|
|
225
|
-
coder = EncodedId::ReversibleId.
|
|
226
|
-
salt: "my-salt",
|
|
227
|
-
hex_digit_encoding_group_size: 32
|
|
228
|
-
)
|
|
363
|
+
coder = EncodedId::ReversibleId.sqids(hex_digit_encoding_group_size: 32)
|
|
229
364
|
|
|
230
365
|
uuid = "550e8400-e29b-41d4-a716-446655440000"
|
|
231
366
|
encoded = coder.encode_hex(uuid)
|
|
232
|
-
decoded = coder.decode_hex(encoded).first # => original UUID
|
|
367
|
+
decoded = coder.decode_hex(encoded).first # => original UUID (without hyphens)
|
|
233
368
|
```
|
|
234
369
|
|
|
235
370
|
## Performance Considerations
|
|
236
371
|
|
|
237
|
-
1. **Algorithm Choice**:
|
|
372
|
+
1. **Algorithm Choice**:
|
|
238
373
|
- HashIds: Faster encoding, especially with blocklists
|
|
239
|
-
- Sqids: Faster decoding
|
|
374
|
+
- Sqids: Faster decoding, automatically avoids blocklisted words
|
|
375
|
+
|
|
376
|
+
2. **Blocklist Impact**:
|
|
377
|
+
- Large blocklists slow encoding, especially with Sqids (which iterates to avoid words)
|
|
378
|
+
- Hashids may raise exceptions requiring retry logic
|
|
379
|
+
- Use `blocklist_mode: :length_threshold` for best performance
|
|
380
|
+
- `:always` mode can significantly impact encoding speed for long IDs
|
|
381
|
+
- Blocklists are automatically filtered for your alphabet, improving performance
|
|
382
|
+
|
|
383
|
+
3. **Blocklist Mode Performance**:
|
|
384
|
+
- `:length_threshold` (default): Only checks blocklist for IDs ≤ `blocklist_max_length` (default: 32)
|
|
385
|
+
- `:always`: Checks all IDs regardless of length (can be slow)
|
|
386
|
+
- `:raise_if_likely`: Validates configuration at initialization to prevent performance issues
|
|
387
|
+
|
|
388
|
+
4. **Length vs Performance**: Longer minimum lengths may require more computation
|
|
389
|
+
|
|
390
|
+
5. **Memory Usage**: The gem uses optimized implementations to minimize memory allocation
|
|
240
391
|
|
|
241
|
-
|
|
392
|
+
## Version Compatibility
|
|
242
393
|
|
|
243
|
-
|
|
394
|
+
**v1.0.0 Breaking Changes:**
|
|
244
395
|
|
|
245
|
-
|
|
396
|
+
1. **Default encoder**: Changed from `:hashids` to `:sqids`
|
|
397
|
+
2. **Case sensitivity**: `decode` is now case-sensitive by default (`downcase: false`)
|
|
398
|
+
- Pre-v1: `decode("ABC")` and `decode("abc")` were equivalent
|
|
399
|
+
- v1.0.0+: These produce different results unless `downcase: true`
|
|
400
|
+
3. **Salt requirement**: Sqids (default) doesn't require salt; Hashids still requires salt
|
|
401
|
+
4. **Migration**: For backwards compatibility with pre-v1:
|
|
402
|
+
```ruby
|
|
403
|
+
coder = EncodedId::ReversibleId.hashid(salt: "your-salt")
|
|
404
|
+
decoded = coder.decode(id, downcase: true)
|
|
405
|
+
```
|
|
246
406
|
|
|
247
407
|
## Security Notes
|
|
248
408
|
|
|
@@ -252,32 +412,26 @@ Use encoded IDs for:
|
|
|
252
412
|
- Hiding sequential database IDs
|
|
253
413
|
- Creating user-friendly URLs
|
|
254
414
|
- Preventing ID enumeration attacks
|
|
415
|
+
- Obscuring business metrics (user counts, order volumes)
|
|
255
416
|
|
|
256
417
|
Do NOT use for:
|
|
257
418
|
- Secure tokens
|
|
258
419
|
- Authentication
|
|
259
420
|
- Sensitive data protection
|
|
421
|
+
- Cryptographic purposes
|
|
260
422
|
|
|
261
423
|
## Installation
|
|
262
424
|
|
|
263
425
|
```ruby
|
|
264
426
|
# Gemfile
|
|
265
427
|
gem 'encoded_id'
|
|
266
|
-
|
|
267
|
-
# Or install directly
|
|
268
|
-
gem install encoded_id
|
|
269
|
-
```
|
|
270
|
-
|
|
271
|
-
For Sqids support:
|
|
272
|
-
```ruby
|
|
273
|
-
gem 'encoded_id'
|
|
274
|
-
gem 'sqids'
|
|
275
428
|
```
|
|
276
429
|
|
|
277
430
|
## Best Practices
|
|
278
431
|
|
|
279
|
-
1. **
|
|
280
|
-
2. **
|
|
281
|
-
3. **
|
|
282
|
-
4. **
|
|
283
|
-
5. **
|
|
432
|
+
1. **Consistent Configuration**: Once in production, don't change salt, encoder, or alphabet
|
|
433
|
+
2. **Error Handling**: Always handle potential exceptions when decoding user input
|
|
434
|
+
3. **Length Limits**: Set appropriate max_length to prevent DoS attacks
|
|
435
|
+
4. **Validation**: Validate decoded IDs before using them in database queries
|
|
436
|
+
5. **Blocklist Mode**: Use `:length_threshold` (default) for production - best performance/safety balance
|
|
437
|
+
6. **Factory Methods**: Prefer `ReversibleId.sqids()` and `ReversibleId.hashid()` over constructor
|
data/lib/encoded_id/alphabet.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# rbs_inline: enabled
|
|
4
4
|
|
|
5
5
|
module EncodedId
|
|
6
|
+
# Represents a character set (alphabet) used for encoding IDs, with optional character equivalences.
|
|
6
7
|
class Alphabet
|
|
7
8
|
MIN_UNIQUE_CHARACTERS = 16
|
|
8
9
|
|
|
@@ -94,6 +95,7 @@ module EncodedId
|
|
|
94
95
|
unique_characters.size >= MIN_UNIQUE_CHARACTERS
|
|
95
96
|
end
|
|
96
97
|
|
|
98
|
+
# Validates equivalences ensure: keys map to values in the alphabet, and keys are not already in the alphabet
|
|
97
99
|
# @rbs (Hash[String, String]? equivalences) -> bool
|
|
98
100
|
def valid_equivalences?(equivalences)
|
|
99
101
|
return true if equivalences.nil?
|