api_keys 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.simplecov +36 -0
  3. data/AGENTS.md +5 -0
  4. data/Appraisals +17 -0
  5. data/CHANGELOG.md +9 -0
  6. data/CLAUDE.md +5 -0
  7. data/README.md +767 -3
  8. data/Rakefile +9 -4
  9. data/app/controllers/api_keys/keys_controller.rb +43 -11
  10. data/app/controllers/api_keys/security_controller.rb +8 -0
  11. data/app/views/api_keys/keys/_empty_state.html.erb +9 -0
  12. data/app/views/api_keys/keys/_form.html.erb +31 -2
  13. data/app/views/api_keys/keys/_key_actions.html.erb +20 -0
  14. data/app/views/api_keys/keys/_key_badges.html.erb +17 -0
  15. data/app/views/api_keys/keys/_key_row.html.erb +21 -35
  16. data/app/views/api_keys/keys/_key_status.html.erb +10 -0
  17. data/app/views/api_keys/keys/_keys_table.html.erb +2 -7
  18. data/app/views/api_keys/keys/_publishable_keys.html.erb +40 -0
  19. data/app/views/api_keys/keys/_secret_keys.html.erb +39 -0
  20. data/app/views/api_keys/keys/_show_token.html.erb +5 -1
  21. data/app/views/api_keys/keys/_token_display.html.erb +11 -0
  22. data/app/views/api_keys/keys/index.html.erb +40 -8
  23. data/app/views/api_keys/security/best_practices.html.erb +73 -47
  24. data/app/views/layouts/api_keys/application.html.erb +113 -7
  25. data/context7.json +4 -0
  26. data/gemfiles/rails_7.2.gemfile +21 -0
  27. data/gemfiles/rails_8.0.gemfile +21 -0
  28. data/gemfiles/rails_8.1.gemfile +21 -0
  29. data/lib/api_keys/configuration.rb +77 -0
  30. data/lib/api_keys/errors.rb +73 -0
  31. data/lib/api_keys/form_builder_extensions.rb +158 -0
  32. data/lib/api_keys/helpers/expiration_options.rb +131 -0
  33. data/lib/api_keys/helpers/token_session.rb +68 -0
  34. data/lib/api_keys/helpers/view_helpers.rb +216 -0
  35. data/lib/api_keys/models/api_key.rb +229 -17
  36. data/lib/api_keys/models/concerns/has_api_keys.rb +183 -3
  37. data/lib/api_keys/services/authenticator.rb +45 -2
  38. data/lib/api_keys/services/digestor.rb +6 -2
  39. data/lib/api_keys/tenant_resolution.rb +3 -1
  40. data/lib/api_keys/version.rb +1 -1
  41. data/lib/api_keys.rb +12 -0
  42. data/lib/generators/api_keys/add_key_types_generator.rb +68 -0
  43. data/lib/generators/api_keys/templates/add_key_types_to_api_keys.rb.erb +18 -0
  44. data/lib/generators/api_keys/templates/create_api_keys_table.rb.erb +9 -0
  45. data/lib/generators/api_keys/templates/initializer.rb +242 -120
  46. metadata +24 -58
@@ -1,70 +1,96 @@
1
1
  <header>
2
- <h1>API Key Security Best Practices</h1>
3
- <p>Protecting your API keys is crucial for maintaining the security and integrity of your account and data.</p>
2
+ <h1>API Key Security</h1>
3
+ <p>Protecting your API keys is crucial for maintaining the security of your account and data.</p>
4
4
  </header>
5
5
 
6
- <article class="col-5">
6
+ <article class="col-8">
7
7
 
8
- <section>
9
- <h3>1. Treat API Keys Like Passwords</h3>
10
- <p>Your API keys grant access to your account and potentially sensitive operations. Handle them with the same level of security you would apply to your account password or other critical credentials.</p>
11
- </section>
8
+ <% if key_types_feature_enabled? %>
9
+ <section class="api-keys-section">
10
+ <h2>Understanding Key Types</h2>
12
11
 
13
- <section>
14
- <h3>2. Use Unique Keys for Different Applications & Environments</h3>
15
- <p>Generate distinct API keys for different applications, services, or integrations that need access. If a key for one application is compromised, you can revoke it without disrupting others. Use separate keys for development, staging, and production environments.</p>
16
- <p><em>Tip:</em> Use the "Name" field when creating keys to easily identify their purpose (e.g., "Production Zapier Integration", "Staging iOS App").</p>
17
- </section>
12
+ <h3>Secret Keys</h3>
13
+ <p>Secret keys provide full access to your account and should be treated like passwords.</p>
14
+ <ul>
15
+ <li><strong>Never expose</strong> in client-side code (browsers, mobile apps, desktop apps)</li>
16
+ <li><strong>Never commit</strong> to version control (Git, etc.)</li>
17
+ <li><strong>Store securely</strong> using environment variables or secrets management</li>
18
+ <li><strong>Can be revoked</strong> immediately if compromised</li>
19
+ </ul>
18
20
 
19
- <section>
20
- <h3>3. Never Expose Keys in Client-Side Code</h3>
21
- <p><strong>Never</strong> embed API keys directly in mobile apps (iOS, Android), browser-side JavaScript, desktop applications, or any code that resides on a user's device. Exposed keys can be easily extracted by malicious actors.</p>
22
- <p><strong>Solution:</strong> Route API requests through your own backend server. Your server can securely store and use the API key to communicate with the target API on behalf of the client.</p>
23
- </section>
21
+ <h3>Publishable Keys</h3>
22
+ <p>Publishable keys are designed for client-side use with limited, safe permissions.</p>
23
+ <ul>
24
+ <li><strong>Safe to embed</strong> in browser JavaScript, mobile apps, and public code</li>
25
+ <li><strong>Limited access</strong> &mdash; cannot perform sensitive operations</li>
26
+ <li><strong>Always visible</strong> &mdash; you can view the full key anytime in your dashboard</li>
27
+ <li><strong>Cannot be revoked</strong> &mdash; designed to be long-lived identifiers</li>
28
+ </ul>
29
+ </section>
30
+ <% end %>
24
31
 
25
32
  <section>
26
- <h3>4. Never Commit Keys to Version Control (e.g., Git)</h3>
27
- <p>Committing keys to your source code repository (like Git, Mercurial, etc.) is a common and dangerous mistake. Even in private repositories, accidental pushes or repository breaches can leak your keys.</p>
28
- <p><strong>Solution:</strong> Store keys in environment variables or use a dedicated secrets management system. Access the key in your code via these secure methods.</p>
29
- </section>
33
+ <h2>Essential Practices</h2>
30
34
 
31
- <section>
32
- <h3>5. Securely Store Keys on Your Backend</h3>
33
- <ul>
34
- <li><strong>Environment Variables:</strong> The simplest secure method for many applications. Set an environment variable (e.g., `YOUR_SERVICE_API_KEY`) on your server and access it in your code (e.g., `ENV['YOUR_SERVICE_API_KEY']` in Ruby/Rails).</li>
35
- <li><strong>Secrets Management Services:</strong> For more robust needs, especially in production or team environments, use dedicated services like HashiCorp Vault, AWS Secrets Manager, Google Secret Manager, Doppler, etc. These provide encrypted storage, access control, auditing, and often automated rotation capabilities.</li>
36
- <li><strong>Encrypted Configuration Files:</strong> If using configuration files, ensure they are encrypted (e.g., Rails encrypted credentials `config/credentials.yml.enc` and `Rails.application.credentials`). <%= link_to "More info here", "https://guides.rubyonrails.org/security.html#custom-credentials", target: "_blank", rel: "noopener noreferrer", class: "text-primary" %>.</li>
37
- </ul>
35
+ <h3>Treat Secret Keys Like Passwords</h3>
36
+ <p>Your API keys grant access to your account. Handle them with the same care you would apply to your account password.</p>
37
+
38
+ <h3>Use Separate Keys for Different Purposes</h3>
39
+ <p>Create distinct keys for different applications and environments. If one key is compromised, you can revoke it without disrupting others.</p>
40
+ <p><em>Tip:</em> Use descriptive names like "Production Backend" or "Staging iOS App" to easily identify each key's purpose.</p>
41
+
42
+ <% unless key_types_feature_enabled? %>
43
+ <h3>Never Expose Keys in Client-Side Code</h3>
44
+ <p><strong>Never</strong> embed API keys in mobile apps, browser JavaScript, or desktop applications. Exposed keys can be easily extracted by malicious actors.</p>
45
+ <p><strong>Solution:</strong> Route API requests through your own backend server, which can securely store and use the API key.</p>
46
+ <% end %>
38
47
  </section>
39
48
 
40
49
  <section>
41
- <h3>6. Implement the Principle of Least Privilege (Scopes)</h3>
42
- <p>If the API service supports it (and this `api_keys` gem allows for scopes), create keys with only the minimum permissions (scopes) required for their specific task. Avoid using a key with full access if only read access is needed.</p>
43
- <p><em>Note:</em> Scope availability and enforcement depend on how the host application integrates and utilizes the `scopes` attribute provided by this gem.</p>
50
+ <h2>Secure Storage</h2>
51
+
52
+ <h3>Environment Variables</h3>
53
+ <p>The simplest secure method. Set an environment variable on your server:</p>
54
+ <pre><code># In your shell or deployment config
55
+ export YOUR_SERVICE_API_KEY="sk_..."
56
+
57
+ # Access in Ruby
58
+ ENV['YOUR_SERVICE_API_KEY']</code></pre>
59
+
60
+ <h3>Rails Encrypted Credentials</h3>
61
+ <p>For Rails applications, use encrypted credentials:</p>
62
+ <pre><code># Edit credentials
63
+ bin/rails credentials:edit
64
+
65
+ # Access in code
66
+ Rails.application.credentials.your_service_api_key</code></pre>
67
+ <p><%= link_to "Rails Security Guide", "https://guides.rubyonrails.org/security.html#custom-credentials", target: "_blank", rel: "noopener noreferrer", class: "text-primary" %></p>
68
+
69
+ <h3>Secrets Management Services</h3>
70
+ <p>For production environments, consider dedicated services like HashiCorp Vault, AWS Secrets Manager, or Google Secret Manager.</p>
44
71
  </section>
45
72
 
46
73
  <section>
47
- <h3>7. Monitor Usage and Rotate Keys Regularly</h3>
48
- <ul>
49
- <li><strong>Monitor Usage:</strong> Regularly check API usage logs or dashboards (if provided by the service or your monitoring tools). Look for unexpected spikes in activity or requests from unusual locations, which could indicate a compromised key.</li>
50
- <li><strong>Rotate Keys:</strong> Periodically generate new keys and revoke old ones (key rotation). This limits the window of opportunity for attackers if a key is ever leaked undetected. How often you rotate depends on your security requirements (e.g., every 90 days, annually).
51
- <br><em>Tip:</em> This dashboard allows creating multiple keys, facilitating rotation. Create a new key, update your application(s), verify they work, and then revoke the old key.</li>
52
- <li><strong>Revoke Immediately if Compromised:</strong> If you suspect a key has been leaked or compromised, revoke it immediately using the "Revoke" button on your keys dashboard.</li>
53
- </ul>
74
+ <h2>Monitoring &amp; Rotation</h2>
75
+
76
+ <h3>Monitor Usage</h3>
77
+ <p>Regularly check for unexpected activity spikes or requests from unusual locations, which could indicate a compromised key.</p>
78
+
79
+ <h3>Rotate Keys Periodically</h3>
80
+ <p>Generate new keys and revoke old ones on a regular schedule (e.g., every 90 days). This limits exposure if a key is leaked undetected.</p>
81
+ <p><em>Tip:</em> Create a new key first, update your application, verify it works, then revoke the old key.</p>
82
+
83
+ <h3>Revoke Immediately if Compromised</h3>
84
+ <p>If you suspect a key has been leaked, revoke it immediately from your dashboard.</p>
54
85
  </section>
55
86
 
56
87
  <section>
57
- <h3>8. Use HTTPS Exclusively</h3>
58
- <p>Ensure all API requests are made over HTTPS to encrypt the connection and prevent eavesdropping. Transmitting keys over unencrypted HTTP is highly insecure.</p>
88
+ <h2>Always Use HTTPS</h2>
89
+ <p>Ensure all API requests are made over HTTPS. Transmitting keys over unencrypted HTTP exposes them to eavesdropping.</p>
59
90
  </section>
60
91
 
61
92
  <hr>
62
93
 
63
- <p>By following these best practices, you significantly reduce the risk associated with API key management.</p>
64
-
65
- <%# Link back to the keys index if appropriate %>
66
- <% if defined?(api_keys.keys_path) %>
67
- <p><%= link_to "Back to API Keys", api_keys.keys_path, class: "text-primary" %></p>
68
- <% end %>
94
+ <p><%= link_to "Back to API Keys", api_keys.keys_path, class: "text-primary" %></p>
69
95
 
70
- </article>
96
+ </article>
@@ -12,15 +12,67 @@
12
12
  </head>
13
13
  <body>
14
14
  <style>
15
+ /*
16
+ * API Keys CSS Variables
17
+ * Override these in your host application to customize the dashboard appearance.
18
+ * Example in your app's CSS:
19
+ * :root {
20
+ * --api-keys-primary-color: #your-brand-color;
21
+ * --api-keys-danger-color: #your-danger-color;
22
+ * }
23
+ */
24
+ :root {
25
+ /* Colors */
26
+ --api-keys-primary-color: #007bff;
27
+ --api-keys-danger-color: #c23539;
28
+ --api-keys-success-color: #28a745;
29
+ --api-keys-warning-color: #ffc107;
30
+ --api-keys-muted-color: #6c757d;
31
+
32
+ /* Badge colors */
33
+ --api-keys-badge-secret-bg: #e7f1ff;
34
+ --api-keys-badge-secret-color: #004085;
35
+ --api-keys-badge-publishable-bg: #fef3cd;
36
+ --api-keys-badge-publishable-color: #856404;
37
+ --api-keys-badge-live-bg: #d4edda;
38
+ --api-keys-badge-live-color: #155724;
39
+ --api-keys-badge-test-bg: #f8d7da;
40
+ --api-keys-badge-test-color: #721c24;
41
+
42
+ /* Status colors */
43
+ --api-keys-status-active-color: green;
44
+ --api-keys-status-revoked-color: orange;
45
+ --api-keys-status-expired-color: red;
46
+
47
+ /* Spacing */
48
+ --api-keys-section-padding: 1.5em;
49
+ --api-keys-section-margin: 2em;
50
+ --api-keys-border-radius: 8px;
51
+
52
+ /* Typography */
53
+ --api-keys-font-family: inherit;
54
+ --api-keys-code-font-size: 0.8em;
55
+ }
56
+
15
57
  body {
16
- /* grid-template-columns: 1fr min(100rem, 90%) 1fr !important; */
58
+ font-family: var(--api-keys-font-family);
17
59
  }
18
60
 
19
61
  @media (prefers-color-scheme: dark) {
62
+ :root {
63
+ --api-keys-badge-secret-bg: #1a365d;
64
+ --api-keys-badge-secret-color: #90cdf4;
65
+ --api-keys-badge-publishable-bg: #744210;
66
+ --api-keys-badge-publishable-color: #faf089;
67
+ --api-keys-badge-live-bg: #22543d;
68
+ --api-keys-badge-live-color: #9ae6b4;
69
+ --api-keys-badge-test-bg: #742a2a;
70
+ --api-keys-badge-test-color: #feb2b2;
71
+ }
72
+
20
73
  body {
21
- /* Define dark mode variables directly */
22
- --bg-color:rgb(14, 14, 14);
23
- --bg-secondary-color:rgb(34, 34, 34);
74
+ --bg-color: rgb(14, 14, 14);
75
+ --bg-secondary-color: rgb(34, 34, 34);
24
76
  --font-color: #f5f5f5;
25
77
  --color-grey: #ccc;
26
78
  --color-darkGrey: #777;
@@ -29,7 +81,7 @@
29
81
 
30
82
  code, pre {
31
83
  color: var(--font-color);
32
- font-size: 0.8em;
84
+ font-size: var(--api-keys-code-font-size);
33
85
  }
34
86
 
35
87
  .api-keys-align-center {
@@ -52,7 +104,7 @@
52
104
  .api-keys-action-buttons button {
53
105
  background: none;
54
106
  padding: 0;
55
- color: #c23539
107
+ color: var(--api-keys-danger-color);
56
108
  }
57
109
 
58
110
  .api-keys-button-text {
@@ -77,6 +129,38 @@
77
129
  margin-top: 2.4em;
78
130
  }
79
131
 
132
+ /* API Keys sections styling */
133
+ .api-keys-section {
134
+ margin-top: var(--api-keys-section-margin);
135
+ padding: var(--api-keys-section-padding);
136
+ border: 1px solid var(--color-grey, #ccc);
137
+ border-radius: var(--api-keys-border-radius);
138
+ background-color: var(--bg-secondary-color, #f9f9f9);
139
+ }
140
+
141
+ .api-keys-section h2 {
142
+ margin-top: 0;
143
+ margin-bottom: 0.5em;
144
+ font-size: 1.25em;
145
+ }
146
+
147
+ .api-keys-section-description {
148
+ margin-bottom: 1em;
149
+ color: var(--color-darkGrey, #666);
150
+ font-size: 0.9em;
151
+ }
152
+
153
+ .api-keys-info-text {
154
+ margin-bottom: 1em;
155
+ }
156
+
157
+ @media (prefers-color-scheme: dark) {
158
+ .api-keys-section {
159
+ background-color: var(--bg-secondary-color, rgb(34, 34, 34));
160
+ border-color: var(--color-darkGrey, #555);
161
+ }
162
+ }
163
+
80
164
  </style>
81
165
 
82
166
  <nav class="nav container">
@@ -109,7 +193,29 @@
109
193
  <%# Footer content %>
110
194
  </footer>
111
195
 
112
- <%# Add any necessary JS includes if needed later %>
196
+ <script>
197
+ // Minimal JS for API Keys dashboard - event delegation for show/copy tokens
198
+ document.addEventListener('click', function(e) {
199
+ // Show token (one-way: masked -> full, no hiding back)
200
+ if (e.target.matches('.btn-show-token')) {
201
+ var cell = e.target.closest('td');
202
+ cell.querySelector('.token-masked').style.display = 'none';
203
+ cell.querySelector('.token-full').style.display = 'inline';
204
+ e.target.style.display = 'none';
205
+ cell.querySelector('.btn-copy-token').style.display = 'inline';
206
+ }
207
+
208
+ // Copy token to clipboard
209
+ if (e.target.matches('.btn-copy-token')) {
210
+ var token = e.target.getAttribute('data-token');
211
+ navigator.clipboard.writeText(token).then(function() {
212
+ var originalText = e.target.textContent;
213
+ e.target.textContent = 'Copied!';
214
+ setTimeout(function() { e.target.textContent = originalText; }, 2000);
215
+ });
216
+ }
217
+ });
218
+ </script>
113
219
 
114
220
  </body>
115
221
  </html>
data/context7.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "url": "https://context7.com/rameerez/api_keys",
3
+ "public_key": "pk_HibNJE5rTFvy1txHHXUot"
4
+ }
@@ -0,0 +1,21 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rails", "~> 7.2.3"
7
+
8
+ group :development, :test do
9
+ gem "appraisal"
10
+ gem "minitest", "~> 6.0"
11
+ gem "minitest-mock"
12
+ gem "minitest-reporters"
13
+ gem "rack-test"
14
+ gem "simplecov", require: false
15
+ gem "sqlite3", ">= 2.1"
16
+ gem "bootsnap", require: false
17
+ gem "mocha", "~> 2.0"
18
+ gem "ostruct"
19
+ end
20
+
21
+ gemspec path: "../"
@@ -0,0 +1,21 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rails", "~> 8.0"
7
+
8
+ group :development, :test do
9
+ gem "appraisal"
10
+ gem "minitest", "~> 6.0"
11
+ gem "minitest-mock"
12
+ gem "minitest-reporters"
13
+ gem "rack-test"
14
+ gem "simplecov", require: false
15
+ gem "sqlite3", ">= 2.1"
16
+ gem "bootsnap", require: false
17
+ gem "mocha", "~> 2.0"
18
+ gem "ostruct"
19
+ end
20
+
21
+ gemspec path: "../"
@@ -0,0 +1,21 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rails", "~> 8.1.2"
7
+
8
+ group :development, :test do
9
+ gem "appraisal"
10
+ gem "minitest", "~> 6.0"
11
+ gem "minitest-mock"
12
+ gem "minitest-reporters"
13
+ gem "rack-test"
14
+ gem "simplecov", require: false
15
+ gem "sqlite3", ">= 2.1"
16
+ gem "bootsnap", require: false
17
+ gem "mocha", "~> 2.0"
18
+ gem "ostruct"
19
+ end
20
+
21
+ gemspec path: "../"
@@ -55,6 +55,75 @@ module ApiKeys
55
55
  # Debugging
56
56
  attr_accessor :debug_logging
57
57
 
58
+ # Key Types & Environments (Stripe-style publishable/secret keys)
59
+ #
60
+ # @!attribute [rw] key_types
61
+ # @return [Hash] Key type definitions. Each key type has:
62
+ # - :prefix [String] Token prefix (e.g., "pk" → pk_test_)
63
+ # - :permissions [Array<String>, :all] Scope ceiling for this type
64
+ # - :revocable [Boolean] Whether keys can be revoked (default: true)
65
+ # - :limit [Integer, nil] Max keys per owner per environment (nil = unlimited)
66
+ # - :public [Boolean] If true AND revocable: false, store plaintext token in
67
+ # metadata so it can be viewed again in dashboard. Use ONLY for publishable
68
+ # keys that are designed to be embedded in distributed apps. (default: false)
69
+ # @example
70
+ # config.key_types = {
71
+ # publishable: { prefix: "pk", permissions: %w[read], revocable: false, public: true, limit: 1 },
72
+ # secret: { prefix: "sk", permissions: :all }
73
+ # }
74
+ #
75
+ # @!attribute [rw] environments
76
+ # @return [Hash] Environment definitions. Each environment has:
77
+ # - :prefix_segment [String, nil] Middle part of prefix (e.g., "test" → pk_test_)
78
+ # @example
79
+ # config.environments = { test: { prefix_segment: "test" }, live: { prefix_segment: "live" } }
80
+ #
81
+ # @!attribute [rw] current_environment
82
+ # @return [Proc, Symbol] Lambda or symbol returning current environment
83
+ # @example
84
+ # config.current_environment = -> { Rails.env.production? ? :live : :test }
85
+ #
86
+ # @!attribute [rw] strict_environment_isolation
87
+ # @return [Boolean] If true, keys only work in their matching environment
88
+ #
89
+ # @!attribute [rw] default_key_type
90
+ # @return [Symbol, nil] Default key type when not specified in create_api_key!
91
+ # @example
92
+ # config.default_key_type = :secret
93
+ #
94
+ # @!attribute [rw] dashboard_allow_cross_environment
95
+ # @return [Boolean] If true, dashboard shows keys from all environments.
96
+ # If false (default), dashboard only shows keys matching current_environment.
97
+ # @example
98
+ # config.dashboard_allow_cross_environment = false
99
+ attr_accessor :environments, :current_environment, :strict_environment_isolation,
100
+ :default_key_type, :dashboard_allow_cross_environment
101
+
102
+ # Custom writer for key_types that validates prefix uniqueness
103
+ attr_reader :key_types
104
+
105
+ # Sets the key types configuration with prefix collision validation.
106
+ # @param value [Hash] Key type definitions
107
+ # @raise [ArgumentError] If multiple key types share the same prefix
108
+ def key_types=(value)
109
+ validate_key_type_prefixes!(value) if value.is_a?(Hash) && value.any?
110
+ @key_types = value
111
+ end
112
+
113
+ private
114
+
115
+ # Validates that all key type prefixes are unique to prevent token collision
116
+ def validate_key_type_prefixes!(key_types_hash)
117
+ prefixes = key_types_hash.map { |_type, config| config[:prefix] }.compact
118
+ duplicates = prefixes.group_by(&:itself).select { |_k, v| v.size > 1 }.keys
119
+
120
+ if duplicates.any?
121
+ raise ArgumentError, "Key type prefixes must be unique. Duplicate prefix(es): #{duplicates.join(', ')}"
122
+ end
123
+ end
124
+
125
+ public
126
+
58
127
  # == Initialization ==
59
128
 
60
129
  def initialize
@@ -127,6 +196,14 @@ module ApiKeys
127
196
 
128
197
  # Tenant Resolution
129
198
  @tenant_resolver = ->(api_key) { api_key.owner if api_key.respond_to?(:owner) }
199
+
200
+ # Key Types & Environments (default to empty/disabled for backwards compatibility)
201
+ @key_types = {} # Empty = feature disabled, legacy behavior
202
+ @environments = {} # Empty = no environment-based prefixes
203
+ @current_environment = -> { :default } # Default environment detection
204
+ @strict_environment_isolation = false # Don't enforce environment isolation by default
205
+ @default_key_type = nil # No default key type (must be specified explicitly)
206
+ @dashboard_allow_cross_environment = false # Dashboard shows only current environment's keys
130
207
  end
131
208
  end
132
209
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ApiKeys
4
+ module Errors
5
+ # Base error class for all ApiKeys errors
6
+ class BaseError < StandardError; end
7
+
8
+ # Raised when attempting to revoke or destroy a non-revocable key
9
+ class KeyNotRevocableError < BaseError
10
+ def initialize(message = "This API key cannot be revoked or deleted")
11
+ super
12
+ end
13
+ end
14
+
15
+ # Raised when API key environment doesn't match current environment (strict isolation)
16
+ class EnvironmentMismatchError < BaseError
17
+ attr_reader :key_environment, :current_environment
18
+
19
+ def initialize(key_environment:, current_environment:)
20
+ @key_environment = key_environment
21
+ @current_environment = current_environment
22
+ super("API key environment (#{key_environment}) does not match current environment (#{current_environment})")
23
+ end
24
+ end
25
+
26
+ # Raised when an invalid key type is specified
27
+ class InvalidKeyTypeError < BaseError
28
+ attr_reader :key_type, :valid_types
29
+
30
+ def initialize(key_type:, valid_types:)
31
+ @key_type = key_type
32
+ @valid_types = valid_types
33
+ super("Invalid key type '#{key_type}'. Valid types: #{valid_types.join(', ')}")
34
+ end
35
+ end
36
+
37
+ # Raised when an invalid environment is specified
38
+ class InvalidEnvironmentError < BaseError
39
+ attr_reader :environment, :valid_environments
40
+
41
+ def initialize(environment:, valid_environments:)
42
+ @environment = environment
43
+ @valid_environments = valid_environments
44
+ super("Invalid environment '#{environment}'. Valid environments: #{valid_environments.join(', ')}")
45
+ end
46
+ end
47
+
48
+ # Raised when key limit per type/environment is exceeded
49
+ class KeyLimitExceededError < BaseError
50
+ attr_reader :key_type, :environment, :limit
51
+
52
+ def initialize(key_type:, environment:, limit:)
53
+ @key_type = key_type
54
+ @environment = environment
55
+ @limit = limit
56
+ super("Maximum number of #{key_type} keys (#{limit}) reached for #{environment} environment")
57
+ end
58
+ end
59
+
60
+ # Raised when key_types are configured but required database columns are missing
61
+ class MigrationRequiredError < BaseError
62
+ attr_reader :missing_columns
63
+
64
+ def initialize(missing_columns:)
65
+ @missing_columns = missing_columns
66
+ super(
67
+ "Key types are configured but required database columns are missing: #{missing_columns.join(', ')}. " \
68
+ "Run: rails generate api_keys:add_key_types && rails db:migrate"
69
+ )
70
+ end
71
+ end
72
+ end
73
+ end