better_auth-api-key 0.2.1 → 0.5.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.
@@ -3,765 +3,204 @@
3
3
  require "json"
4
4
  require "securerandom"
5
5
  require "time"
6
+ require_relative "../api_key/error_codes"
7
+ require_relative "../api_key/types"
8
+ require_relative "../api_key/utils"
9
+ require_relative "../api_key/rate_limit"
10
+ require_relative "../api_key/keys"
11
+ require_relative "../api_key/adapter"
12
+ require_relative "../api_key/schema"
13
+ require_relative "../api_key/org_authorization"
14
+ require_relative "../api_key/validation"
15
+ require_relative "../api_key/configuration"
16
+ require_relative "../api_key/session"
17
+ require_relative "../api_key/plugin_factory"
18
+ require_relative "../api_key/routes/index"
19
+ require_relative "../api_key/routes/create_api_key"
20
+ require_relative "../api_key/routes/verify_api_key"
21
+ require_relative "../api_key/routes/get_api_key"
22
+ require_relative "../api_key/routes/update_api_key"
23
+ require_relative "../api_key/routes/delete_api_key"
24
+ require_relative "../api_key/routes/list_api_keys"
25
+ require_relative "../api_key/routes/delete_all_expired_api_keys"
6
26
 
7
27
  module BetterAuth
8
28
  module Plugins
9
29
  singleton_class.remove_method(:api_key) if singleton_class.method_defined?(:api_key)
10
30
  remove_method(:api_key) if method_defined?(:api_key) || private_method_defined?(:api_key)
11
31
 
12
- API_KEY_ERROR_CODES = {
13
- "INVALID_METADATA_TYPE" => "metadata must be an object or undefined",
14
- "REFILL_AMOUNT_AND_INTERVAL_REQUIRED" => "refillAmount is required when refillInterval is provided",
15
- "REFILL_INTERVAL_AND_AMOUNT_REQUIRED" => "refillInterval is required when refillAmount is provided",
16
- "USER_BANNED" => "User is banned",
17
- "UNAUTHORIZED_SESSION" => "Unauthorized or invalid session",
18
- "KEY_NOT_FOUND" => "API Key not found",
19
- "KEY_DISABLED" => "API Key is disabled",
20
- "KEY_EXPIRED" => "API Key has expired",
21
- "USAGE_EXCEEDED" => "API Key has reached its usage limit",
22
- "KEY_NOT_RECOVERABLE" => "API Key is not recoverable",
23
- "EXPIRES_IN_IS_TOO_SMALL" => "The expiresIn is smaller than the predefined minimum value.",
24
- "EXPIRES_IN_IS_TOO_LARGE" => "The expiresIn is larger than the predefined maximum value.",
25
- "INVALID_REMAINING" => "The remaining count is either too large or too small.",
26
- "INVALID_PREFIX_LENGTH" => "The prefix length is either too large or too small.",
27
- "INVALID_NAME_LENGTH" => "The name length is either too large or too small.",
28
- "METADATA_DISABLED" => "Metadata is disabled.",
29
- "RATE_LIMIT_EXCEEDED" => "Rate limit exceeded.",
30
- "NO_VALUES_TO_UPDATE" => "No values to update.",
31
- "KEY_DISABLED_EXPIRATION" => "Custom key expiration values are disabled.",
32
- "INVALID_API_KEY" => "Invalid API key.",
33
- "INVALID_USER_ID_FROM_API_KEY" => "The user id from the API key is invalid.",
34
- "INVALID_REFERENCE_ID_FROM_API_KEY" => "The reference id from the API key is invalid.",
35
- "INVALID_API_KEY_GETTER_RETURN_TYPE" => "API Key getter returned an invalid key type. Expected string.",
36
- "SERVER_ONLY_PROPERTY" => "The property you're trying to set can only be set from the server auth instance only.",
37
- "FAILED_TO_UPDATE_API_KEY" => "Failed to update API key",
38
- "NAME_REQUIRED" => "API Key name is required.",
39
- "ORGANIZATION_ID_REQUIRED" => "Organization ID is required for organization-owned API keys.",
40
- "USER_NOT_MEMBER_OF_ORGANIZATION" => "You are not a member of the organization that owns this API key.",
41
- "INSUFFICIENT_API_KEY_PERMISSIONS" => "You do not have permission to perform this action on organization API keys.",
42
- "NO_DEFAULT_API_KEY_CONFIGURATION_FOUND" => "No default api-key configuration found.",
43
- "ORGANIZATION_PLUGIN_REQUIRED" => "Organization plugin is required for organization-owned API keys. Please install and configure the organization plugin."
44
- }.freeze
45
-
46
- API_KEY_TABLE_NAME = "apikey"
32
+ API_KEY_ERROR_CODES = BetterAuth::APIKey::ERROR_CODES
33
+
34
+ API_KEY_TABLE_NAME = BetterAuth::APIKey::Types::API_KEY_TABLE_NAME
47
35
 
48
36
  module_function
49
37
 
50
38
  def default_api_key_hasher(key)
51
- Crypto.sha256(key.to_s, encoding: :base64url)
39
+ BetterAuth::APIKey::Keys.default_hasher(key)
52
40
  end
53
41
 
54
42
  def api_key(configurations = {}, options = nil)
55
- config = api_key_config(configurations, options)
56
- Plugin.new(
57
- id: "api-key",
58
- version: BetterAuth::APIKey::VERSION,
59
- hooks: {
60
- before: [
61
- {
62
- matcher: ->(ctx) { !!api_key_session_header_config(ctx, config) },
63
- handler: ->(ctx) { api_key_session_hook(ctx, config) }
64
- }
65
- ]
66
- },
67
- endpoints: {
68
- create_api_key: api_key_create_endpoint(config),
69
- verify_api_key: api_key_verify_endpoint(config),
70
- get_api_key: api_key_get_endpoint(config),
71
- update_api_key: api_key_update_endpoint(config),
72
- delete_api_key: api_key_delete_endpoint(config),
73
- list_api_keys: api_key_list_endpoint(config),
74
- delete_all_expired_api_keys: api_key_delete_expired_endpoint(config)
75
- },
76
- schema: api_key_schema(config, config[:schema]),
77
- error_codes: API_KEY_ERROR_CODES,
78
- options: config
79
- )
43
+ BetterAuth::APIKey::PluginFactory.build(configurations, options)
80
44
  end
81
45
 
82
46
  def api_key_config(configurations, options = nil)
83
- if configurations.is_a?(Array)
84
- normalized_configs = configurations.map { |config| api_key_single_config(config) }
85
- if normalized_configs.any? { |config| config[:config_id].to_s.empty? }
86
- raise Error, "configId is required for each API key configuration in the api-key plugin."
87
- end
88
- config_ids = normalized_configs.map { |config| config[:config_id] }
89
- raise Error, "configId must be unique for each API key configuration in the api-key plugin." if config_ids.uniq.length != config_ids.length
90
-
91
- plugin_options = normalize_hash(options || {})
92
- default_config = normalized_configs.find { |config| api_key_default_config_id?(config[:config_id]) }
93
- default_config ||= normalized_configs.first
94
- default_config.merge(
95
- configurations: normalized_configs,
96
- schema: plugin_options[:schema] || default_config[:schema]
97
- )
98
- else
99
- config = api_key_single_config(configurations)
100
- config[:config_id] ||= "default"
101
- config.merge(configurations: [config])
102
- end
47
+ BetterAuth::APIKey::Configuration.normalize(configurations, options)
103
48
  end
104
49
 
105
50
  def api_key_single_config(options)
106
- data = normalize_hash(options || {})
107
- rate_limit_options = data[:rate_limit] || {}
108
- starting_characters_options = data[:starting_characters_config] || {}
109
- {
110
- config_id: data[:config_id],
111
- api_key_headers: data[:api_key_headers] || "x-api-key",
112
- default_key_length: data[:default_key_length] || 64,
113
- default_prefix: data[:default_prefix],
114
- maximum_prefix_length: data.key?(:maximum_prefix_length) ? data[:maximum_prefix_length] : 32,
115
- minimum_prefix_length: data.key?(:minimum_prefix_length) ? data[:minimum_prefix_length] : 1,
116
- maximum_name_length: data.key?(:maximum_name_length) ? data[:maximum_name_length] : 32,
117
- minimum_name_length: data.key?(:minimum_name_length) ? data[:minimum_name_length] : 1,
118
- enable_metadata: data[:enable_metadata] || false,
119
- disable_key_hashing: data[:disable_key_hashing] || false,
120
- require_name: data[:require_name] || false,
121
- storage: data[:storage] || "database",
122
- rate_limit: {
123
- enabled: rate_limit_options.fetch(:enabled, true),
124
- time_window: rate_limit_options[:time_window] || 86_400_000,
125
- max_requests: rate_limit_options[:max_requests] || 10
126
- },
127
- key_expiration: {
128
- default_expires_in: data.dig(:key_expiration, :default_expires_in),
129
- disable_custom_expires_time: data.dig(:key_expiration, :disable_custom_expires_time) || false,
130
- max_expires_in: data.dig(:key_expiration, :max_expires_in) || 365,
131
- min_expires_in: data.dig(:key_expiration, :min_expires_in) || 1
132
- },
133
- starting_characters_config: {
134
- should_store: starting_characters_options.fetch(:should_store, true),
135
- characters_length: starting_characters_options[:characters_length] || 6
136
- },
137
- enable_session_for_api_keys: data[:enable_session_for_api_keys] || false,
138
- fallback_to_database: data[:fallback_to_database] || false,
139
- custom_storage: data[:custom_storage],
140
- custom_key_generator: data[:custom_key_generator],
141
- custom_api_key_getter: data[:custom_api_key_getter],
142
- custom_api_key_validator: data[:custom_api_key_validator],
143
- default_permissions: data[:default_permissions],
144
- permissions: data[:permissions] || {},
145
- references: data[:references] || "user",
146
- defer_updates: data[:defer_updates] || false,
147
- schema: data[:schema]
148
- }
51
+ BetterAuth::APIKey::Configuration.single(options)
149
52
  end
150
53
 
151
54
  def api_key_schema(config, custom_schema = nil)
152
- base = {
153
- apikey: {
154
- fields: {
155
- configId: {type: "string", required: true, default_value: "default", index: true},
156
- name: {type: "string", required: false},
157
- start: {type: "string", required: false},
158
- prefix: {type: "string", required: false},
159
- key: {type: "string", required: true, index: true},
160
- referenceId: {type: "string", required: true, index: true},
161
- refillInterval: {type: "number", required: false},
162
- refillAmount: {type: "number", required: false},
163
- lastRefillAt: {type: "date", required: false},
164
- enabled: {type: "boolean", required: false, default_value: true},
165
- rateLimitEnabled: {type: "boolean", required: false, default_value: true},
166
- rateLimitTimeWindow: {type: "number", required: false, default_value: config[:rate_limit][:time_window]},
167
- rateLimitMax: {type: "number", required: false, default_value: config[:rate_limit][:max_requests]},
168
- requestCount: {type: "number", required: false, default_value: 0},
169
- remaining: {type: "number", required: false},
170
- lastRequest: {type: "date", required: false},
171
- expiresAt: {type: "date", required: false},
172
- createdAt: {type: "date", required: true},
173
- updatedAt: {type: "date", required: true},
174
- permissions: {type: "string", required: false},
175
- metadata: {type: "string", required: false}
176
- }
177
- }
178
- }
179
- deep_merge_hashes(base, normalize_hash(custom_schema || {}))
55
+ BetterAuth::APIKey::SchemaDefinition.schema(config, custom_schema)
180
56
  end
181
57
 
182
58
  def api_key_create_endpoint(config)
183
- Endpoint.new(path: "/api-key/create", method: "POST") do |ctx|
184
- body = api_key_normalize_body(ctx.body)
185
- resolved_config = api_key_resolve_config(ctx.context, config, body[:config_id])
186
- session = Routes.current_session(ctx, allow_nil: true)
187
- reference_id = api_key_create_reference_id!(ctx, body, session, resolved_config)
188
-
189
- api_key_validate_create_update!(body, resolved_config, create: true, client: !ctx.headers.empty?)
190
- key_prefix = body.key?(:prefix) ? body[:prefix] : resolved_config[:default_prefix]
191
- key = api_key_generate_key(resolved_config, key_prefix)
192
- now = Time.now
193
- hashed = api_key_hash(key, resolved_config)
194
- data = {
195
- configId: resolved_config[:config_id] || "default",
196
- name: body[:name],
197
- start: resolved_config[:starting_characters_config][:should_store] ? key[0, resolved_config[:starting_characters_config][:characters_length].to_i] : nil,
198
- prefix: key_prefix,
199
- key: hashed,
200
- referenceId: reference_id,
201
- enabled: true,
202
- rateLimitEnabled: body.key?(:rate_limit_enabled) ? body[:rate_limit_enabled] : resolved_config[:rate_limit][:enabled],
203
- rateLimitTimeWindow: body[:rate_limit_time_window] || resolved_config[:rate_limit][:time_window],
204
- rateLimitMax: body[:rate_limit_max] || resolved_config[:rate_limit][:max_requests],
205
- requestCount: 0,
206
- remaining: body.key?(:remaining) ? body[:remaining] : nil,
207
- refillAmount: body[:refill_amount],
208
- refillInterval: body[:refill_interval],
209
- lastRefillAt: nil,
210
- expiresAt: api_key_expires_at(body, resolved_config),
211
- createdAt: now,
212
- updatedAt: now,
213
- permissions: api_key_encode_json(body[:permissions] || api_key_default_permissions(resolved_config, reference_id, ctx)),
214
- metadata: body.key?(:metadata) ? api_key_encode_json(body[:metadata]) : nil
215
- }
216
- record = api_key_store(ctx, data, resolved_config)
217
- api_key_public(record, reveal_key: key, include_key_field: true)
218
- end
59
+ BetterAuth::APIKey::Routes::CreateAPIKey.endpoint(config)
219
60
  end
220
61
 
221
62
  def api_key_verify_endpoint(config)
222
- Endpoint.new(path: "/api-key/verify", method: "POST") do |ctx|
223
- body = normalize_hash(ctx.body)
224
- resolved_config = api_key_resolve_config(ctx.context, config, body[:config_id])
225
- key = body[:key]
226
- raise APIError.new("FORBIDDEN", message: API_KEY_ERROR_CODES["INVALID_API_KEY"], code: "INVALID_API_KEY") if key.to_s.empty?
227
-
228
- if resolved_config[:custom_api_key_validator].respond_to?(:call) && !resolved_config[:custom_api_key_validator].call({ctx: ctx, key: key})
229
- ctx.json({valid: false, error: {message: API_KEY_ERROR_CODES["INVALID_API_KEY"], code: "KEY_NOT_FOUND"}, key: nil})
230
- else
231
- record = api_key_validate!(ctx, key, resolved_config, permissions: body[:permissions])
232
- record_config = api_key_resolve_config(ctx.context, config, api_key_record_config_id(record))
233
- api_key_schedule_cleanup(ctx, record_config)
234
- ctx.json({valid: true, error: nil, key: api_key_public(record, include_key_field: false)})
235
- end
236
- rescue APIError => error
237
- ctx.context.logger.error("Failed to validate API key: #{error.message}") if ctx.context.logger.respond_to?(:error)
238
- ctx.json({valid: false, error: api_key_error_payload(error), key: nil})
239
- rescue => error
240
- ctx.context.logger.error("Failed to validate API key: #{error.message}") if ctx.context.logger.respond_to?(:error)
241
- ctx.json({valid: false, error: {message: API_KEY_ERROR_CODES["INVALID_API_KEY"], code: "INVALID_API_KEY"}, key: nil})
242
- end
63
+ BetterAuth::APIKey::Routes::VerifyAPIKey.endpoint(config)
243
64
  end
244
65
 
245
66
  def api_key_get_endpoint(config)
246
- Endpoint.new(path: "/api-key/get", method: "GET") do |ctx|
247
- session = Routes.current_session(ctx)
248
- query = normalize_hash(ctx.query)
249
- resolved_config = api_key_resolve_config(ctx.context, config, query[:config_id])
250
- id = query[:id]
251
- record = api_key_find_by_id(ctx, id, resolved_config)
252
- raise APIError.new("NOT_FOUND", message: API_KEY_ERROR_CODES["KEY_NOT_FOUND"]) unless record && api_key_config_id_matches?(api_key_record_config_id(record), resolved_config[:config_id])
253
-
254
- record_config = api_key_resolve_config(ctx.context, config, api_key_record_config_id(record))
255
- api_key_authorize_reference!(ctx, record_config, session[:user]["id"], api_key_record_reference_id(record), "read")
256
-
257
- record = api_key_migrate_legacy_metadata(ctx, record, record_config)
258
- api_key_delete_expired(ctx.context, record_config)
259
- ctx.json(api_key_public(record, include_key_field: false))
260
- end
67
+ BetterAuth::APIKey::Routes::GetAPIKey.endpoint(config)
261
68
  end
262
69
 
263
70
  def api_key_update_endpoint(config)
264
- Endpoint.new(path: "/api-key/update", method: "POST") do |ctx|
265
- body = api_key_normalize_body(ctx.body)
266
- resolved_config = api_key_resolve_config(ctx.context, config, body[:config_id])
267
- session = Routes.current_session(ctx, allow_nil: true)
268
- user_id = session&.dig(:user, "id") || body[:user_id]
269
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"]) unless user_id
270
- if session && body[:user_id] && body[:user_id] != session[:user]["id"]
271
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"])
272
- end
273
-
274
- key_id = body[:key_id]
275
- record = api_key_find_by_id(ctx, key_id, resolved_config)
276
- raise APIError.new("NOT_FOUND", message: API_KEY_ERROR_CODES["KEY_NOT_FOUND"]) unless record
277
- raise APIError.new("NOT_FOUND", message: API_KEY_ERROR_CODES["KEY_NOT_FOUND"]) unless api_key_config_id_matches?(api_key_record_config_id(record), resolved_config[:config_id])
278
-
279
- record_config = api_key_resolve_config(ctx.context, config, api_key_record_config_id(record))
280
- api_key_authorize_reference!(ctx, record_config, user_id, api_key_record_reference_id(record), "update")
281
-
282
- api_key_validate_create_update!(body, record_config, create: false, client: api_key_auth_required?(ctx))
283
- update = api_key_update_payload(body, record_config)
284
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["NO_VALUES_TO_UPDATE"]) if update.empty?
285
-
286
- updated = api_key_update_record(ctx, record, update.merge(updatedAt: Time.now), record_config)
287
- updated = api_key_migrate_legacy_metadata(ctx, updated, record_config)
288
- api_key_delete_expired(ctx.context, record_config)
289
- ctx.json(api_key_public(updated, include_key_field: false))
290
- end
71
+ BetterAuth::APIKey::Routes::UpdateAPIKey.endpoint(config)
291
72
  end
292
73
 
293
74
  def api_key_delete_endpoint(config)
294
- Endpoint.new(path: "/api-key/delete", method: "POST") do |ctx|
295
- session = Routes.current_session(ctx)
296
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["USER_BANNED"]) if session[:user]["banned"] == true
297
-
298
- body = normalize_hash(ctx.body)
299
- resolved_config = api_key_resolve_config(ctx.context, config, body[:config_id])
300
- key_id = body[:key_id]
301
- record = api_key_find_by_id(ctx, key_id, resolved_config)
302
- raise APIError.new("NOT_FOUND", message: API_KEY_ERROR_CODES["KEY_NOT_FOUND"]) unless record && api_key_config_id_matches?(api_key_record_config_id(record), resolved_config[:config_id])
303
-
304
- record_config = api_key_resolve_config(ctx.context, config, api_key_record_config_id(record))
305
- api_key_authorize_reference!(ctx, record_config, session[:user]["id"], api_key_record_reference_id(record), "delete")
306
-
307
- api_key_delete_record(ctx, record, record_config)
308
- api_key_delete_expired(ctx.context, record_config)
309
- ctx.json({success: true})
310
- end
75
+ BetterAuth::APIKey::Routes::DeleteAPIKey.endpoint(config)
311
76
  end
312
77
 
313
78
  def api_key_list_endpoint(config)
314
- Endpoint.new(path: "/api-key/list", method: "GET") do |ctx|
315
- session = Routes.current_session(ctx)
316
- query = normalize_hash(ctx.query)
317
- api_key_validate_list_query!(query)
318
- configs = query[:config_id] ? [api_key_resolve_config(ctx.context, config, query[:config_id])] : config.fetch(:configurations, [config])
319
- reference_id = query[:organization_id] || session[:user]["id"]
320
- expected_reference = query[:organization_id] ? "organization" : "user"
321
- api_key_check_org_permission!(ctx, session[:user]["id"], reference_id, "read") if query[:organization_id]
322
- records = configs.flat_map { |entry| api_key_list_for_reference(ctx, reference_id, entry) }.uniq { |record| record["id"] }
323
- records = records.select do |record|
324
- record_config = api_key_resolve_config(ctx.context, config, api_key_record_config_id(record))
325
- record_config[:references].to_s == expected_reference &&
326
- api_key_record_reference_id(record) == reference_id &&
327
- (!query[:config_id] || api_key_config_id_matches?(api_key_record_config_id(record), query[:config_id]))
328
- end
329
- total = records.length
330
- records = api_key_sort_records(records, query[:sort_by], query[:sort_direction])
331
- offset = query.key?(:offset) ? query[:offset].to_i : nil
332
- limit = query.key?(:limit) ? query[:limit].to_i : nil
333
- records = records.drop(offset) if offset
334
- records = records.first(limit) if limit
335
- records.each { |record| api_key_delete_expired(ctx.context, api_key_resolve_config(ctx.context, config, api_key_record_config_id(record))) }
336
- api_keys = records.map do |record|
337
- record_config = api_key_resolve_config(ctx.context, config, api_key_record_config_id(record))
338
- api_key_public(api_key_migrate_legacy_metadata(ctx, record, record_config), include_key_field: false)
339
- end
340
- ctx.json({apiKeys: api_keys, total: total, limit: limit, offset: offset})
341
- end
79
+ BetterAuth::APIKey::Routes::ListAPIKeys.endpoint(config)
342
80
  end
343
81
 
344
82
  def api_key_delete_expired_endpoint(config)
345
- Endpoint.new(path: "/api-key/delete-all-expired-api-keys", method: "POST") do |ctx|
346
- api_key_delete_expired(ctx.context, config, bypass_last_check: true)
347
- ctx.json({success: true, error: nil})
348
- rescue => error
349
- ctx.context.logger.error("[API KEY PLUGIN] Failed to delete expired API keys: #{error.message}") if ctx.context.logger.respond_to?(:error)
350
- ctx.json({success: false, error: error})
351
- end
83
+ BetterAuth::APIKey::Routes::DeleteAllExpiredAPIKeys.endpoint(config)
352
84
  end
353
85
 
354
86
  def api_key_resolve_config(context, config, config_id = nil)
355
- configurations = config.fetch(:configurations, [config])
356
- return configurations.find { |entry| api_key_default_config_id?(entry[:config_id]) } || configurations.first if config_id.to_s.empty?
357
-
358
- configurations.find { |entry| entry[:config_id].to_s == config_id.to_s } ||
359
- begin
360
- default = configurations.find { |entry| api_key_default_config_id?(entry[:config_id]) }
361
- unless default
362
- context.logger.error(API_KEY_ERROR_CODES["NO_DEFAULT_API_KEY_CONFIGURATION_FOUND"]) if context.respond_to?(:logger) && context.logger.respond_to?(:error)
363
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["NO_DEFAULT_API_KEY_CONFIGURATION_FOUND"])
364
- end
365
- default
366
- end
87
+ BetterAuth::APIKey::Routes.resolve_config(context, config, config_id)
367
88
  end
368
89
 
369
90
  def api_key_default_config_id?(value)
370
- value.nil? || value.to_s.empty? || value.to_s == "default"
91
+ BetterAuth::APIKey::Routes.default_config_id?(value)
371
92
  end
372
93
 
373
94
  def api_key_config_id_matches?(record_config_id, expected_config_id)
374
- return true if api_key_default_config_id?(record_config_id) && api_key_default_config_id?(expected_config_id)
375
-
376
- record_config_id.to_s == expected_config_id.to_s
95
+ BetterAuth::APIKey::Routes.config_id_matches?(record_config_id, expected_config_id)
377
96
  end
378
97
 
379
98
  def api_key_create_reference_id!(ctx, body, session, config)
380
- if config[:references].to_s == "organization"
381
- organization_id = body[:organization_id]
382
- if organization_id.to_s.empty?
383
- raise APIError.new(
384
- "BAD_REQUEST",
385
- message: API_KEY_ERROR_CODES["ORGANIZATION_ID_REQUIRED"],
386
- code: "ORGANIZATION_ID_REQUIRED"
387
- )
388
- end
389
-
390
- user_id = session&.dig(:user, "id") || body[:user_id]
391
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"]) if user_id.to_s.empty?
392
-
393
- api_key_check_org_permission!(ctx, user_id, organization_id, "create")
394
- organization_id
395
- elsif session && body[:user_id] && body[:user_id] != session[:user]["id"]
396
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"])
397
- elsif session
398
-
399
- session[:user]["id"]
400
- else
401
- user_id = body[:user_id]
402
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"]) if user_id.to_s.empty?
403
-
404
- user_id
405
- end
99
+ BetterAuth::APIKey::OrgAuthorization.create_reference_id!(ctx, body, session, config)
406
100
  end
407
101
 
408
102
  def api_key_record_reference_id(record)
409
- record["referenceId"] || record[:referenceId] || record["userId"] || record[:userId]
103
+ BetterAuth::APIKey::Types.record_reference_id(record)
410
104
  end
411
105
 
412
106
  def api_key_record_user_id(record)
413
- record["userId"] || record[:userId] || (api_key_default_config_id?(record["configId"] || record[:configId]) && (record["referenceId"] || record[:referenceId]))
107
+ BetterAuth::APIKey::Types.record_user_id(record)
414
108
  end
415
109
 
416
110
  def api_key_record_config_id(record)
417
- record["configId"] || record[:configId] || "default"
111
+ BetterAuth::APIKey::Types.record_config_id(record)
418
112
  end
419
113
 
420
114
  def api_key_default_permissions(config, reference_id, ctx)
421
- permissions = config.dig(:permissions, :default_permissions) || config[:default_permissions]
422
- return permissions.call(reference_id, ctx) if permissions.respond_to?(:call)
423
-
424
- permissions
115
+ BetterAuth::APIKey::Types.default_permissions(config, reference_id, ctx)
425
116
  end
426
117
 
427
118
  def api_key_authorize_reference!(ctx, config, user_id, reference_id, action)
428
- if config[:references].to_s == "organization"
429
- api_key_check_org_permission!(ctx, user_id, reference_id, action)
430
- elsif reference_id != user_id
431
- raise APIError.new("NOT_FOUND", message: API_KEY_ERROR_CODES["KEY_NOT_FOUND"])
432
- end
119
+ BetterAuth::APIKey::OrgAuthorization.authorize_reference!(ctx, config, user_id, reference_id, action)
433
120
  end
434
121
 
435
122
  def api_key_check_org_permission!(ctx, user_id, organization_id, action)
436
- org_plugin = ctx.context.options.plugins.find { |plugin| plugin.id == "organization" }
437
- unless org_plugin
438
- raise APIError.new(
439
- "INTERNAL_SERVER_ERROR",
440
- message: API_KEY_ERROR_CODES["ORGANIZATION_PLUGIN_REQUIRED"],
441
- code: "ORGANIZATION_PLUGIN_REQUIRED"
442
- )
443
- end
444
-
445
- member = ctx.context.adapter.find_one(model: "member", where: [{field: "userId", value: user_id}, {field: "organizationId", value: organization_id}])
446
- unless member
447
- raise APIError.new(
448
- "FORBIDDEN",
449
- message: API_KEY_ERROR_CODES["USER_NOT_MEMBER_OF_ORGANIZATION"],
450
- code: "USER_NOT_MEMBER_OF_ORGANIZATION"
451
- )
452
- end
453
-
454
- return member if member["role"].to_s == (org_plugin.options[:creator_role] || "owner").to_s
455
-
456
- permissions = {"apiKey" => [action]}
457
- return member if BetterAuth::Plugins.organization_permission?(ctx, org_plugin.options, member["role"], permissions, organization_id)
458
-
459
- raise APIError.new(
460
- "FORBIDDEN",
461
- message: API_KEY_ERROR_CODES["INSUFFICIENT_API_KEY_PERMISSIONS"],
462
- code: "INSUFFICIENT_API_KEY_PERMISSIONS"
463
- )
123
+ BetterAuth::APIKey::OrgAuthorization.check_permission!(ctx, user_id, organization_id, action)
464
124
  end
465
125
 
466
126
  def api_key_sort_records(records, sort_by, direction)
467
- return records unless sort_by
468
-
469
- key = Schema.storage_key(sort_by)
470
- sorted = records.sort_by { |record| record[key] || record[key.to_sym] || "" }
471
- if direction.to_s.downcase == "desc"
472
- sorted.reverse
473
- else
474
- sorted
475
- end
127
+ BetterAuth::APIKey::Utils.sort_records(records, sort_by, direction)
476
128
  end
477
129
 
478
130
  def api_key_validate_list_query!(query)
479
- %i[limit offset].each do |key|
480
- next unless query.key?(key)
481
-
482
- value = query[key]
483
- raise APIError.new("BAD_REQUEST", message: "Invalid #{key}") unless value.to_s.match?(/\A\d+\z/)
484
- end
485
-
486
- direction = query[:sort_direction]
487
- return if direction.nil? || %w[asc desc].include?(direction.to_s.downcase)
488
-
489
- raise APIError.new("BAD_REQUEST", message: "Invalid sortDirection")
131
+ BetterAuth::APIKey::Utils.validate_list_query!(query)
490
132
  end
491
133
 
492
134
  def api_key_error_code(error)
493
- API_KEY_ERROR_CODES.key(error.message) || error.code.to_s
135
+ BetterAuth::APIKey::Utils.error_code(error)
494
136
  end
495
137
 
496
138
  def api_key_error_payload(error)
497
- payload = error.to_h
498
- return payload if payload.is_a?(Hash) && payload.key?(:details)
499
-
500
- {message: error.message, code: api_key_error_code(error)}
139
+ BetterAuth::APIKey::Utils.error_payload(error)
501
140
  end
502
141
 
503
142
  def api_key_session_header_config(ctx, config)
504
- config.fetch(:configurations, [config]).find do |entry|
505
- entry[:enable_session_for_api_keys] && api_key_get_from_headers(ctx, entry)
506
- end
143
+ BetterAuth::APIKey::Session.header_config(ctx, config)
507
144
  end
508
145
 
509
146
  def api_key_session_hook(ctx, config)
510
- config = api_key_session_header_config(ctx, config) || config
511
- key = api_key_get_from_headers(ctx, config)
512
- unless key.is_a?(String)
513
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_API_KEY_GETTER_RETURN_TYPE"])
514
- end
515
- raise APIError.new("FORBIDDEN", message: API_KEY_ERROR_CODES["INVALID_API_KEY"]) if key.length < config[:default_key_length].to_i
516
-
517
- if config[:custom_api_key_validator].respond_to?(:call) && !config[:custom_api_key_validator].call({ctx: ctx, key: key})
518
- raise APIError.new("FORBIDDEN", message: API_KEY_ERROR_CODES["INVALID_API_KEY"])
519
- end
520
-
521
- record = api_key_validate!(ctx, key, config)
522
- api_key_schedule_cleanup(ctx, config)
523
- if config[:references].to_s != "user"
524
- raise APIError.new(
525
- "UNAUTHORIZED",
526
- message: API_KEY_ERROR_CODES["INVALID_REFERENCE_ID_FROM_API_KEY"],
527
- code: "INVALID_REFERENCE_ID_FROM_API_KEY"
528
- )
529
- end
530
- reference_id = api_key_record_reference_id(record)
531
- user = ctx.context.internal_adapter.find_user_by_id(reference_id)
532
- unless user
533
- raise APIError.new(
534
- "UNAUTHORIZED",
535
- message: API_KEY_ERROR_CODES["INVALID_REFERENCE_ID_FROM_API_KEY"],
536
- code: "INVALID_REFERENCE_ID_FROM_API_KEY"
537
- )
538
- end
539
-
540
- session = {
541
- user: user,
542
- session: {
543
- "id" => record["id"],
544
- "token" => key,
545
- "userId" => reference_id,
546
- "userAgent" => ctx.headers["user-agent"],
547
- "ipAddress" => RequestIP.client_ip(ctx.request || ctx.headers, ctx.context.options),
548
- "createdAt" => Time.now,
549
- "updatedAt" => Time.now,
550
- "expiresAt" => record["expiresAt"] || (Time.now + ctx.context.options.session[:expires_in].to_i)
551
- }
552
- }
553
- ctx.context.set_current_session(session)
554
- nil
147
+ BetterAuth::APIKey::Session.hook(ctx, config)
555
148
  end
556
149
 
557
150
  def api_key_validate!(ctx, key, config, permissions: nil)
558
- hashed = api_key_hash(key, config)
559
- record = api_key_find_by_hash(ctx, hashed, config)
560
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["INVALID_API_KEY"]) unless record
561
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["INVALID_API_KEY"]) unless api_key_config_id_matches?(api_key_record_config_id(record), config[:config_id])
562
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["KEY_DISABLED"]) if record["enabled"] == false
563
- if record["expiresAt"] && record["expiresAt"] <= Time.now
564
- api_key_schedule_record_delete(ctx, record, config)
565
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["KEY_EXPIRED"])
566
- end
567
- if record["remaining"].to_i <= 0 && !record["remaining"].nil? && record["refillAmount"].nil?
568
- api_key_schedule_record_delete(ctx, record, config)
569
- raise APIError.new("TOO_MANY_REQUESTS", message: API_KEY_ERROR_CODES["USAGE_EXCEEDED"])
570
- end
571
-
572
- api_key_check_permissions!(record, permissions)
573
- update = api_key_usage_update(record, config)
574
- updated = api_key_update_record(ctx, record, update, config, defer: true)
575
- api_key_migrate_legacy_metadata(ctx, updated || record.merge(update.transform_keys { |key_name| Schema.storage_key(key_name) }), config)
151
+ BetterAuth::APIKey::Validation.validate_api_key!(ctx, key, config, permissions: permissions)
576
152
  end
577
153
 
578
154
  def api_key_usage_update(record, config)
579
- now = Time.now
580
- update = {lastRequest: now, updatedAt: now}
581
-
582
- if (try_again_in = api_key_rate_limit_try_again_in(record, config, now))
583
- raise APIError.new(
584
- "UNAUTHORIZED",
585
- message: API_KEY_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
586
- code: "RATE_LIMITED",
587
- body: {
588
- message: API_KEY_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
589
- code: "RATE_LIMITED",
590
- details: {tryAgainIn: try_again_in}
591
- }
592
- )
593
- end
594
- update[:requestCount] = api_key_next_request_count(record, now) if api_key_rate_limit_counts_requests?(record, config)
595
-
596
- remaining = record["remaining"]
597
- if !remaining.nil?
598
- if remaining.to_i <= 0 && record["refillAmount"] && record["refillInterval"]
599
- last_refill = api_key_normalize_time(record["lastRefillAt"] || record["createdAt"])
600
- if !last_refill || ((now - last_refill) * 1000) > record["refillInterval"].to_i
601
- remaining = record["refillAmount"].to_i
602
- update[:lastRefillAt] = now
603
- end
604
- end
605
- raise APIError.new("TOO_MANY_REQUESTS", message: API_KEY_ERROR_CODES["USAGE_EXCEEDED"]) if remaining.to_i <= 0
606
-
607
- update[:remaining] = remaining.to_i - 1
608
- end
609
- update
155
+ BetterAuth::APIKey::Validation.usage_update(record, config)
610
156
  end
611
157
 
612
158
  def api_key_rate_limit_try_again_in(record, config, now)
613
- return nil if config[:rate_limit][:enabled] == false || record["rateLimitEnabled"] == false
614
-
615
- window = record["rateLimitTimeWindow"]
616
- max = record["rateLimitMax"]
617
- return nil if window.nil? || max.nil?
618
-
619
- last = api_key_normalize_time(record["lastRequest"])
620
- return nil unless last
621
-
622
- elapsed = (now - last) * 1000
623
- return nil if elapsed > window.to_i
624
- return nil unless record["requestCount"].to_i >= max.to_i
625
-
626
- (window.to_i - elapsed).ceil
159
+ BetterAuth::APIKey::RateLimit.try_again_in(record, config, now)
627
160
  end
628
161
 
629
162
  def api_key_rate_limit_counts_requests?(record, config)
630
- return false if config[:rate_limit][:enabled] == false || record["rateLimitEnabled"] == false
631
-
632
- !record["rateLimitTimeWindow"].nil? && !record["rateLimitMax"].nil?
163
+ BetterAuth::APIKey::RateLimit.counts_requests?(record, config)
633
164
  end
634
165
 
635
166
  def api_key_next_request_count(record, now)
636
- last = api_key_normalize_time(record["lastRequest"])
637
- window = record["rateLimitTimeWindow"].to_i
638
- if last && window.positive? && ((now - last) * 1000) <= window
639
- record["requestCount"].to_i + 1
640
- else
641
- 1
642
- end
167
+ BetterAuth::APIKey::RateLimit.next_request_count(record, now)
643
168
  end
644
169
 
645
170
  def api_key_validate_create_update!(body, config, create:, client:)
646
- name = body[:name]
647
- if create && config[:require_name] && name.to_s.empty?
648
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["NAME_REQUIRED"])
649
- end
650
- if name && !name.to_s.length.between?(config[:minimum_name_length].to_i, config[:maximum_name_length].to_i)
651
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_NAME_LENGTH"])
652
- end
653
- prefix = body[:prefix]
654
- if prefix && !prefix.to_s.length.between?(config[:minimum_prefix_length].to_i, config[:maximum_prefix_length].to_i)
655
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_PREFIX_LENGTH"])
656
- end
657
- if prefix && !prefix.to_s.match?(/\A[a-zA-Z0-9_-]+\z/)
658
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_PREFIX_LENGTH"])
659
- end
660
- if body.key?(:remaining) && !body[:remaining].nil?
661
- minimum = create ? 0 : 1
662
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_REMAINING"]) if body[:remaining].to_i < minimum
663
- end
664
- if body[:metadata] && (create || config[:enable_metadata])
665
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["METADATA_DISABLED"]) unless config[:enable_metadata]
666
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_METADATA_TYPE"]) unless body[:metadata].nil? || body[:metadata].is_a?(Hash)
667
- end
668
- server_only_keys = %i[refill_amount refill_interval rate_limit_max rate_limit_time_window rate_limit_enabled remaining permissions]
669
- if client && server_only_keys.any? { |key| (create && key == :remaining) ? !body[:remaining].nil? : body.key?(key) }
670
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["SERVER_ONLY_PROPERTY"])
671
- end
672
- amount_present = body.key?(:refill_amount)
673
- interval_present = body.key?(:refill_interval)
674
- if amount_present && !interval_present
675
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["REFILL_AMOUNT_AND_INTERVAL_REQUIRED"])
676
- end
677
- if interval_present && !amount_present
678
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["REFILL_INTERVAL_AND_AMOUNT_REQUIRED"])
679
- end
680
- if body.key?(:expires_in)
681
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["KEY_DISABLED_EXPIRATION"]) if config[:key_expiration][:disable_custom_expires_time]
682
- return if body[:expires_in].nil?
683
-
684
- days = body[:expires_in].to_f / 86_400
685
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["EXPIRES_IN_IS_TOO_SMALL"]) if days < config[:key_expiration][:min_expires_in].to_f
686
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["EXPIRES_IN_IS_TOO_LARGE"]) if days > config[:key_expiration][:max_expires_in].to_f
687
- end
171
+ BetterAuth::APIKey::Validation.validate_create_update!(body, config, create: create, client: client)
688
172
  end
689
173
 
690
174
  def api_key_update_payload(body, config)
691
- update = {}
692
- update[:name] = body[:name] if body.key?(:name)
693
- update[:enabled] = body[:enabled] unless body[:enabled].nil?
694
- update[:remaining] = body[:remaining] if body.key?(:remaining)
695
- update[:refillAmount] = body[:refill_amount] if body.key?(:refill_amount)
696
- update[:refillInterval] = body[:refill_interval] if body.key?(:refill_interval)
697
- update[:rateLimitEnabled] = body[:rate_limit_enabled] if body.key?(:rate_limit_enabled)
698
- update[:rateLimitTimeWindow] = body[:rate_limit_time_window] if body.key?(:rate_limit_time_window)
699
- update[:rateLimitMax] = body[:rate_limit_max] if body.key?(:rate_limit_max)
700
- update[:expiresAt] = body[:expires_in].nil? ? nil : Time.now + body[:expires_in].to_i if body.key?(:expires_in)
701
- update[:metadata] = api_key_encode_json(body[:metadata]) if body.key?(:metadata) && config[:enable_metadata]
702
- update[:permissions] = api_key_encode_json(body[:permissions]) if body.key?(:permissions)
703
- update
175
+ BetterAuth::APIKey::Validation.update_payload(body, config)
704
176
  end
705
177
 
706
178
  def api_key_generate_key(config, prefix)
707
- generator = config[:custom_key_generator]
708
- return generator.call({length: config[:default_key_length], prefix: prefix}) if generator.respond_to?(:call)
709
-
710
- alphabet = [*("a".."z"), *("A".."Z")]
711
- "#{prefix}#{Array.new(config[:default_key_length].to_i) { alphabet[SecureRandom.random_number(alphabet.length)] }.join}"
179
+ BetterAuth::APIKey::Keys.generate(config, prefix)
712
180
  end
713
181
 
714
182
  def api_key_hash(key, config)
715
- config[:disable_key_hashing] ? key.to_s : default_api_key_hasher(key)
183
+ BetterAuth::APIKey::Keys.hash(key, config)
716
184
  end
717
185
 
718
186
  def api_key_normalize_body(raw)
719
- body = normalize_hash(raw)
720
- return body unless raw.is_a?(Hash)
721
-
722
- metadata_key = raw.key?(:metadata) ? :metadata : ("metadata" if raw.key?("metadata"))
723
- body[:metadata] = raw[metadata_key] if metadata_key
724
- body
187
+ BetterAuth::APIKey::Keys.normalize_body(raw)
725
188
  end
726
189
 
727
190
  def api_key_expires_at(body, config)
728
- if body.key?(:expires_in)
729
- Time.now + body[:expires_in].to_i unless body[:expires_in].nil?
730
- elsif config[:key_expiration][:default_expires_in]
731
- Time.now + config[:key_expiration][:default_expires_in].to_i
732
- end
191
+ BetterAuth::APIKey::Keys.expires_at(body, config)
733
192
  end
734
193
 
735
194
  def api_key_store(ctx, data, config)
736
- record = nil
737
- if config[:storage] == "database" || config[:fallback_to_database]
738
- record = ctx.context.adapter.create(model: API_KEY_TABLE_NAME, data: data)
739
- end
740
- record ||= data.transform_keys { |key| Schema.storage_key(key) }.merge("id" => SecureRandom.hex(16))
741
- api_key_storage_set(ctx, record, config) if config[:storage] == "secondary-storage"
742
- record
195
+ BetterAuth::APIKey::Adapter.store(ctx, data, config)
743
196
  end
744
197
 
745
198
  def api_key_find_by_hash(ctx, hashed, config)
746
- if config[:storage] == "secondary-storage"
747
- record = api_key_storage_get(ctx, "api-key:#{hashed}", config) || api_key_storage_get(ctx, "api-key:key:#{hashed}", config)
748
- return record if record
749
- return nil unless config[:fallback_to_database]
750
- end
751
- record = ctx.context.adapter.find_one(model: API_KEY_TABLE_NAME, where: [{field: "key", value: hashed}])
752
- api_key_storage_set(ctx, record, config) if record && config[:storage] == "secondary-storage" && config[:fallback_to_database]
753
- record
199
+ BetterAuth::APIKey::Adapter.find_by_hash(ctx, hashed, config)
754
200
  end
755
201
 
756
202
  def api_key_find_by_id(ctx, id, config)
757
- if config[:storage] == "secondary-storage"
758
- record = api_key_storage_get(ctx, "api-key:by-id:#{id}", config) || api_key_storage_get(ctx, "api-key:id:#{id}", config)
759
- return record if record
760
- return nil unless config[:fallback_to_database]
761
- end
762
- record = ctx.context.adapter.find_one(model: API_KEY_TABLE_NAME, where: [{field: "id", value: id}])
763
- api_key_storage_set(ctx, record, config) if record && config[:storage] == "secondary-storage" && config[:fallback_to_database]
764
- record
203
+ BetterAuth::APIKey::Adapter.find_by_id(ctx, id, config)
765
204
  end
766
205
 
767
206
  def api_key_list_for_user(ctx, user_id, config)
@@ -769,279 +208,107 @@ module BetterAuth
769
208
  end
770
209
 
771
210
  def api_key_list_for_reference(ctx, reference_id, config)
772
- if config[:storage] == "secondary-storage"
773
- begin
774
- storage = api_key_storage(config, ctx.context)
775
- ids = JSON.parse((storage&.get("api-key:by-ref:#{reference_id}") || storage&.get("api-key:user:#{reference_id}")).to_s)
776
- records = ids.filter_map { |id| api_key_find_by_id(ctx, id, config) }
777
- return records unless records.empty? && config[:fallback_to_database]
778
- rescue JSON::ParserError, NoMethodError
779
- return [] unless config[:fallback_to_database]
780
- end
781
- end
782
- records = ctx.context.adapter.find_many(model: API_KEY_TABLE_NAME, where: [{field: "referenceId", value: reference_id}])
783
- legacy = ctx.context.adapter.find_many(model: API_KEY_TABLE_NAME, where: [{field: "userId", value: reference_id}])
784
- combined = (records + legacy).uniq { |record| record["id"] }
785
- api_key_storage_populate_reference(ctx, reference_id, combined, config) if config[:storage] == "secondary-storage" && config[:fallback_to_database]
786
- combined
211
+ BetterAuth::APIKey::Adapter.list_for_reference(ctx, reference_id, config)
787
212
  end
788
213
 
789
214
  def api_key_update_record(ctx, record, update, config, defer: false)
790
- performer = lambda do
791
- updated = nil
792
- if config[:storage] == "database" || config[:fallback_to_database]
793
- updated = ctx.context.adapter.update(model: API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}], update: update)
794
- end
795
- updated ||= record.merge(update.transform_keys { |key| Schema.storage_key(key) })
796
- api_key_storage_set(ctx, updated, config) if config[:storage] == "secondary-storage"
797
- updated
798
- end
799
-
800
- if defer && config[:defer_updates] && api_key_background_tasks?(ctx)
801
- scheduled = record.merge(update.transform_keys { |key| Schema.storage_key(key) })
802
- ctx.context.run_in_background(performer)
803
- scheduled
804
- else
805
- performer.call
806
- end
215
+ BetterAuth::APIKey::Adapter.update_record(ctx, record, update, config, defer: defer)
807
216
  end
808
217
 
809
218
  def api_key_delete_record(ctx, record, config)
810
- ctx.context.adapter.delete(model: API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}]) if config[:storage] == "database" || config[:fallback_to_database]
811
- api_key_storage_delete(ctx, record, config) if config[:storage] == "secondary-storage"
219
+ BetterAuth::APIKey::Adapter.delete_record(ctx, record, config)
812
220
  end
813
221
 
814
222
  def api_key_schedule_record_delete(ctx, record, config)
815
- task = -> { api_key_delete_record(ctx, record, config) }
816
- if config[:defer_updates] && api_key_background_tasks?(ctx)
817
- ctx.context.run_in_background(task)
818
- else
819
- task.call
820
- end
223
+ BetterAuth::APIKey::Adapter.schedule_record_delete(ctx, record, config)
821
224
  end
822
225
 
823
226
  def api_key_schedule_cleanup(ctx, config)
824
- task = -> { api_key_delete_expired(ctx.context, config) }
825
- if config[:defer_updates] && api_key_background_tasks?(ctx)
826
- ctx.context.run_in_background(task)
827
- else
828
- task.call
829
- end
227
+ BetterAuth::APIKey::Routes.schedule_cleanup(ctx, config)
830
228
  end
831
229
 
832
- @api_key_last_expired_check = nil
833
-
834
230
  def api_key_delete_expired(context, config, bypass_last_check: false)
835
- return unless config[:storage] == "database" || config[:fallback_to_database]
836
- unless bypass_last_check
837
- now = Time.now
838
- return if @api_key_last_expired_check && ((now - @api_key_last_expired_check) * 1000) < 10_000
839
-
840
- @api_key_last_expired_check = now
841
- end
842
-
843
- expired = context.adapter.find_many(model: API_KEY_TABLE_NAME).select do |record|
844
- record["expiresAt"] && record["expiresAt"] < Time.now
845
- end
846
- expired.each do |record|
847
- context.adapter.delete(model: API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}])
848
- end
231
+ BetterAuth::APIKey::Routes.delete_expired(context, config, bypass_last_check: bypass_last_check)
849
232
  end
850
233
 
851
234
  def api_key_storage(config, context = nil)
852
- config[:custom_storage] || context&.options&.secondary_storage
235
+ BetterAuth::APIKey::Adapter.storage(config, context)
853
236
  end
854
237
 
855
238
  def api_key_storage_get(ctx, key, config)
856
- raw = api_key_storage(config, ctx.context)&.get(key)
857
- raw && api_key_deserialize_storage_record(JSON.parse(raw))
858
- rescue JSON::ParserError
859
- nil
239
+ BetterAuth::APIKey::Adapter.get(ctx, key, config)
860
240
  end
861
241
 
862
242
  def api_key_storage_set(ctx, record, config)
863
- storage = api_key_storage(config, ctx.context)
864
- unless storage
865
- raise APIError.new("INTERNAL_SERVER_ERROR", message: "Secondary storage is required when storage mode is 'secondary-storage'")
866
- end
867
-
868
- serialized = JSON.generate(api_key_storage_record(record))
869
- expires_at = api_key_normalize_time(record["expiresAt"])
870
- ttl = expires_at ? [(expires_at - Time.now).to_i, 0].max : nil
871
- reference_id = api_key_record_reference_id(record)
872
- user_key = "api-key:by-ref:#{reference_id}"
873
-
874
- api_key_storage_batch(storage) do
875
- operations = [
876
- -> { storage.set("api-key:#{record["key"]}", serialized, ttl) },
877
- -> { storage.set("api-key:by-id:#{record["id"]}", serialized, ttl) }
878
- ]
879
- operations << if config[:fallback_to_database]
880
- # In fallback mode the ref list is a cache invalidated on writes
881
- # to avoid races with concurrent writers of the same reference.
882
- -> { storage.delete(user_key) }
883
- else
884
- -> { api_key_ref_list_add(storage, user_key, record["id"]) }
885
- end
886
- operations.each(&:call)
887
- end
243
+ BetterAuth::APIKey::Adapter.set(ctx, record, config)
888
244
  end
889
245
 
890
246
  def api_key_storage_delete(ctx, record, config)
891
- storage = api_key_storage(config, ctx.context)
892
- return unless storage
893
-
894
- reference_id = api_key_record_reference_id(record)
895
- user_key = "api-key:by-ref:#{reference_id}"
896
-
897
- api_key_storage_batch(storage) do
898
- operations = [
899
- -> { storage.delete("api-key:#{record["key"]}") },
900
- -> { storage.delete("api-key:by-id:#{record["id"]}") },
901
- # Ruby-only legacy storage layout cleanup; upstream never wrote here.
902
- -> { storage.delete("api-key:key:#{record["key"]}") },
903
- -> { storage.delete("api-key:id:#{record["id"]}") }
904
- ]
905
- operations << if config[:fallback_to_database]
906
- -> { storage.delete(user_key) }
907
- else
908
- -> { api_key_ref_list_remove(storage, user_key, record["id"]) }
909
- end
910
- operations.each(&:call)
911
- end
247
+ BetterAuth::APIKey::Adapter.delete(ctx, record, config)
912
248
  end
913
249
 
914
250
  def api_key_ref_list_add(storage, user_key, id)
915
- ids = api_key_safe_parse_id_list(storage.get(user_key))
916
- ids << id unless ids.include?(id)
917
- storage.set(user_key, JSON.generate(ids))
251
+ BetterAuth::APIKey::Adapter.ref_list_add(storage, user_key, id)
918
252
  end
919
253
 
920
254
  def api_key_ref_list_remove(storage, user_key, id)
921
- ids = api_key_safe_parse_id_list(storage.get(user_key)).reject { |existing| existing == id }
922
- ids.empty? ? storage.delete(user_key) : storage.set(user_key, JSON.generate(ids))
255
+ BetterAuth::APIKey::Adapter.ref_list_remove(storage, user_key, id)
923
256
  end
924
257
 
925
258
  def api_key_safe_parse_id_list(raw)
926
- return [] if raw.nil?
927
-
928
- parsed = JSON.parse(raw.to_s)
929
- parsed.is_a?(Array) ? parsed : []
930
- rescue JSON::ParserError
931
- []
259
+ BetterAuth::APIKey::Adapter.safe_parse_id_list(raw)
932
260
  end
933
261
 
934
262
  def api_key_storage_batch(storage, &block)
935
- if storage.respond_to?(:batch)
936
- storage.batch(&block)
937
- else
938
- block.call
939
- end
263
+ BetterAuth::APIKey::Adapter.batch(storage, &block)
940
264
  end
941
265
 
942
266
  def api_key_storage_populate_reference(ctx, reference_id, records, config)
943
- storage = api_key_storage(config, ctx.context)
944
- return unless storage
945
-
946
- ids = []
947
- records.each do |record|
948
- serialized = JSON.generate(api_key_storage_record(record))
949
- expires_at = api_key_normalize_time(record["expiresAt"])
950
- ttl = expires_at ? [(expires_at - Time.now).to_i, 0].max : nil
951
- storage.set("api-key:#{record["key"]}", serialized, ttl)
952
- storage.set("api-key:by-id:#{record["id"]}", serialized, ttl)
953
- ids << record["id"]
954
- end
955
- ids.empty? ? storage.delete("api-key:by-ref:#{reference_id}") : storage.set("api-key:by-ref:#{reference_id}", JSON.generate(ids))
267
+ BetterAuth::APIKey::Adapter.populate_reference(ctx, reference_id, records, config)
956
268
  end
957
269
 
958
270
  def api_key_storage_record(record)
959
- record.transform_values { |value| value.is_a?(Time) ? value.iso8601 : value }
271
+ BetterAuth::APIKey::Adapter.storage_record(record)
960
272
  end
961
273
 
962
274
  def api_key_deserialize_storage_record(record)
963
- %w[createdAt updatedAt expiresAt lastRefillAt lastRequest].each do |field|
964
- record[field] = api_key_normalize_time(record[field]) if record[field]
965
- end
966
- record
275
+ BetterAuth::APIKey::Adapter.deserialize_record(record)
967
276
  end
968
277
 
969
278
  def api_key_public(record, reveal_key: nil, include_key_field: false)
970
- data = record.transform_keys(&:to_sym)
971
- output = data.except(:key)
972
- output[:configId] ||= api_key_record_config_id(record)
973
- output[:referenceId] ||= api_key_record_reference_id(record)
974
- output[:key] = reveal_key if include_key_field && reveal_key
975
- output[:metadata] = api_key_decode_json(data[:metadata])
976
- output[:permissions] = api_key_decode_json(data[:permissions])
977
- output
279
+ BetterAuth::APIKey::Utils.public_record(record, reveal_key: reveal_key, include_key_field: include_key_field)
978
280
  end
979
281
 
980
282
  def api_key_migrate_legacy_metadata(ctx, record, config)
981
- parsed = api_key_decode_json(record["metadata"])
982
- return record unless parsed.is_a?(Hash)
983
-
984
- encoded = api_key_encode_json(parsed)
985
- return record.merge("metadata" => encoded) if record["metadata"] == encoded
986
-
987
- updated = record.merge("metadata" => encoded)
988
- if config[:storage] == "database" || config[:fallback_to_database]
989
- ctx.context.adapter.update(model: API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}], update: {metadata: encoded})
990
- end
991
- api_key_storage_set(ctx, updated, config) if config[:storage] == "secondary-storage"
992
- updated
283
+ BetterAuth::APIKey::Adapter.migrate_legacy_metadata(ctx, record, config)
993
284
  end
994
285
 
995
286
  def api_key_background_tasks?(ctx)
996
- ctx.context.options.advanced.dig(:background_tasks, :handler).respond_to?(:call)
287
+ BetterAuth::APIKey::Utils.background_tasks?(ctx)
997
288
  end
998
289
 
999
290
  def api_key_auth_required?(ctx)
1000
- !!(ctx.request || (ctx.headers && !ctx.headers.empty?))
291
+ BetterAuth::APIKey::Utils.auth_required?(ctx)
1001
292
  end
1002
293
 
1003
294
  def api_key_get_from_headers(ctx, config)
1004
- getter = config[:custom_api_key_getter]
1005
- return getter.call(ctx) if getter.respond_to?(:call)
1006
-
1007
- Array(config[:api_key_headers]).each do |header|
1008
- value = ctx.headers[header.to_s.downcase]
1009
- return value if value
1010
- end
1011
- nil
295
+ BetterAuth::APIKey::Keys.from_headers(ctx, config)
1012
296
  end
1013
297
 
1014
298
  def api_key_check_permissions!(record, required)
1015
- return if required.nil? || required == {}
1016
-
1017
- actual = api_key_decode_json(record["permissions"]) || {}
1018
- result = Role.new(actual).authorize(required)
1019
- unless result[:success]
1020
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["KEY_NOT_FOUND"], code: "KEY_NOT_FOUND")
1021
- end
299
+ BetterAuth::APIKey::Validation.check_permissions!(record, required)
1022
300
  end
1023
301
 
1024
302
  def api_key_encode_json(value)
1025
- return nil if value.nil?
1026
-
1027
- JSON.generate(value)
303
+ BetterAuth::APIKey::Utils.encode_json(value)
1028
304
  end
1029
305
 
1030
306
  def api_key_decode_json(value)
1031
- return nil if value.nil?
1032
- return value if value.is_a?(Hash)
1033
-
1034
- parsed = JSON.parse(value.to_s)
1035
- parsed.is_a?(String) ? api_key_decode_json(parsed) : parsed
1036
- rescue JSON::ParserError
1037
- nil
307
+ BetterAuth::APIKey::Utils.decode_json(value)
1038
308
  end
1039
309
 
1040
310
  def api_key_normalize_time(value)
1041
- return value if value.is_a?(Time)
1042
- return nil if value.nil?
1043
-
1044
- Time.parse(value.to_s)
311
+ BetterAuth::APIKey::Utils.normalize_time(value)
1045
312
  end
1046
313
  end
1047
314
  end