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