opaque_id 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 +7 -0
- data/.cursor/rules/create-prd.md +56 -0
- data/.cursor/rules/generate-tasks.md +60 -0
- data/.cursor/rules/process-task-list.md +47 -0
- data/.cursorignore +55 -0
- data/.release-please-manifest.json +3 -0
- data/.rubocop.yml +31 -0
- data/CHANGELOG.md +169 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +1602 -0
- data/Rakefile +12 -0
- data/lib/generators/opaque_id/install_generator.rb +57 -0
- data/lib/generators/opaque_id/templates/migration.rb.tt +6 -0
- data/lib/opaque_id/model.rb +130 -0
- data/lib/opaque_id/version.rb +5 -0
- data/lib/opaque_id.rb +58 -0
- data/release-please-config.json +55 -0
- data/sig/opaque_id.rbs +4 -0
- data/tasks/0001-prd-opaque-id-gem.md +202 -0
- data/tasks/0002-prd-publishing-release-automation.md +206 -0
- data/tasks/references/opaque_gem_requirements.md +482 -0
- data/tasks/references/original_identifiable_concern_and_nanoid.md +110 -0
- data/tasks/tasks-0001-prd-opaque-id-gem.md +109 -0
- data/tasks/tasks-0002-prd-publishing-release-automation.md +177 -0
- metadata +168 -0
data/README.md
ADDED
@@ -0,0 +1,1602 @@
|
|
1
|
+
# OpaqueId
|
2
|
+
|
3
|
+
A Ruby gem for generating cryptographically secure, collision-free opaque IDs for ActiveRecord models. OpaqueId provides a drop-in replacement for `nanoid.rb` using Ruby's built-in `SecureRandom` methods with optimized algorithms for unbiased distribution.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **๐ Cryptographically Secure**: Uses Ruby's `SecureRandom` for secure ID generation
|
8
|
+
- **โก High Performance**: Optimized algorithms with fast paths for 64-character alphabets
|
9
|
+
- **๐ฏ Collision-Free**: Built-in collision detection with configurable retry attempts
|
10
|
+
- **๐ง Highly Configurable**: Customizable alphabet, length, column name, and validation rules
|
11
|
+
- **๐ Rails Integration**: Seamless ActiveRecord integration with automatic ID generation
|
12
|
+
- **๐ฆ Rails Generator**: One-command setup with `rails generate opaque_id:install`
|
13
|
+
- **๐งช Well Tested**: Comprehensive test suite with statistical uniformity tests
|
14
|
+
- **๐ Rails 8.0+ Compatible**: Built for modern Rails applications
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
### Requirements
|
19
|
+
|
20
|
+
- Ruby 3.2.0 or higher
|
21
|
+
- Rails 8.0 or higher
|
22
|
+
- ActiveRecord 8.0 or higher
|
23
|
+
|
24
|
+
### Using Bundler (Recommended)
|
25
|
+
|
26
|
+
Add this line to your application's Gemfile:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
gem 'opaque_id'
|
30
|
+
```
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
```bash
|
35
|
+
bundle install
|
36
|
+
```
|
37
|
+
|
38
|
+
### Manual Installation
|
39
|
+
|
40
|
+
If you're not using Bundler, you can install the gem directly:
|
41
|
+
|
42
|
+
```bash
|
43
|
+
gem install opaque_id
|
44
|
+
```
|
45
|
+
|
46
|
+
### From Source
|
47
|
+
|
48
|
+
To install from the latest source:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# In your Gemfile
|
52
|
+
gem 'opaque_id', git: 'https://github.com/nyaggah/opaque_id.git'
|
53
|
+
```
|
54
|
+
|
55
|
+
```bash
|
56
|
+
bundle install
|
57
|
+
```
|
58
|
+
|
59
|
+
### Troubleshooting
|
60
|
+
|
61
|
+
**Rails Version Compatibility**: If you're using an older version of Rails, you may need to check compatibility. OpaqueId is designed for Rails 8.0+.
|
62
|
+
|
63
|
+
**Ruby Version**: Ensure you're using Ruby 3.2.0 or higher. Check your version with:
|
64
|
+
|
65
|
+
```bash
|
66
|
+
ruby --version
|
67
|
+
```
|
68
|
+
|
69
|
+
## Quick Start
|
70
|
+
|
71
|
+
### 1. Generate Migration and Update Model
|
72
|
+
|
73
|
+
```bash
|
74
|
+
rails generate opaque_id:install users
|
75
|
+
```
|
76
|
+
|
77
|
+
This will:
|
78
|
+
|
79
|
+
- Create a migration to add an `opaque_id` column with a unique index
|
80
|
+
- Automatically add `include OpaqueId::Model` to your `User` model
|
81
|
+
|
82
|
+
### 2. Run Migration
|
83
|
+
|
84
|
+
```bash
|
85
|
+
rails db:migrate
|
86
|
+
```
|
87
|
+
|
88
|
+
### 3. Use in Your Models
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
class User < ApplicationRecord
|
92
|
+
include OpaqueId::Model
|
93
|
+
end
|
94
|
+
|
95
|
+
# IDs are automatically generated on creation
|
96
|
+
user = User.create!(name: "John Doe")
|
97
|
+
puts user.opaque_id # => "V1StGXR8_Z5jdHi6B-myT"
|
98
|
+
|
99
|
+
# Find by opaque ID
|
100
|
+
user = User.find_by_opaque_id("V1StGXR8_Z5jdHi6B-myT")
|
101
|
+
user = User.find_by_opaque_id!("V1StGXR8_Z5jdHi6B-myT") # raises if not found
|
102
|
+
```
|
103
|
+
|
104
|
+
## Usage
|
105
|
+
|
106
|
+
### Standalone ID Generation
|
107
|
+
|
108
|
+
OpaqueId can be used independently of ActiveRecord for generating secure IDs in any Ruby application:
|
109
|
+
|
110
|
+
#### Basic Usage
|
111
|
+
|
112
|
+
```ruby
|
113
|
+
# Generate with default settings (21 characters, alphanumeric)
|
114
|
+
id = OpaqueId.generate
|
115
|
+
# => "V1StGXR8_Z5jdHi6B-myT"
|
116
|
+
|
117
|
+
# Custom length
|
118
|
+
id = OpaqueId.generate(size: 10)
|
119
|
+
# => "V1StGXR8_Z5"
|
120
|
+
|
121
|
+
# Custom alphabet
|
122
|
+
id = OpaqueId.generate(alphabet: OpaqueId::STANDARD_ALPHABET)
|
123
|
+
# => "V1StGXR8_Z5jdHi6B-myT"
|
124
|
+
|
125
|
+
# Custom alphabet and length
|
126
|
+
id = OpaqueId.generate(size: 8, alphabet: "ABCDEFGH")
|
127
|
+
# => "ABCDEFGH"
|
128
|
+
|
129
|
+
# Generate multiple IDs
|
130
|
+
ids = 5.times.map { OpaqueId.generate(size: 8) }
|
131
|
+
# => ["V1StGXR8", "Z5jdHi6B", "myT12345", "ABCdefGH", "IJKlmnoP"]
|
132
|
+
```
|
133
|
+
|
134
|
+
#### Standalone Use Cases
|
135
|
+
|
136
|
+
##### Background Job IDs
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
# Generate unique job identifiers
|
140
|
+
class BackgroundJob
|
141
|
+
def self.enqueue(job_class, *args)
|
142
|
+
job_id = OpaqueId.generate(size: 12)
|
143
|
+
# Store job with unique ID
|
144
|
+
puts "Enqueued job #{job_class} with ID: #{job_id}"
|
145
|
+
job_id
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
job_id = BackgroundJob.enqueue(ProcessDataJob, user_id: 123)
|
150
|
+
# => "V1StGXR8_Z5jd"
|
151
|
+
```
|
152
|
+
|
153
|
+
##### Temporary File Names
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
# Generate unique temporary filenames
|
157
|
+
def create_temp_file(content)
|
158
|
+
temp_filename = "temp_#{OpaqueId.generate(size: 8)}.txt"
|
159
|
+
File.write(temp_filename, content)
|
160
|
+
temp_filename
|
161
|
+
end
|
162
|
+
|
163
|
+
filename = create_temp_file("Hello World")
|
164
|
+
# => "temp_V1StGXR8.txt"
|
165
|
+
```
|
166
|
+
|
167
|
+
##### Cache Keys
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
# Generate cache keys for different data types
|
171
|
+
class CacheManager
|
172
|
+
def self.user_cache_key(user_id)
|
173
|
+
"user:#{OpaqueId.generate(size: 6)}:#{user_id}"
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.session_cache_key
|
177
|
+
"session:#{OpaqueId.generate(size: 16)}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
user_key = CacheManager.user_cache_key(123)
|
182
|
+
# => "user:V1StGX:123"
|
183
|
+
|
184
|
+
session_key = CacheManager.session_cache_key
|
185
|
+
# => "session:V1StGXR8_Z5jdHi6B"
|
186
|
+
```
|
187
|
+
|
188
|
+
##### Webhook Signatures
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
# Generate webhook signatures
|
192
|
+
class WebhookService
|
193
|
+
def self.generate_signature(payload)
|
194
|
+
timestamp = Time.current.to_i
|
195
|
+
nonce = OpaqueId.generate(size: 16)
|
196
|
+
signature = "#{timestamp}:#{nonce}:#{payload.hash}"
|
197
|
+
signature
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
signature = WebhookService.generate_signature({ user_id: 123 })
|
202
|
+
# => "1703123456:V1StGXR8_Z5jdHi6B:1234567890"
|
203
|
+
```
|
204
|
+
|
205
|
+
##### Database Migration IDs
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
# Generate unique migration identifiers
|
209
|
+
def create_migration(name)
|
210
|
+
timestamp = Time.current.strftime("%Y%m%d%H%M%S")
|
211
|
+
unique_id = OpaqueId.generate(size: 4)
|
212
|
+
"#{timestamp}_#{unique_id}_#{name}"
|
213
|
+
end
|
214
|
+
|
215
|
+
migration_name = create_migration("add_user_preferences")
|
216
|
+
# => "20231221143022_V1St_add_user_preferences"
|
217
|
+
```
|
218
|
+
|
219
|
+
##### Email Tracking IDs
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
# Generate email tracking pixel IDs
|
223
|
+
class EmailService
|
224
|
+
def self.tracking_pixel_id
|
225
|
+
OpaqueId.generate(size: 20, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
tracking_id = EmailService.tracking_pixel_id
|
230
|
+
# => "V1StGXR8Z5jdHi6BmyT12"
|
231
|
+
|
232
|
+
# Use in email template
|
233
|
+
# <img src="https://example.com/track/#{tracking_id}" width="1" height="1" />
|
234
|
+
```
|
235
|
+
|
236
|
+
##### API Request IDs
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
# Generate request IDs for API logging
|
240
|
+
class ApiLogger
|
241
|
+
def self.log_request(endpoint, params)
|
242
|
+
request_id = OpaqueId.generate(size: 12)
|
243
|
+
Rails.logger.info "Request #{request_id}: #{endpoint} - #{params}"
|
244
|
+
request_id
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
request_id = ApiLogger.log_request("/api/users", { page: 1 })
|
249
|
+
# => "V1StGXR8_Z5jd"
|
250
|
+
```
|
251
|
+
|
252
|
+
##### Batch Processing IDs
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
# Generate batch processing identifiers
|
256
|
+
class BatchProcessor
|
257
|
+
def self.process_batch(items)
|
258
|
+
batch_id = OpaqueId.generate(size: 10)
|
259
|
+
puts "Processing batch #{batch_id} with #{items.count} items"
|
260
|
+
|
261
|
+
items.each_with_index do |item, index|
|
262
|
+
item_id = "#{batch_id}_#{index.to_s.rjust(3, '0')}"
|
263
|
+
puts "Processing item #{item_id}: #{item}"
|
264
|
+
end
|
265
|
+
|
266
|
+
batch_id
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
batch_id = BatchProcessor.process_batch([1, 2, 3, 4, 5])
|
271
|
+
# => "V1StGXR8_Z5"
|
272
|
+
# => Processing item V1StGXR8_Z5_000: 1
|
273
|
+
# => Processing item V1StGXR8_Z5_001: 2
|
274
|
+
# => ...
|
275
|
+
```
|
276
|
+
|
277
|
+
### Real-World Examples
|
278
|
+
|
279
|
+
#### API Keys
|
280
|
+
|
281
|
+
```ruby
|
282
|
+
# Generate secure API keys
|
283
|
+
api_key = OpaqueId.generate(size: 32, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
|
284
|
+
# => "V1StGXR8_Z5jdHi6B-myT1234567890AB"
|
285
|
+
|
286
|
+
# Store in your API key model
|
287
|
+
class ApiKey < ApplicationRecord
|
288
|
+
include OpaqueId::Model
|
289
|
+
|
290
|
+
self.opaque_id_column = :key
|
291
|
+
self.opaque_id_length = 32
|
292
|
+
end
|
293
|
+
```
|
294
|
+
|
295
|
+
#### Short URLs
|
296
|
+
|
297
|
+
```ruby
|
298
|
+
# Generate short URL identifiers
|
299
|
+
short_id = OpaqueId.generate(size: 6, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
|
300
|
+
# => "V1StGX"
|
301
|
+
|
302
|
+
# Use in your URL shortener
|
303
|
+
class ShortUrl < ApplicationRecord
|
304
|
+
include OpaqueId::Model
|
305
|
+
|
306
|
+
self.opaque_id_column = :short_code
|
307
|
+
self.opaque_id_length = 6
|
308
|
+
end
|
309
|
+
```
|
310
|
+
|
311
|
+
#### File Uploads
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
# Generate unique filenames
|
315
|
+
filename = OpaqueId.generate(size: 12, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
|
316
|
+
# => "V1StGXR8_Z5jd"
|
317
|
+
|
318
|
+
# Use in your file upload system
|
319
|
+
class Upload < ApplicationRecord
|
320
|
+
include OpaqueId::Model
|
321
|
+
|
322
|
+
self.opaque_id_column = :filename
|
323
|
+
self.opaque_id_length = 12
|
324
|
+
end
|
325
|
+
```
|
326
|
+
|
327
|
+
### ActiveRecord Integration
|
328
|
+
|
329
|
+
#### Basic Usage
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
class Post < ApplicationRecord
|
333
|
+
include OpaqueId::Model
|
334
|
+
end
|
335
|
+
|
336
|
+
# Create a new post - opaque_id is automatically generated
|
337
|
+
post = Post.create!(title: "Hello World", content: "This is my first post")
|
338
|
+
puts post.opaque_id # => "V1StGXR8_Z5jdHi6B-myT"
|
339
|
+
|
340
|
+
# Create multiple posts
|
341
|
+
posts = Post.create!([
|
342
|
+
{ title: "Post 1", content: "Content 1" },
|
343
|
+
{ title: "Post 2", content: "Content 2" },
|
344
|
+
{ title: "Post 3", content: "Content 3" }
|
345
|
+
])
|
346
|
+
|
347
|
+
posts.each { |p| puts "#{p.title}: #{p.opaque_id}" }
|
348
|
+
# => Post 1: V1StGXR8_Z5jdHi6B-myT
|
349
|
+
# => Post 2: Z5jdHi6B-myT12345
|
350
|
+
# => Post 3: myT12345-ABCdefGH
|
351
|
+
```
|
352
|
+
|
353
|
+
#### Custom Configuration
|
354
|
+
|
355
|
+
OpaqueId provides extensive configuration options to tailor ID generation to your specific needs:
|
356
|
+
|
357
|
+
##### Basic Customization
|
358
|
+
|
359
|
+
```ruby
|
360
|
+
class User < ApplicationRecord
|
361
|
+
include OpaqueId::Model
|
362
|
+
|
363
|
+
# Use a different column name
|
364
|
+
self.opaque_id_column = :public_id
|
365
|
+
|
366
|
+
# Custom length and alphabet
|
367
|
+
self.opaque_id_length = 15
|
368
|
+
self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET
|
369
|
+
|
370
|
+
# Require ID to start with a letter
|
371
|
+
self.opaque_id_require_letter_start = true
|
372
|
+
|
373
|
+
# Remove specific characters
|
374
|
+
self.opaque_id_purge_chars = ['0', 'O', 'I', 'l']
|
375
|
+
|
376
|
+
# Maximum retry attempts for collision resolution
|
377
|
+
self.opaque_id_max_retry = 5
|
378
|
+
end
|
379
|
+
```
|
380
|
+
|
381
|
+
##### API Key Configuration
|
382
|
+
|
383
|
+
```ruby
|
384
|
+
class ApiKey < ApplicationRecord
|
385
|
+
include OpaqueId::Model
|
386
|
+
|
387
|
+
# Use 'key' as the column name
|
388
|
+
self.opaque_id_column = :key
|
389
|
+
|
390
|
+
# Longer IDs for better security
|
391
|
+
self.opaque_id_length = 32
|
392
|
+
|
393
|
+
# Alphanumeric only for API keys
|
394
|
+
self.opaque_id_alphabet = OpaqueId::ALPHANUMERIC_ALPHABET
|
395
|
+
|
396
|
+
# Remove confusing characters
|
397
|
+
self.opaque_id_purge_chars = ['0', 'O', 'I', 'l', '1']
|
398
|
+
|
399
|
+
# More retry attempts for high-volume systems
|
400
|
+
self.opaque_id_max_retry = 10
|
401
|
+
end
|
402
|
+
|
403
|
+
# Generated API keys will look like: "V1StGXR8Z5jdHi6BmyT1234567890AB"
|
404
|
+
```
|
405
|
+
|
406
|
+
##### Short URL Configuration
|
407
|
+
|
408
|
+
```ruby
|
409
|
+
class ShortUrl < ApplicationRecord
|
410
|
+
include OpaqueId::Model
|
411
|
+
|
412
|
+
# Use 'code' as the column name
|
413
|
+
self.opaque_id_column = :code
|
414
|
+
|
415
|
+
# Shorter IDs for URLs
|
416
|
+
self.opaque_id_length = 6
|
417
|
+
|
418
|
+
# URL-safe characters only
|
419
|
+
self.opaque_id_alphabet = OpaqueId::ALPHANUMERIC_ALPHABET
|
420
|
+
|
421
|
+
# Remove confusing characters for better UX
|
422
|
+
self.opaque_id_purge_chars = ['0', 'O', 'I', 'l', '1']
|
423
|
+
|
424
|
+
# Require letter start for better readability
|
425
|
+
self.opaque_id_require_letter_start = true
|
426
|
+
end
|
427
|
+
|
428
|
+
# Generated short codes will look like: "V1StGX"
|
429
|
+
```
|
430
|
+
|
431
|
+
##### File Upload Configuration
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
class Upload < ApplicationRecord
|
435
|
+
include OpaqueId::Model
|
436
|
+
|
437
|
+
# Use 'filename' as the column name
|
438
|
+
self.opaque_id_column = :filename
|
439
|
+
|
440
|
+
# Medium length for filenames
|
441
|
+
self.opaque_id_length = 12
|
442
|
+
|
443
|
+
# Alphanumeric with hyphens for filenames
|
444
|
+
self.opaque_id_alphabet = OpaqueId::ALPHANUMERIC_ALPHABET + "-"
|
445
|
+
|
446
|
+
# Remove problematic characters for filesystems
|
447
|
+
self.opaque_id_purge_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|']
|
448
|
+
end
|
449
|
+
|
450
|
+
# Generated filenames will look like: "V1StGXR8-Z5jd"
|
451
|
+
```
|
452
|
+
|
453
|
+
##### Session Token Configuration
|
454
|
+
|
455
|
+
```ruby
|
456
|
+
class Session < ApplicationRecord
|
457
|
+
include OpaqueId::Model
|
458
|
+
|
459
|
+
# Use 'token' as the column name
|
460
|
+
self.opaque_id_column = :token
|
461
|
+
|
462
|
+
# Longer tokens for security
|
463
|
+
self.opaque_id_length = 24
|
464
|
+
|
465
|
+
# URL-safe characters for cookies
|
466
|
+
self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET
|
467
|
+
|
468
|
+
# Remove confusing characters
|
469
|
+
self.opaque_id_purge_chars = ['0', 'O', 'I', 'l', '1']
|
470
|
+
|
471
|
+
# More retry attempts for high-concurrency
|
472
|
+
self.opaque_id_max_retry = 8
|
473
|
+
end
|
474
|
+
|
475
|
+
# Generated session tokens will look like: "V1StGXR8_Z5jdHi6B-myT123"
|
476
|
+
```
|
477
|
+
|
478
|
+
##### Custom Alphabet Examples
|
479
|
+
|
480
|
+
```ruby
|
481
|
+
# Numeric only
|
482
|
+
class Order < ApplicationRecord
|
483
|
+
include OpaqueId::Model
|
484
|
+
|
485
|
+
self.opaque_id_alphabet = "0123456789"
|
486
|
+
self.opaque_id_length = 8
|
487
|
+
end
|
488
|
+
# Generated: "12345678"
|
489
|
+
|
490
|
+
# Uppercase only
|
491
|
+
class Product < ApplicationRecord
|
492
|
+
include OpaqueId::Model
|
493
|
+
|
494
|
+
self.opaque_id_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
495
|
+
self.opaque_id_length = 6
|
496
|
+
end
|
497
|
+
# Generated: "ABCDEF"
|
498
|
+
|
499
|
+
# Custom character set
|
500
|
+
class Invite < ApplicationRecord
|
501
|
+
include OpaqueId::Model
|
502
|
+
|
503
|
+
self.opaque_id_alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # No confusing chars
|
504
|
+
self.opaque_id_length = 8
|
505
|
+
end
|
506
|
+
# Generated: "ABCDEFGH"
|
507
|
+
```
|
508
|
+
|
509
|
+
#### Finder Methods
|
510
|
+
|
511
|
+
```ruby
|
512
|
+
# Find by opaque ID (returns nil if not found)
|
513
|
+
user = User.find_by_opaque_id("V1StGXR8_Z5jdHi6B-myT")
|
514
|
+
if user
|
515
|
+
puts "Found user: #{user.name}"
|
516
|
+
else
|
517
|
+
puts "User not found"
|
518
|
+
end
|
519
|
+
|
520
|
+
# Find by opaque ID (raises ActiveRecord::RecordNotFound if not found)
|
521
|
+
user = User.find_by_opaque_id!("V1StGXR8_Z5jdHi6B-myT")
|
522
|
+
puts "Found user: #{user.name}"
|
523
|
+
|
524
|
+
# Use in controllers for public-facing URLs
|
525
|
+
class PostsController < ApplicationController
|
526
|
+
def show
|
527
|
+
@post = Post.find_by_opaque_id!(params[:id])
|
528
|
+
# This allows URLs like /posts/V1StGXR8_Z5jdHi6B-myT
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# Use in API endpoints
|
533
|
+
class Api::UsersController < ApplicationController
|
534
|
+
def show
|
535
|
+
user = User.find_by_opaque_id(params[:id])
|
536
|
+
if user
|
537
|
+
render json: { id: user.opaque_id, name: user.name }
|
538
|
+
else
|
539
|
+
render json: { error: "User not found" }, status: 404
|
540
|
+
end
|
541
|
+
end
|
542
|
+
end
|
543
|
+
```
|
544
|
+
|
545
|
+
### Rails Generator
|
546
|
+
|
547
|
+
#### Basic Usage
|
548
|
+
|
549
|
+
```bash
|
550
|
+
rails generate opaque_id:install users
|
551
|
+
```
|
552
|
+
|
553
|
+
#### Custom Column Name
|
554
|
+
|
555
|
+
```bash
|
556
|
+
rails generate opaque_id:install users --column-name=public_id
|
557
|
+
```
|
558
|
+
|
559
|
+
#### What the Generator Does
|
560
|
+
|
561
|
+
1. **Creates Migration**: Adds `opaque_id` column with unique index
|
562
|
+
2. **Updates Model**: Automatically adds `include OpaqueId::Model` to your model
|
563
|
+
3. **Handles Edge Cases**: Detects if concern is already included, handles missing model files
|
564
|
+
|
565
|
+
## Configuration Options
|
566
|
+
|
567
|
+
OpaqueId provides comprehensive configuration options to customize ID generation behavior:
|
568
|
+
|
569
|
+
| Option | Type | Default | Description | Example Usage |
|
570
|
+
| -------------------------------- | --------------- | ----------------------- | ----------------------------------------------- | ------------------------------------------------------- |
|
571
|
+
| `opaque_id_column` | `Symbol` | `:opaque_id` | Column name for storing the opaque ID | `self.opaque_id_column = :public_id` |
|
572
|
+
| `opaque_id_length` | `Integer` | `21` | Length of generated IDs | `self.opaque_id_length = 32` |
|
573
|
+
| `opaque_id_alphabet` | `String` | `ALPHANUMERIC_ALPHABET` | Character set for ID generation | `self.opaque_id_alphabet = OpaqueId::STANDARD_ALPHABET` |
|
574
|
+
| `opaque_id_require_letter_start` | `Boolean` | `false` | Require ID to start with a letter | `self.opaque_id_require_letter_start = true` |
|
575
|
+
| `opaque_id_purge_chars` | `Array<String>` | `[]` | Characters to remove from generated IDs | `self.opaque_id_purge_chars = ['0', 'O', 'I', 'l']` |
|
576
|
+
| `opaque_id_max_retry` | `Integer` | `3` | Maximum retry attempts for collision resolution | `self.opaque_id_max_retry = 10` |
|
577
|
+
|
578
|
+
### Configuration Details
|
579
|
+
|
580
|
+
#### `opaque_id_column`
|
581
|
+
|
582
|
+
- **Purpose**: Specifies the database column name for storing opaque IDs
|
583
|
+
- **Use Cases**: When you want to use a different column name (e.g., `public_id`, `external_id`, `key`)
|
584
|
+
- **Example**: `self.opaque_id_column = :public_id` โ IDs stored in `public_id` column
|
585
|
+
|
586
|
+
#### `opaque_id_length`
|
587
|
+
|
588
|
+
- **Purpose**: Controls the length of generated IDs
|
589
|
+
- **Range**: 1 to 255 characters (practical limit)
|
590
|
+
- **Performance**: Longer IDs are more secure but use more storage
|
591
|
+
- **Examples**:
|
592
|
+
- `6` โ Short URLs: `"V1StGX"`
|
593
|
+
- `21` โ Default: `"V1StGXR8_Z5jdHi6B-myT"`
|
594
|
+
- `32` โ API Keys: `"V1StGXR8_Z5jdHi6B-myT1234567890AB"`
|
595
|
+
|
596
|
+
#### `opaque_id_alphabet`
|
597
|
+
|
598
|
+
- **Purpose**: Defines the character set used for ID generation
|
599
|
+
- **Built-in Options**: `ALPHANUMERIC_ALPHABET`, `STANDARD_ALPHABET`
|
600
|
+
- **Custom**: Any string of unique characters
|
601
|
+
- **Security**: Larger alphabets provide more entropy per character
|
602
|
+
- **Examples**:
|
603
|
+
- `"0123456789"` โ Numeric only: `"12345678"`
|
604
|
+
- `"ABCDEFGHIJKLMNOPQRSTUVWXYZ"` โ Uppercase only: `"ABCDEF"`
|
605
|
+
- `"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"` โ No confusing chars: `"ABCDEFGH"`
|
606
|
+
|
607
|
+
#### `opaque_id_require_letter_start`
|
608
|
+
|
609
|
+
- **Purpose**: Ensures IDs start with a letter for better readability
|
610
|
+
- **Use Cases**: When IDs are user-facing or need to be easily readable
|
611
|
+
- **Performance**: Slight overhead due to rejection sampling
|
612
|
+
- **Example**: `true` โ `"V1StGXR8_Z5jdHi6B-myT"`, `false` โ `"1StGXR8_Z5jdHi6B-myT"`
|
613
|
+
|
614
|
+
#### `opaque_id_purge_chars`
|
615
|
+
|
616
|
+
- **Purpose**: Removes problematic characters from generated IDs
|
617
|
+
- **Use Cases**: Avoiding confusing characters (0/O, 1/I/l) or filesystem-unsafe chars
|
618
|
+
- **Performance**: Minimal overhead, applied after generation
|
619
|
+
- **Examples**:
|
620
|
+
- `['0', 'O', 'I', 'l']` โ Removes visually similar characters
|
621
|
+
- `['/', '\\', ':', '*', '?', '"', '<', '>', '|']` โ Removes filesystem-unsafe characters
|
622
|
+
|
623
|
+
#### `opaque_id_max_retry`
|
624
|
+
|
625
|
+
- **Purpose**: Controls collision resolution attempts
|
626
|
+
- **Use Cases**: High-volume systems where collisions are more likely
|
627
|
+
- **Performance**: Higher values provide better collision resolution but may slow down creation
|
628
|
+
- **Examples**:
|
629
|
+
- `3` โ Default, good for most applications
|
630
|
+
- `10` โ High-volume systems with many concurrent creations
|
631
|
+
- `1` โ When you want to fail fast on collisions
|
632
|
+
|
633
|
+
## Built-in Alphabets
|
634
|
+
|
635
|
+
OpaqueId provides two pre-configured alphabets optimized for different use cases:
|
636
|
+
|
637
|
+
### `ALPHANUMERIC_ALPHABET` (Default)
|
638
|
+
|
639
|
+
```ruby
|
640
|
+
OpaqueId::ALPHANUMERIC_ALPHABET
|
641
|
+
# => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
642
|
+
```
|
643
|
+
|
644
|
+
**Characteristics:**
|
645
|
+
|
646
|
+
- **Length**: 62 characters
|
647
|
+
- **Characters**: A-Z, a-z, 0-9
|
648
|
+
- **URL Safety**: โ
Fully URL-safe
|
649
|
+
- **Readability**: โ
High (no confusing characters)
|
650
|
+
- **Entropy**: 62^n possible combinations
|
651
|
+
- **Performance**: โก Fast path (64-character optimization)
|
652
|
+
|
653
|
+
**Best For:**
|
654
|
+
|
655
|
+
- API keys and tokens
|
656
|
+
- Public-facing URLs
|
657
|
+
- User-visible identifiers
|
658
|
+
- Database primary keys
|
659
|
+
- General-purpose ID generation
|
660
|
+
|
661
|
+
**Example Output:**
|
662
|
+
|
663
|
+
```ruby
|
664
|
+
OpaqueId.generate(size: 8, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET)
|
665
|
+
# => "V1StGXR8"
|
666
|
+
```
|
667
|
+
|
668
|
+
### `STANDARD_ALPHABET`
|
669
|
+
|
670
|
+
```ruby
|
671
|
+
OpaqueId::STANDARD_ALPHABET
|
672
|
+
# => "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
673
|
+
```
|
674
|
+
|
675
|
+
**Characteristics:**
|
676
|
+
|
677
|
+
- **Length**: 64 characters
|
678
|
+
- **Characters**: A-Z, a-z, 0-9, -, \_
|
679
|
+
- **URL Safety**: โ
Fully URL-safe
|
680
|
+
- **Readability**: โ
High (no confusing characters)
|
681
|
+
- **Entropy**: 64^n possible combinations
|
682
|
+
- **Performance**: โก Fast path (64-character optimization)
|
683
|
+
|
684
|
+
**Best For:**
|
685
|
+
|
686
|
+
- Short URLs and links
|
687
|
+
- File names and paths
|
688
|
+
- Configuration keys
|
689
|
+
- Session identifiers
|
690
|
+
- High-performance applications
|
691
|
+
|
692
|
+
**Example Output:**
|
693
|
+
|
694
|
+
```ruby
|
695
|
+
OpaqueId.generate(size: 8, alphabet: OpaqueId::STANDARD_ALPHABET)
|
696
|
+
# => "V1StGXR8"
|
697
|
+
```
|
698
|
+
|
699
|
+
### Alphabet Comparison
|
700
|
+
|
701
|
+
| Feature | ALPHANUMERIC_ALPHABET | STANDARD_ALPHABET |
|
702
|
+
| ------------------------- | --------------------- | ----------------- |
|
703
|
+
| **Character Count** | 62 | 64 |
|
704
|
+
| **URL Safe** | โ
Yes | โ
Yes |
|
705
|
+
| **Performance** | โก Fast | โก Fastest |
|
706
|
+
| **Entropy per Character** | ~5.95 bits | 6 bits |
|
707
|
+
| **Collision Resistance** | High | Highest |
|
708
|
+
| **Use Case** | General purpose | High performance |
|
709
|
+
|
710
|
+
### Custom Alphabets
|
711
|
+
|
712
|
+
You can also create custom alphabets for specific needs:
|
713
|
+
|
714
|
+
```ruby
|
715
|
+
# Numeric only (10 characters)
|
716
|
+
numeric_alphabet = "0123456789"
|
717
|
+
OpaqueId.generate(size: 8, alphabet: numeric_alphabet)
|
718
|
+
# => "12345678"
|
719
|
+
|
720
|
+
# Uppercase only (26 characters)
|
721
|
+
uppercase_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
722
|
+
OpaqueId.generate(size: 6, alphabet: uppercase_alphabet)
|
723
|
+
# => "ABCDEF"
|
724
|
+
|
725
|
+
# No confusing characters (58 characters)
|
726
|
+
safe_alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz23456789"
|
727
|
+
OpaqueId.generate(size: 8, alphabet: safe_alphabet)
|
728
|
+
# => "ABCDEFGH"
|
729
|
+
|
730
|
+
# Filesystem safe (63 characters)
|
731
|
+
filesystem_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
|
732
|
+
OpaqueId.generate(size: 12, alphabet: filesystem_alphabet)
|
733
|
+
# => "V1StGXR8_Z5jd"
|
734
|
+
|
735
|
+
# Base64-like (64 characters)
|
736
|
+
base64_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
737
|
+
OpaqueId.generate(size: 16, alphabet: base64_alphabet)
|
738
|
+
# => "V1StGXR8/Z5jdHi6B"
|
739
|
+
```
|
740
|
+
|
741
|
+
### Alphabet Selection Guide
|
742
|
+
|
743
|
+
**Choose `ALPHANUMERIC_ALPHABET` when:**
|
744
|
+
|
745
|
+
- Building APIs or web services
|
746
|
+
- IDs will be user-visible
|
747
|
+
- You need maximum compatibility
|
748
|
+
- General-purpose ID generation
|
749
|
+
|
750
|
+
**Choose `STANDARD_ALPHABET` when:**
|
751
|
+
|
752
|
+
- Building high-performance applications
|
753
|
+
- Creating short URLs or links
|
754
|
+
- You need maximum entropy
|
755
|
+
- File names or paths are involved
|
756
|
+
|
757
|
+
**Create custom alphabets when:**
|
758
|
+
|
759
|
+
- You need specific character sets
|
760
|
+
- Avoiding certain characters (0/O, 1/I/l)
|
761
|
+
- Working with legacy systems
|
762
|
+
- Special formatting requirements
|
763
|
+
|
764
|
+
## Algorithm Details
|
765
|
+
|
766
|
+
OpaqueId implements two optimized algorithms for secure ID generation, automatically selecting the best approach based on alphabet size:
|
767
|
+
|
768
|
+
### Fast Path Algorithm (64-character alphabets)
|
769
|
+
|
770
|
+
When using 64-character alphabets (like `STANDARD_ALPHABET`), OpaqueId uses an optimized bitwise approach:
|
771
|
+
|
772
|
+
```ruby
|
773
|
+
# Simplified algorithm for 64-character alphabets
|
774
|
+
def generate_fast(size, alphabet)
|
775
|
+
result = ""
|
776
|
+
size.times do
|
777
|
+
# Get random byte from SecureRandom
|
778
|
+
byte = SecureRandom.random_number(256)
|
779
|
+
# Use bitwise AND to get index 0-63
|
780
|
+
index = byte & 63
|
781
|
+
result << alphabet[index]
|
782
|
+
end
|
783
|
+
result
|
784
|
+
end
|
785
|
+
```
|
786
|
+
|
787
|
+
**Advantages:**
|
788
|
+
|
789
|
+
- โก **Maximum Performance**: Direct bitwise operations, no rejection sampling
|
790
|
+
- ๐ฏ **Perfect Distribution**: Each character has exactly 1/64 probability
|
791
|
+
- ๐ **Cryptographically Secure**: Uses `SecureRandom` as entropy source
|
792
|
+
- ๐ **Predictable Performance**: Constant time complexity O(n)
|
793
|
+
|
794
|
+
**Why 64 characters?**
|
795
|
+
|
796
|
+
- 64 = 2^6, allowing efficient bitwise operations
|
797
|
+
- `byte & 63` extracts exactly 6 bits (0-63 range)
|
798
|
+
- No modulo bias since 256 is divisible by 64
|
799
|
+
|
800
|
+
### Unbiased Path Algorithm (other alphabets)
|
801
|
+
|
802
|
+
For alphabets with sizes other than 64, OpaqueId uses rejection sampling:
|
803
|
+
|
804
|
+
```ruby
|
805
|
+
# Simplified algorithm for non-64-character alphabets
|
806
|
+
def generate_unbiased(size, alphabet, alphabet_size)
|
807
|
+
result = ""
|
808
|
+
size.times do
|
809
|
+
loop do
|
810
|
+
# Get random byte
|
811
|
+
byte = SecureRandom.random_number(256)
|
812
|
+
# Calculate index using modulo
|
813
|
+
index = byte % alphabet_size
|
814
|
+
# Check if within unbiased range
|
815
|
+
if byte < (256 / alphabet_size) * alphabet_size
|
816
|
+
result << alphabet[index]
|
817
|
+
break
|
818
|
+
end
|
819
|
+
# Reject and try again (rare occurrence)
|
820
|
+
end
|
821
|
+
end
|
822
|
+
result
|
823
|
+
end
|
824
|
+
```
|
825
|
+
|
826
|
+
**Advantages:**
|
827
|
+
|
828
|
+
- ๐ฏ **Perfect Uniformity**: Eliminates modulo bias through rejection sampling
|
829
|
+
- ๐ **Cryptographically Secure**: Uses `SecureRandom` as entropy source
|
830
|
+
- ๐ง **Flexible**: Works with any alphabet size
|
831
|
+
- ๐ **Statistically Sound**: Mathematically proven unbiased distribution
|
832
|
+
|
833
|
+
**Rejection Sampling Explained:**
|
834
|
+
|
835
|
+
- When `byte % alphabet_size` would create bias, the byte is rejected
|
836
|
+
- Only bytes in the "unbiased range" are used
|
837
|
+
- Rejection rate is minimal (typically <1% for common alphabet sizes)
|
838
|
+
|
839
|
+
### Algorithm Selection
|
840
|
+
|
841
|
+
```ruby
|
842
|
+
def generate(size:, alphabet:)
|
843
|
+
alphabet_size = alphabet.size
|
844
|
+
|
845
|
+
if alphabet_size == 64
|
846
|
+
generate_fast(size, alphabet) # Fast path
|
847
|
+
else
|
848
|
+
generate_unbiased(size, alphabet, alphabet_size) # Unbiased path
|
849
|
+
end
|
850
|
+
end
|
851
|
+
```
|
852
|
+
|
853
|
+
## Performance Benchmarks
|
854
|
+
|
855
|
+
### Generation Speed (IDs per second)
|
856
|
+
|
857
|
+
| Alphabet Size | Algorithm | Performance | Relative Speed |
|
858
|
+
| ------------- | --------- | ------------------ | --------------- |
|
859
|
+
| 64 characters | Fast Path | ~2,500,000 IDs/sec | 100% (baseline) |
|
860
|
+
| 62 characters | Unbiased | ~1,200,000 IDs/sec | 48% |
|
861
|
+
| 36 characters | Unbiased | ~1,100,000 IDs/sec | 44% |
|
862
|
+
| 26 characters | Unbiased | ~1,000,000 IDs/sec | 40% |
|
863
|
+
| 10 characters | Unbiased | ~900,000 IDs/sec | 36% |
|
864
|
+
|
865
|
+
_Benchmarks run on Ruby 3.2.0, generating 21-character IDs_
|
866
|
+
|
867
|
+
### Memory Usage
|
868
|
+
|
869
|
+
| Algorithm | Memory per ID | Memory per 1M IDs |
|
870
|
+
| --------- | ------------- | ----------------- |
|
871
|
+
| Fast Path | ~21 bytes | ~21 MB |
|
872
|
+
| Unbiased | ~21 bytes | ~21 MB |
|
873
|
+
|
874
|
+
_Memory usage is consistent regardless of algorithm choice_
|
875
|
+
|
876
|
+
### Collision Probability
|
877
|
+
|
878
|
+
For 21-character IDs with different alphabets:
|
879
|
+
|
880
|
+
| Alphabet | Characters | Collision Probability (1 in) |
|
881
|
+
| --------------------- | ---------- | ---------------------------- |
|
882
|
+
| STANDARD_ALPHABET | 64 | 2.9 ร 10^37 |
|
883
|
+
| ALPHANUMERIC_ALPHABET | 62 | 1.4 ร 10^37 |
|
884
|
+
| Numeric (0-9) | 10 | 1.0 ร 10^21 |
|
885
|
+
| Binary (0-1) | 2 | 2.1 ร 10^6 |
|
886
|
+
|
887
|
+
_Collision probability calculated using birthday paradox formula_
|
888
|
+
|
889
|
+
### Performance Characteristics
|
890
|
+
|
891
|
+
#### Fast Path (64-character alphabets)
|
892
|
+
|
893
|
+
- **Time Complexity**: O(n) where n = ID length
|
894
|
+
- **Space Complexity**: O(n)
|
895
|
+
- **Rejection Rate**: 0% (no rejections)
|
896
|
+
- **Distribution**: Perfect uniform
|
897
|
+
- **Best For**: High-performance applications, short URLs
|
898
|
+
|
899
|
+
#### Unbiased Path (other alphabets)
|
900
|
+
|
901
|
+
- **Time Complexity**: O(n ร (1 + rejection_rate)) where rejection_rate โ 0.01
|
902
|
+
- **Space Complexity**: O(n)
|
903
|
+
- **Rejection Rate**: <1% for most alphabet sizes
|
904
|
+
- **Distribution**: Perfect uniform (mathematically proven)
|
905
|
+
- **Best For**: General-purpose applications, custom alphabets
|
906
|
+
|
907
|
+
### Real-World Performance
|
908
|
+
|
909
|
+
```ruby
|
910
|
+
# Benchmark example
|
911
|
+
require 'benchmark'
|
912
|
+
|
913
|
+
# Fast path (STANDARD_ALPHABET - 64 characters)
|
914
|
+
Benchmark.measure do
|
915
|
+
1_000_000.times { OpaqueId.generate(size: 21, alphabet: OpaqueId::STANDARD_ALPHABET) }
|
916
|
+
end
|
917
|
+
# => 0.400000 seconds
|
918
|
+
|
919
|
+
# Unbiased path (ALPHANUMERIC_ALPHABET - 62 characters)
|
920
|
+
Benchmark.measure do
|
921
|
+
1_000_000.times { OpaqueId.generate(size: 21, alphabet: OpaqueId::ALPHANUMERIC_ALPHABET) }
|
922
|
+
end
|
923
|
+
# => 0.830000 seconds
|
924
|
+
```
|
925
|
+
|
926
|
+
### Performance Optimization Tips
|
927
|
+
|
928
|
+
1. **Use 64-character alphabets** when possible for maximum speed
|
929
|
+
2. **Prefer `STANDARD_ALPHABET`** over `ALPHANUMERIC_ALPHABET` for performance-critical applications
|
930
|
+
3. **Batch generation** is more efficient than individual calls
|
931
|
+
4. **Avoid very small alphabets** (2-10 characters) for high-volume applications
|
932
|
+
5. **Consider ID length** - longer IDs take proportionally more time
|
933
|
+
|
934
|
+
## Error Handling
|
935
|
+
|
936
|
+
```ruby
|
937
|
+
# Invalid size
|
938
|
+
OpaqueId.generate(size: 0)
|
939
|
+
# => raises OpaqueId::ConfigurationError
|
940
|
+
|
941
|
+
# Empty alphabet
|
942
|
+
OpaqueId.generate(alphabet: "")
|
943
|
+
# => raises OpaqueId::ConfigurationError
|
944
|
+
|
945
|
+
# Collision resolution failure
|
946
|
+
# => raises OpaqueId::GenerationError after max retry attempts
|
947
|
+
```
|
948
|
+
|
949
|
+
## Security Considerations
|
950
|
+
|
951
|
+
### Cryptographic Security
|
952
|
+
|
953
|
+
OpaqueId is designed with security as a primary concern:
|
954
|
+
|
955
|
+
- **๐ Cryptographically Secure**: Uses Ruby's `SecureRandom` for entropy generation
|
956
|
+
- **๐ฏ Unbiased Distribution**: Implements rejection sampling to eliminate modulo bias
|
957
|
+
- **๐ซ Non-Sequential**: IDs are unpredictable and don't reveal creation order
|
958
|
+
- **๐ Collision Resistant**: Automatic collision detection and resolution
|
959
|
+
- **๐ Statistically Sound**: Mathematically proven uniform distribution
|
960
|
+
|
961
|
+
### Security Best Practices
|
962
|
+
|
963
|
+
#### โ
**DO Use OpaqueId For:**
|
964
|
+
|
965
|
+
- **Public-facing identifiers** (user IDs, post IDs, order numbers)
|
966
|
+
- **API keys and authentication tokens**
|
967
|
+
- **Session identifiers and CSRF tokens**
|
968
|
+
- **File upload names and temporary URLs**
|
969
|
+
- **Webhook signatures and verification tokens**
|
970
|
+
- **Database migration identifiers**
|
971
|
+
- **Cache keys and job identifiers**
|
972
|
+
|
973
|
+
#### โ **DON'T Use OpaqueId For:**
|
974
|
+
|
975
|
+
- **Passwords or password hashes** (use proper password hashing)
|
976
|
+
- **Encryption keys** (use dedicated key generation libraries)
|
977
|
+
- **Sensitive data** (IDs are not encrypted, just opaque)
|
978
|
+
- **Sequential operations** (where order matters)
|
979
|
+
- **Very short IDs** (less than 8 characters for security-critical use cases)
|
980
|
+
|
981
|
+
### Security Recommendations
|
982
|
+
|
983
|
+
#### ID Length Guidelines
|
984
|
+
|
985
|
+
| Use Case | Minimum Length | Recommended Length | Reasoning |
|
986
|
+
| ------------------ | -------------- | ------------------ | ------------------------------- |
|
987
|
+
| **Public URLs** | 8 characters | 12-16 characters | Balance security vs. URL length |
|
988
|
+
| **API Keys** | 16 characters | 21+ characters | High security requirements |
|
989
|
+
| **Session Tokens** | 21 characters | 21+ characters | Standard security practice |
|
990
|
+
| **File Names** | 8 characters | 12+ characters | Prevent enumeration attacks |
|
991
|
+
| **Database IDs** | 12 characters | 16+ characters | Long-term security |
|
992
|
+
|
993
|
+
#### Alphabet Selection for Security
|
994
|
+
|
995
|
+
- **`STANDARD_ALPHABET`**: Best for high-security applications (64 characters = 6 bits entropy per character)
|
996
|
+
- **`ALPHANUMERIC_ALPHABET`**: Good for general use (62 characters = ~5.95 bits entropy per character)
|
997
|
+
- **Custom alphabets**: Avoid very small alphabets (< 16 characters) for security-critical use cases
|
998
|
+
|
999
|
+
#### Entropy Calculations
|
1000
|
+
|
1001
|
+
For 21-character IDs:
|
1002
|
+
|
1003
|
+
- **STANDARD_ALPHABET**: 2^126 โ 8.5 ร 10^37 possible combinations
|
1004
|
+
- **ALPHANUMERIC_ALPHABET**: 2^124 โ 2.1 ร 10^37 possible combinations
|
1005
|
+
- **Numeric (0-9)**: 2^70 โ 1.2 ร 10^21 possible combinations
|
1006
|
+
|
1007
|
+
### Threat Model Considerations
|
1008
|
+
|
1009
|
+
#### Information Disclosure
|
1010
|
+
|
1011
|
+
- **โ
OpaqueId prevents**: Sequential ID enumeration, creation time inference
|
1012
|
+
- **โ ๏ธ OpaqueId doesn't prevent**: ID guessing (use proper authentication)
|
1013
|
+
|
1014
|
+
#### Brute Force Attacks
|
1015
|
+
|
1016
|
+
- **Protection**: Extremely large ID space makes brute force impractical
|
1017
|
+
- **Recommendation**: Combine with rate limiting and authentication
|
1018
|
+
|
1019
|
+
#### Timing Attacks
|
1020
|
+
|
1021
|
+
- **Protection**: Constant-time generation algorithms
|
1022
|
+
- **Recommendation**: Use consistent ID lengths to prevent timing analysis
|
1023
|
+
|
1024
|
+
### Security Audit Checklist
|
1025
|
+
|
1026
|
+
When implementing OpaqueId in security-critical applications:
|
1027
|
+
|
1028
|
+
- [ ] **ID Length**: Using appropriate length for threat model
|
1029
|
+
- [ ] **Alphabet Choice**: Using alphabet with sufficient entropy
|
1030
|
+
- [ ] **Collision Handling**: Proper error handling for rare collisions
|
1031
|
+
- [ ] **Rate Limiting**: Implementing rate limits on ID-based endpoints
|
1032
|
+
- [ ] **Authentication**: Proper authentication before ID-based operations
|
1033
|
+
- [ ] **Logging**: Not logging sensitive IDs in plain text
|
1034
|
+
- [ ] **Database Indexing**: Proper indexing for performance and security
|
1035
|
+
- [ ] **Error Messages**: Not revealing ID existence in error messages
|
1036
|
+
|
1037
|
+
## Use Cases
|
1038
|
+
|
1039
|
+
### E-Commerce Applications
|
1040
|
+
|
1041
|
+
#### Order Management
|
1042
|
+
|
1043
|
+
```ruby
|
1044
|
+
class Order < ApplicationRecord
|
1045
|
+
include OpaqueId::Model
|
1046
|
+
|
1047
|
+
# Generate secure order numbers
|
1048
|
+
opaque_id_length 16
|
1049
|
+
opaque_id_alphabet OpaqueId::ALPHANUMERIC_ALPHABET
|
1050
|
+
end
|
1051
|
+
|
1052
|
+
# Usage
|
1053
|
+
order = Order.create!(customer_id: 123, total: 99.99)
|
1054
|
+
# => #<Order id: 1, opaque_id: "K8mN2pQ7rS9tU3vW", ...>
|
1055
|
+
|
1056
|
+
# Public-facing order tracking
|
1057
|
+
# https://store.com/orders/K8mN2pQ7rS9tU3vW
|
1058
|
+
```
|
1059
|
+
|
1060
|
+
#### Product Catalog
|
1061
|
+
|
1062
|
+
```ruby
|
1063
|
+
class Product < ApplicationRecord
|
1064
|
+
include OpaqueId::Model
|
1065
|
+
|
1066
|
+
# Shorter IDs for product URLs
|
1067
|
+
opaque_id_length 12
|
1068
|
+
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
|
1069
|
+
end
|
1070
|
+
|
1071
|
+
# Usage
|
1072
|
+
product = Product.create!(name: "Wireless Headphones", price: 199.99)
|
1073
|
+
# => #<Product id: 1, opaque_id: "aB3dE6fG9hI", ...>
|
1074
|
+
|
1075
|
+
# SEO-friendly product URLs
|
1076
|
+
# https://store.com/products/aB3dE6fG9hI
|
1077
|
+
```
|
1078
|
+
|
1079
|
+
### API Development
|
1080
|
+
|
1081
|
+
#### API Key Management
|
1082
|
+
|
1083
|
+
```ruby
|
1084
|
+
class ApiKey < ApplicationRecord
|
1085
|
+
include OpaqueId::Model
|
1086
|
+
|
1087
|
+
# Long, secure API keys
|
1088
|
+
opaque_id_length 32
|
1089
|
+
opaque_id_alphabet OpaqueId::ALPHANUMERIC_ALPHABET
|
1090
|
+
opaque_id_require_letter_start true # Start with letter for readability
|
1091
|
+
end
|
1092
|
+
|
1093
|
+
# Usage
|
1094
|
+
api_key = ApiKey.create!(user_id: 123, name: "Production API")
|
1095
|
+
# => #<ApiKey id: 1, opaque_id: "K8mN2pQ7rS9tU3vW5xY1zA4bC6dE8fG", ...>
|
1096
|
+
|
1097
|
+
# API authentication
|
1098
|
+
# Authorization: Bearer K8mN2pQ7rS9tU3vW5xY1zA4bC6dE8fG
|
1099
|
+
```
|
1100
|
+
|
1101
|
+
#### Webhook Signatures
|
1102
|
+
|
1103
|
+
```ruby
|
1104
|
+
class WebhookEvent < ApplicationRecord
|
1105
|
+
include OpaqueId::Model
|
1106
|
+
|
1107
|
+
# Unique event identifiers
|
1108
|
+
opaque_id_length 21
|
1109
|
+
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
# Usage
|
1113
|
+
event = WebhookEvent.create!(
|
1114
|
+
event_type: "payment.completed",
|
1115
|
+
payload: { order_id: "K8mN2pQ7rS9tU3vW" }
|
1116
|
+
)
|
1117
|
+
# => #<WebhookEvent id: 1, opaque_id: "aB3dE6fG9hI2jK5lM8nP", ...>
|
1118
|
+
|
1119
|
+
# Webhook delivery
|
1120
|
+
# POST https://client.com/webhooks
|
1121
|
+
# X-Event-ID: aB3dE6fG9hI2jK5lM8nP
|
1122
|
+
```
|
1123
|
+
|
1124
|
+
### Content Management Systems
|
1125
|
+
|
1126
|
+
#### Blog Posts
|
1127
|
+
|
1128
|
+
```ruby
|
1129
|
+
class Post < ApplicationRecord
|
1130
|
+
include OpaqueId::Model
|
1131
|
+
|
1132
|
+
# Medium-length IDs for blog URLs
|
1133
|
+
opaque_id_length 14
|
1134
|
+
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
|
1135
|
+
end
|
1136
|
+
|
1137
|
+
# Usage
|
1138
|
+
post = Post.create!(title: "Getting Started with OpaqueId", content: "...")
|
1139
|
+
# => #<Post id: 1, opaque_id: "aB3dE6fG9hI2jK", ...>
|
1140
|
+
|
1141
|
+
# Clean blog URLs
|
1142
|
+
# https://blog.com/posts/aB3dE6fG9hI2jK
|
1143
|
+
```
|
1144
|
+
|
1145
|
+
#### File Uploads
|
1146
|
+
|
1147
|
+
```ruby
|
1148
|
+
class Attachment < ApplicationRecord
|
1149
|
+
include OpaqueId::Model
|
1150
|
+
|
1151
|
+
# Secure file identifiers
|
1152
|
+
opaque_id_length 16
|
1153
|
+
opaque_id_alphabet OpaqueId::ALPHANUMERIC_ALPHABET
|
1154
|
+
end
|
1155
|
+
|
1156
|
+
# Usage
|
1157
|
+
attachment = Attachment.create!(
|
1158
|
+
filename: "document.pdf",
|
1159
|
+
content_type: "application/pdf"
|
1160
|
+
)
|
1161
|
+
# => #<Attachment id: 1, opaque_id: "K8mN2pQ7rS9tU3vW", ...>
|
1162
|
+
|
1163
|
+
# Secure file access
|
1164
|
+
# https://cdn.example.com/files/K8mN2pQ7rS9tU3vW
|
1165
|
+
```
|
1166
|
+
|
1167
|
+
### User Management
|
1168
|
+
|
1169
|
+
#### User Profiles
|
1170
|
+
|
1171
|
+
```ruby
|
1172
|
+
class User < ApplicationRecord
|
1173
|
+
include OpaqueId::Model
|
1174
|
+
|
1175
|
+
# Public user identifiers
|
1176
|
+
opaque_id_length 12
|
1177
|
+
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
# Usage
|
1181
|
+
user = User.create!(email: "user@example.com", name: "John Doe")
|
1182
|
+
# => #<User id: 1, opaque_id: "aB3dE6fG9hI2", ...>
|
1183
|
+
|
1184
|
+
# Public profile URLs
|
1185
|
+
# https://social.com/users/aB3dE6fG9hI2
|
1186
|
+
```
|
1187
|
+
|
1188
|
+
#### Session Management
|
1189
|
+
|
1190
|
+
```ruby
|
1191
|
+
class Session < ApplicationRecord
|
1192
|
+
include OpaqueId::Model
|
1193
|
+
|
1194
|
+
# Secure session tokens
|
1195
|
+
opaque_id_length 21
|
1196
|
+
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
# Usage
|
1200
|
+
session = Session.create!(user_id: 123, expires_at: 1.week.from_now)
|
1201
|
+
# => #<Session id: 1, opaque_id: "aB3dE6fG9hI2jK5lM8nP", ...>
|
1202
|
+
|
1203
|
+
# Session cookie
|
1204
|
+
# session_token=aB3dE6fG9hI2jK5lM8nP
|
1205
|
+
```
|
1206
|
+
|
1207
|
+
### Background Job Systems
|
1208
|
+
|
1209
|
+
#### Job Tracking
|
1210
|
+
|
1211
|
+
```ruby
|
1212
|
+
class Job < ApplicationRecord
|
1213
|
+
include OpaqueId::Model
|
1214
|
+
|
1215
|
+
# Unique job identifiers
|
1216
|
+
opaque_id_length 18
|
1217
|
+
opaque_id_alphabet OpaqueId::ALPHANUMERIC_ALPHABET
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
# Usage
|
1221
|
+
job = Job.create!(
|
1222
|
+
job_type: "email_delivery",
|
1223
|
+
status: "pending",
|
1224
|
+
payload: { user_id: 123, template: "welcome" }
|
1225
|
+
)
|
1226
|
+
# => #<Job id: 1, opaque_id: "K8mN2pQ7rS9tU3vW5x", ...>
|
1227
|
+
|
1228
|
+
# Job status API
|
1229
|
+
# GET /api/jobs/K8mN2pQ7rS9tU3vW5x/status
|
1230
|
+
```
|
1231
|
+
|
1232
|
+
### Short URL Services
|
1233
|
+
|
1234
|
+
#### URL Shortening
|
1235
|
+
|
1236
|
+
```ruby
|
1237
|
+
class ShortUrl < ApplicationRecord
|
1238
|
+
include OpaqueId::Model
|
1239
|
+
|
1240
|
+
# Very short IDs for URL shortening
|
1241
|
+
opaque_id_length 6
|
1242
|
+
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
|
1243
|
+
end
|
1244
|
+
|
1245
|
+
# Usage
|
1246
|
+
short_url = ShortUrl.create!(
|
1247
|
+
original_url: "https://very-long-url.com/path/to/resource",
|
1248
|
+
user_id: 123
|
1249
|
+
)
|
1250
|
+
# => #<ShortUrl id: 1, opaque_id: "aB3dE6", ...>
|
1251
|
+
|
1252
|
+
# Short URL
|
1253
|
+
# https://short.ly/aB3dE6
|
1254
|
+
```
|
1255
|
+
|
1256
|
+
### Real-Time Applications
|
1257
|
+
|
1258
|
+
#### Chat Rooms
|
1259
|
+
|
1260
|
+
```ruby
|
1261
|
+
class ChatRoom < ApplicationRecord
|
1262
|
+
include OpaqueId::Model
|
1263
|
+
|
1264
|
+
# Medium-length room identifiers
|
1265
|
+
opaque_id_length 10
|
1266
|
+
opaque_id_alphabet OpaqueId::STANDARD_ALPHABET
|
1267
|
+
end
|
1268
|
+
|
1269
|
+
# Usage
|
1270
|
+
room = ChatRoom.create!(name: "General Discussion", owner_id: 123)
|
1271
|
+
# => #<ChatRoom id: 1, opaque_id: "aB3dE6fG9h", ...>
|
1272
|
+
|
1273
|
+
# WebSocket connection
|
1274
|
+
# ws://chat.example.com/rooms/aB3dE6fG9h
|
1275
|
+
```
|
1276
|
+
|
1277
|
+
### Analytics and Tracking
|
1278
|
+
|
1279
|
+
#### Event Tracking
|
1280
|
+
|
1281
|
+
```ruby
|
1282
|
+
class AnalyticsEvent < ApplicationRecord
|
1283
|
+
include OpaqueId::Model
|
1284
|
+
|
1285
|
+
# Unique event identifiers
|
1286
|
+
opaque_id_length 20
|
1287
|
+
opaque_id_alphabet OpaqueId::ALPHANUMERIC_ALPHABET
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
# Usage
|
1291
|
+
event = AnalyticsEvent.create!(
|
1292
|
+
event_type: "page_view",
|
1293
|
+
user_id: 123,
|
1294
|
+
properties: { page: "/products", referrer: "google.com" }
|
1295
|
+
)
|
1296
|
+
# => #<AnalyticsEvent id: 1, opaque_id: "K8mN2pQ7rS9tU3vW5xY1", ...>
|
1297
|
+
|
1298
|
+
# Event tracking pixel
|
1299
|
+
# <img src="/track/K8mN2pQ7rS9tU3vW5xY1" />
|
1300
|
+
```
|
1301
|
+
|
1302
|
+
### Use Case Summary
|
1303
|
+
|
1304
|
+
| Use Case | ID Length | Alphabet | Reasoning |
|
1305
|
+
| ------------------- | --------- | ------------ | -------------------------------- |
|
1306
|
+
| **Order Numbers** | 16 chars | Alphanumeric | Balance security vs. readability |
|
1307
|
+
| **Product URLs** | 12 chars | Standard | SEO-friendly, secure |
|
1308
|
+
| **API Keys** | 32 chars | Alphanumeric | High security, letter start |
|
1309
|
+
| **Webhook Events** | 21 chars | Standard | Standard security practice |
|
1310
|
+
| **Blog Posts** | 14 chars | Standard | Clean URLs, good security |
|
1311
|
+
| **File Uploads** | 16 chars | Alphanumeric | Secure, collision-resistant |
|
1312
|
+
| **User Profiles** | 12 chars | Standard | Public-facing, secure |
|
1313
|
+
| **Sessions** | 21 chars | Standard | High security requirement |
|
1314
|
+
| **Background Jobs** | 18 chars | Alphanumeric | Unique, trackable |
|
1315
|
+
| **Short URLs** | 6 chars | Standard | Very short, still secure |
|
1316
|
+
| **Chat Rooms** | 10 chars | Standard | Medium length, secure |
|
1317
|
+
| **Analytics** | 20 chars | Alphanumeric | Unique, high volume |
|
1318
|
+
|
1319
|
+
## Development
|
1320
|
+
|
1321
|
+
### Prerequisites
|
1322
|
+
|
1323
|
+
- **Ruby**: 3.2.0 or higher
|
1324
|
+
- **Rails**: 8.0 or higher (for generator testing)
|
1325
|
+
- **Bundler**: Latest version
|
1326
|
+
|
1327
|
+
### Setup
|
1328
|
+
|
1329
|
+
1. **Clone the repository**:
|
1330
|
+
|
1331
|
+
```bash
|
1332
|
+
git clone https://github.com/nyaggah/opaque_id.git
|
1333
|
+
cd opaque_id
|
1334
|
+
```
|
1335
|
+
|
1336
|
+
2. **Install dependencies**:
|
1337
|
+
|
1338
|
+
```bash
|
1339
|
+
bundle install
|
1340
|
+
```
|
1341
|
+
|
1342
|
+
3. **Run the setup script**:
|
1343
|
+
```bash
|
1344
|
+
bin/setup
|
1345
|
+
```
|
1346
|
+
|
1347
|
+
### Development Commands
|
1348
|
+
|
1349
|
+
#### Testing
|
1350
|
+
|
1351
|
+
```bash
|
1352
|
+
# Run all tests
|
1353
|
+
bundle exec rake test
|
1354
|
+
|
1355
|
+
# Run specific test files
|
1356
|
+
bundle exec ruby -Itest test/opaque_id_test.rb
|
1357
|
+
bundle exec ruby -Itest test/opaque_id/model_test.rb
|
1358
|
+
bundle exec ruby -Itest test/opaque_id/generators/install_generator_test.rb
|
1359
|
+
|
1360
|
+
# Run tests with verbose output
|
1361
|
+
bundle exec rake test TESTOPTS="--verbose"
|
1362
|
+
```
|
1363
|
+
|
1364
|
+
#### Code Quality
|
1365
|
+
|
1366
|
+
```bash
|
1367
|
+
# Run RuboCop linter
|
1368
|
+
bundle exec rubocop
|
1369
|
+
|
1370
|
+
# Auto-correct RuboCop offenses
|
1371
|
+
bundle exec rubocop -a
|
1372
|
+
|
1373
|
+
# Run RuboCop on specific files
|
1374
|
+
bundle exec rubocop lib/opaque_id.rb
|
1375
|
+
```
|
1376
|
+
|
1377
|
+
#### Interactive Development
|
1378
|
+
|
1379
|
+
```bash
|
1380
|
+
# Start interactive console
|
1381
|
+
bin/console
|
1382
|
+
|
1383
|
+
# Example usage in console:
|
1384
|
+
# OpaqueId.generate
|
1385
|
+
# OpaqueId.generate(size: 10, alphabet: OpaqueId::STANDARD_ALPHABET)
|
1386
|
+
```
|
1387
|
+
|
1388
|
+
#### Local Installation
|
1389
|
+
|
1390
|
+
```bash
|
1391
|
+
# Install gem locally for testing
|
1392
|
+
bundle exec rake install
|
1393
|
+
|
1394
|
+
# Uninstall local version
|
1395
|
+
gem uninstall opaque_id
|
1396
|
+
```
|
1397
|
+
|
1398
|
+
### Project Structure
|
1399
|
+
|
1400
|
+
```
|
1401
|
+
opaque_id/
|
1402
|
+
โโโ lib/
|
1403
|
+
โ โโโ opaque_id.rb # Main module and core functionality
|
1404
|
+
โ โโโ opaque_id/
|
1405
|
+
โ โ โโโ model.rb # ActiveRecord concern
|
1406
|
+
โ โ โโโ version.rb # Version constant
|
1407
|
+
โ โโโ generators/
|
1408
|
+
โ โโโ opaque_id/
|
1409
|
+
โ โโโ install_generator.rb
|
1410
|
+
โ โโโ templates/
|
1411
|
+
โ โโโ migration.rb.tt
|
1412
|
+
โโโ test/
|
1413
|
+
โ โโโ opaque_id_test.rb # Core module tests
|
1414
|
+
โ โโโ opaque_id/
|
1415
|
+
โ โ โโโ model_test.rb # Model concern tests
|
1416
|
+
โ โ โโโ generators/
|
1417
|
+
โ โ โโโ install_generator_test.rb
|
1418
|
+
โ โโโ test_helper.rb # Test configuration
|
1419
|
+
โโโ tasks/ # Project management and documentation
|
1420
|
+
โโโ opaque_id.gemspec # Gem specification
|
1421
|
+
โโโ Gemfile # Development dependencies
|
1422
|
+
โโโ Rakefile # Rake tasks
|
1423
|
+
โโโ README.md # This file
|
1424
|
+
```
|
1425
|
+
|
1426
|
+
### Testing Strategy
|
1427
|
+
|
1428
|
+
#### Test Coverage
|
1429
|
+
|
1430
|
+
- **Core Module**: ID generation, error handling, edge cases
|
1431
|
+
- **ActiveRecord Integration**: Model callbacks, finder methods, configuration
|
1432
|
+
- **Rails Generator**: Migration generation, model modification
|
1433
|
+
- **Performance**: Statistical uniformity, benchmark tests
|
1434
|
+
- **Error Handling**: Invalid inputs, collision scenarios
|
1435
|
+
|
1436
|
+
#### Test Database
|
1437
|
+
|
1438
|
+
- Uses in-memory SQLite for fast, isolated testing
|
1439
|
+
- No external database dependencies
|
1440
|
+
- Automatic cleanup between tests
|
1441
|
+
|
1442
|
+
### Release Process
|
1443
|
+
|
1444
|
+
#### Version Management
|
1445
|
+
|
1446
|
+
1. **Update version** in `lib/opaque_id/version.rb`
|
1447
|
+
2. **Update CHANGELOG.md** with new features/fixes
|
1448
|
+
3. **Run tests** to ensure everything works
|
1449
|
+
4. **Commit changes** with conventional commit message
|
1450
|
+
5. **Create release** using rake task
|
1451
|
+
|
1452
|
+
#### Release Commands
|
1453
|
+
|
1454
|
+
```bash
|
1455
|
+
# Build and release gem
|
1456
|
+
bundle exec rake release
|
1457
|
+
|
1458
|
+
# This will:
|
1459
|
+
# 1. Build the gem
|
1460
|
+
# 2. Create a git tag
|
1461
|
+
# 3. Push to GitHub
|
1462
|
+
# 4. Push to RubyGems
|
1463
|
+
```
|
1464
|
+
|
1465
|
+
### Development Guidelines
|
1466
|
+
|
1467
|
+
#### Code Style
|
1468
|
+
|
1469
|
+
- Follow RuboCop configuration
|
1470
|
+
- Use conventional commit messages
|
1471
|
+
- Write comprehensive tests for new features
|
1472
|
+
- Document public APIs with examples
|
1473
|
+
|
1474
|
+
#### Git Workflow
|
1475
|
+
|
1476
|
+
- Use feature branches for development
|
1477
|
+
- Write descriptive commit messages
|
1478
|
+
- Keep commits focused and atomic
|
1479
|
+
- Test before committing
|
1480
|
+
|
1481
|
+
#### Performance Considerations
|
1482
|
+
|
1483
|
+
- Benchmark new features
|
1484
|
+
- Consider memory usage for high-volume scenarios
|
1485
|
+
- Test with various alphabet sizes
|
1486
|
+
- Validate statistical properties
|
1487
|
+
|
1488
|
+
## Contributing
|
1489
|
+
|
1490
|
+
### Reporting Issues
|
1491
|
+
|
1492
|
+
We welcome bug reports and feature requests! Please help us improve OpaqueId by reporting issues on GitHub:
|
1493
|
+
|
1494
|
+
- **๐ Bug Reports**: [Create an issue](https://github.com/nyaggah/opaque_id/issues/new?template=bug_report.md)
|
1495
|
+
- **๐ก Feature Requests**: [Create an issue](https://github.com/nyaggah/opaque_id/issues/new?template=feature_request.md)
|
1496
|
+
- **๐ Documentation**: [Create an issue](https://github.com/nyaggah/opaque_id/issues/new?template=documentation.md)
|
1497
|
+
|
1498
|
+
### Issue Guidelines
|
1499
|
+
|
1500
|
+
When reporting issues, please include:
|
1501
|
+
|
1502
|
+
#### For Bug Reports
|
1503
|
+
|
1504
|
+
- **Ruby version**: `ruby --version`
|
1505
|
+
- **Rails version**: `rails --version` (if applicable)
|
1506
|
+
- **OpaqueId version**: `gem list opaque_id`
|
1507
|
+
- **Steps to reproduce**: Clear, minimal steps
|
1508
|
+
- **Expected behavior**: What should happen
|
1509
|
+
- **Actual behavior**: What actually happens
|
1510
|
+
- **Error messages**: Full error output
|
1511
|
+
- **Code example**: Minimal code that reproduces the issue
|
1512
|
+
|
1513
|
+
#### For Feature Requests
|
1514
|
+
|
1515
|
+
- **Use case**: Why is this feature needed?
|
1516
|
+
- **Proposed solution**: How should it work?
|
1517
|
+
- **Alternatives considered**: What other approaches were considered?
|
1518
|
+
- **Additional context**: Any other relevant information
|
1519
|
+
|
1520
|
+
### Code of Conduct
|
1521
|
+
|
1522
|
+
This project is intended to be a safe, welcoming space for collaboration. Everyone interacting in the OpaqueId project's codebases, issue trackers, and community spaces is expected to follow the [Code of Conduct](https://github.com/nyaggah/opaque_id/blob/main/CODE_OF_CONDUCT.md).
|
1523
|
+
|
1524
|
+
### Community Guidelines
|
1525
|
+
|
1526
|
+
- **Be respectful**: Treat everyone with respect and kindness
|
1527
|
+
- **Be constructive**: Provide helpful feedback and suggestions
|
1528
|
+
- **Be patient**: Maintainers are volunteers with limited time
|
1529
|
+
- **Be specific**: Provide clear, detailed information in issues
|
1530
|
+
- **Be collaborative**: Work together to solve problems
|
1531
|
+
|
1532
|
+
### Getting Help
|
1533
|
+
|
1534
|
+
- **Documentation**: Check this README and inline code documentation
|
1535
|
+
- **Issues**: Search existing issues before creating new ones
|
1536
|
+
- **Discussions**: Use GitHub Discussions for questions and general discussion
|
1537
|
+
|
1538
|
+
## License
|
1539
|
+
|
1540
|
+
OpaqueId is released under the **MIT License**. This is a permissive open source license that allows you to use, modify, and distribute the software with minimal restrictions.
|
1541
|
+
|
1542
|
+
### License Summary
|
1543
|
+
|
1544
|
+
**You are free to:**
|
1545
|
+
|
1546
|
+
- โ
Use OpaqueId in commercial and non-commercial projects
|
1547
|
+
- โ
Modify the source code to suit your needs
|
1548
|
+
- โ
Distribute copies of the software
|
1549
|
+
- โ
Include OpaqueId in proprietary applications
|
1550
|
+
- โ
Sell products that include OpaqueId
|
1551
|
+
|
1552
|
+
**You must:**
|
1553
|
+
|
1554
|
+
- ๐ Include the original copyright notice and license text
|
1555
|
+
- ๐ Include the license in any distribution of the software
|
1556
|
+
|
1557
|
+
**You are not required to:**
|
1558
|
+
|
1559
|
+
- โ Share your modifications (though contributions are welcome)
|
1560
|
+
- โ Use the same license for your project
|
1561
|
+
- โ Provide source code for your application
|
1562
|
+
|
1563
|
+
### Full License Text
|
1564
|
+
|
1565
|
+
The complete MIT License text is available in the [LICENSE.txt](LICENSE.txt) file in this repository.
|
1566
|
+
|
1567
|
+
### License Compatibility
|
1568
|
+
|
1569
|
+
The MIT License is compatible with:
|
1570
|
+
|
1571
|
+
- **GPL**: Can be included in GPL projects
|
1572
|
+
- **Apache 2.0**: Compatible with Apache-licensed projects
|
1573
|
+
- **BSD**: Compatible with BSD-licensed projects
|
1574
|
+
- **Commercial**: Can be used in proprietary, commercial software
|
1575
|
+
|
1576
|
+
### Copyright
|
1577
|
+
|
1578
|
+
Copyright (c) 2025 Joey Doey. All rights reserved.
|
1579
|
+
|
1580
|
+
### Third-Party Licenses
|
1581
|
+
|
1582
|
+
OpaqueId uses the following dependencies:
|
1583
|
+
|
1584
|
+
- **ActiveRecord**: MIT License
|
1585
|
+
- **ActiveSupport**: MIT License
|
1586
|
+
- **SecureRandom**: Part of Ruby standard library (Ruby License)
|
1587
|
+
|
1588
|
+
### Legal Disclaimer
|
1589
|
+
|
1590
|
+
This software is provided "as is" without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose, and noninfringement.
|
1591
|
+
|
1592
|
+
## Code of Conduct
|
1593
|
+
|
1594
|
+
Everyone interacting in the OpaqueId project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nyaggah/opaque_id/blob/main/CODE_OF_CONDUCT.md).
|
1595
|
+
|
1596
|
+
## Acknowledgements
|
1597
|
+
|
1598
|
+
OpaqueId is heavily inspired by [nanoid.rb](https://github.com/radeno/nanoid.rb), which is a Ruby implementation of the original [NanoID](https://github.com/ai/nanoid) project. The core algorithm and approach to secure ID generation draws from the excellent work done by the NanoID team.
|
1599
|
+
|
1600
|
+
The motivation and use case for OpaqueId was inspired by the insights shared in ["Why we chose NanoIDs for PlanetScale's API"](https://planetscale.com/blog/why-we-chose-nanoids-for-planetscales-api) by Mike Coutermarsh, which highlights the benefits of using opaque, non-sequential identifiers in modern web applications.
|
1601
|
+
|
1602
|
+
We're grateful to the open source community for these foundational contributions that made OpaqueId possible.
|