opaque_id 1.6.0 → 1.7.7

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