better_auth-api-key 0.2.0 → 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,740 +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 = normalize_hash(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 = normalize_hash(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
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["ORGANIZATION_ID_REQUIRED"]) if organization_id.to_s.empty?
383
-
384
- user_id = session&.dig(:user, "id") || body[:user_id]
385
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"]) if user_id.to_s.empty?
386
-
387
- api_key_check_org_permission!(ctx, user_id, organization_id, "create")
388
- organization_id
389
- elsif session && body[:user_id] && body[:user_id] != session[:user]["id"]
390
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"])
391
- elsif session
392
-
393
- session[:user]["id"]
394
- else
395
- user_id = body[:user_id]
396
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"]) if user_id.to_s.empty?
397
-
398
- user_id
399
- end
99
+ BetterAuth::APIKey::OrgAuthorization.create_reference_id!(ctx, body, session, config)
400
100
  end
401
101
 
402
102
  def api_key_record_reference_id(record)
403
- record["referenceId"] || record[:referenceId] || record["userId"] || record[:userId]
103
+ BetterAuth::APIKey::Types.record_reference_id(record)
404
104
  end
405
105
 
406
106
  def api_key_record_user_id(record)
407
- 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)
408
108
  end
409
109
 
410
110
  def api_key_record_config_id(record)
411
- record["configId"] || record[:configId] || "default"
111
+ BetterAuth::APIKey::Types.record_config_id(record)
412
112
  end
413
113
 
414
114
  def api_key_default_permissions(config, reference_id, ctx)
415
- permissions = config.dig(:permissions, :default_permissions) || config[:default_permissions]
416
- return permissions.call(reference_id, ctx) if permissions.respond_to?(:call)
417
-
418
- permissions
115
+ BetterAuth::APIKey::Types.default_permissions(config, reference_id, ctx)
419
116
  end
420
117
 
421
118
  def api_key_authorize_reference!(ctx, config, user_id, reference_id, action)
422
- if config[:references].to_s == "organization"
423
- api_key_check_org_permission!(ctx, user_id, reference_id, action)
424
- elsif reference_id != user_id
425
- raise APIError.new("NOT_FOUND", message: API_KEY_ERROR_CODES["KEY_NOT_FOUND"])
426
- end
119
+ BetterAuth::APIKey::OrgAuthorization.authorize_reference!(ctx, config, user_id, reference_id, action)
427
120
  end
428
121
 
429
122
  def api_key_check_org_permission!(ctx, user_id, organization_id, action)
430
- org_plugin = ctx.context.options.plugins.find { |plugin| plugin.id == "organization" }
431
- unless org_plugin
432
- raise APIError.new(
433
- "INTERNAL_SERVER_ERROR",
434
- message: API_KEY_ERROR_CODES["ORGANIZATION_PLUGIN_REQUIRED"],
435
- code: "ORGANIZATION_PLUGIN_REQUIRED"
436
- )
437
- end
438
-
439
- member = ctx.context.adapter.find_one(model: "member", where: [{field: "userId", value: user_id}, {field: "organizationId", value: organization_id}])
440
- unless member
441
- raise APIError.new(
442
- "FORBIDDEN",
443
- message: API_KEY_ERROR_CODES["USER_NOT_MEMBER_OF_ORGANIZATION"],
444
- code: "USER_NOT_MEMBER_OF_ORGANIZATION"
445
- )
446
- end
447
-
448
- return member if member["role"].to_s == (org_plugin.options[:creator_role] || "owner").to_s
449
-
450
- permissions = {"apiKey" => [action]}
451
- return member if BetterAuth::Plugins.organization_permission?(ctx, org_plugin.options, member["role"], permissions, organization_id)
452
-
453
- raise APIError.new(
454
- "FORBIDDEN",
455
- message: API_KEY_ERROR_CODES["INSUFFICIENT_API_KEY_PERMISSIONS"],
456
- code: "INSUFFICIENT_API_KEY_PERMISSIONS"
457
- )
123
+ BetterAuth::APIKey::OrgAuthorization.check_permission!(ctx, user_id, organization_id, action)
458
124
  end
459
125
 
460
126
  def api_key_sort_records(records, sort_by, direction)
461
- return records unless sort_by
462
-
463
- key = Schema.storage_key(sort_by)
464
- sorted = records.sort_by { |record| record[key] || record[key.to_sym] || "" }
465
- if direction.to_s.downcase == "desc"
466
- sorted.reverse
467
- else
468
- sorted
469
- end
127
+ BetterAuth::APIKey::Utils.sort_records(records, sort_by, direction)
470
128
  end
471
129
 
472
130
  def api_key_validate_list_query!(query)
473
- %i[limit offset].each do |key|
474
- next unless query.key?(key)
475
-
476
- value = query[key]
477
- raise APIError.new("BAD_REQUEST", message: "Invalid #{key}") unless value.to_s.match?(/\A\d+\z/)
478
- end
479
-
480
- direction = query[:sort_direction]
481
- return if direction.nil? || %w[asc desc].include?(direction.to_s.downcase)
482
-
483
- raise APIError.new("BAD_REQUEST", message: "Invalid sortDirection")
131
+ BetterAuth::APIKey::Utils.validate_list_query!(query)
484
132
  end
485
133
 
486
134
  def api_key_error_code(error)
487
- API_KEY_ERROR_CODES.key(error.message) || error.code.to_s
135
+ BetterAuth::APIKey::Utils.error_code(error)
488
136
  end
489
137
 
490
138
  def api_key_error_payload(error)
491
- payload = error.to_h
492
- return payload if payload.is_a?(Hash) && payload.key?(:details)
493
-
494
- {message: error.message, code: api_key_error_code(error)}
139
+ BetterAuth::APIKey::Utils.error_payload(error)
495
140
  end
496
141
 
497
142
  def api_key_session_header_config(ctx, config)
498
- config.fetch(:configurations, [config]).find do |entry|
499
- entry[:enable_session_for_api_keys] && api_key_get_from_headers(ctx, entry)
500
- end
143
+ BetterAuth::APIKey::Session.header_config(ctx, config)
501
144
  end
502
145
 
503
146
  def api_key_session_hook(ctx, config)
504
- config = api_key_session_header_config(ctx, config) || config
505
- key = api_key_get_from_headers(ctx, config)
506
- unless key.is_a?(String)
507
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_API_KEY_GETTER_RETURN_TYPE"])
508
- end
509
- raise APIError.new("FORBIDDEN", message: API_KEY_ERROR_CODES["INVALID_API_KEY"]) if key.length < config[:default_key_length].to_i
510
-
511
- if config[:custom_api_key_validator].respond_to?(:call) && !config[:custom_api_key_validator].call({ctx: ctx, key: key})
512
- raise APIError.new("FORBIDDEN", message: API_KEY_ERROR_CODES["INVALID_API_KEY"])
513
- end
514
-
515
- record = api_key_validate!(ctx, key, config)
516
- api_key_schedule_cleanup(ctx, config)
517
- if config[:references].to_s != "user"
518
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["INVALID_REFERENCE_ID_FROM_API_KEY"])
519
- end
520
- reference_id = api_key_record_reference_id(record)
521
- user = ctx.context.internal_adapter.find_user_by_id(reference_id)
522
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["INVALID_REFERENCE_ID_FROM_API_KEY"]) unless user
523
-
524
- session = {
525
- user: user,
526
- session: {
527
- "id" => record["id"],
528
- "token" => key,
529
- "userId" => reference_id,
530
- "userAgent" => ctx.headers["user-agent"],
531
- "ipAddress" => RequestIP.client_ip(ctx.request || ctx.headers, ctx.context.options),
532
- "createdAt" => Time.now,
533
- "updatedAt" => Time.now,
534
- "expiresAt" => record["expiresAt"] || (Time.now + ctx.context.options.session[:expires_in].to_i)
535
- }
536
- }
537
- ctx.context.set_current_session(session)
538
- nil
147
+ BetterAuth::APIKey::Session.hook(ctx, config)
539
148
  end
540
149
 
541
150
  def api_key_validate!(ctx, key, config, permissions: nil)
542
- hashed = api_key_hash(key, config)
543
- record = api_key_find_by_hash(ctx, hashed, config)
544
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["INVALID_API_KEY"]) unless record
545
- 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])
546
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["KEY_DISABLED"]) if record["enabled"] == false
547
- if record["expiresAt"] && record["expiresAt"] <= Time.now
548
- api_key_schedule_record_delete(ctx, record, config)
549
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["KEY_EXPIRED"])
550
- end
551
- if record["remaining"].to_i <= 0 && !record["remaining"].nil? && record["refillAmount"].nil?
552
- api_key_schedule_record_delete(ctx, record, config)
553
- raise APIError.new("TOO_MANY_REQUESTS", message: API_KEY_ERROR_CODES["USAGE_EXCEEDED"])
554
- end
555
-
556
- api_key_check_permissions!(record, permissions)
557
- update = api_key_usage_update(record, config)
558
- updated = api_key_update_record(ctx, record, update, config, defer: true)
559
- 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)
560
152
  end
561
153
 
562
154
  def api_key_usage_update(record, config)
563
- now = Time.now
564
- update = {lastRequest: now, updatedAt: now}
565
-
566
- if (try_again_in = api_key_rate_limit_try_again_in(record, config, now))
567
- raise APIError.new(
568
- "UNAUTHORIZED",
569
- message: API_KEY_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
570
- code: "RATE_LIMITED",
571
- body: {
572
- message: API_KEY_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
573
- code: "RATE_LIMITED",
574
- details: {tryAgainIn: try_again_in}
575
- }
576
- )
577
- end
578
- update[:requestCount] = api_key_next_request_count(record, now) if api_key_rate_limit_counts_requests?(record, config)
579
-
580
- remaining = record["remaining"]
581
- if !remaining.nil?
582
- if remaining.to_i <= 0 && record["refillAmount"] && record["refillInterval"]
583
- last_refill = api_key_normalize_time(record["lastRefillAt"] || record["createdAt"])
584
- if !last_refill || ((now - last_refill) * 1000) > record["refillInterval"].to_i
585
- remaining = record["refillAmount"].to_i
586
- update[:lastRefillAt] = now
587
- end
588
- end
589
- raise APIError.new("TOO_MANY_REQUESTS", message: API_KEY_ERROR_CODES["USAGE_EXCEEDED"]) if remaining.to_i <= 0
590
-
591
- update[:remaining] = remaining.to_i - 1
592
- end
593
- update
155
+ BetterAuth::APIKey::Validation.usage_update(record, config)
594
156
  end
595
157
 
596
158
  def api_key_rate_limit_try_again_in(record, config, now)
597
- return nil if config[:rate_limit][:enabled] == false || record["rateLimitEnabled"] == false
598
-
599
- window = record["rateLimitTimeWindow"]
600
- max = record["rateLimitMax"]
601
- return nil if window.nil? || max.nil?
602
-
603
- last = api_key_normalize_time(record["lastRequest"])
604
- return nil unless last
605
-
606
- elapsed = (now - last) * 1000
607
- return nil if elapsed > window.to_i
608
- return nil unless record["requestCount"].to_i >= max.to_i
609
-
610
- (window.to_i - elapsed).ceil
159
+ BetterAuth::APIKey::RateLimit.try_again_in(record, config, now)
611
160
  end
612
161
 
613
162
  def api_key_rate_limit_counts_requests?(record, config)
614
- return false if config[:rate_limit][:enabled] == false || record["rateLimitEnabled"] == false
615
-
616
- !record["rateLimitTimeWindow"].nil? && !record["rateLimitMax"].nil?
163
+ BetterAuth::APIKey::RateLimit.counts_requests?(record, config)
617
164
  end
618
165
 
619
166
  def api_key_next_request_count(record, now)
620
- last = api_key_normalize_time(record["lastRequest"])
621
- window = record["rateLimitTimeWindow"].to_i
622
- if last && window.positive? && ((now - last) * 1000) <= window
623
- record["requestCount"].to_i + 1
624
- else
625
- 1
626
- end
167
+ BetterAuth::APIKey::RateLimit.next_request_count(record, now)
627
168
  end
628
169
 
629
170
  def api_key_validate_create_update!(body, config, create:, client:)
630
- name = body[:name]
631
- if create && config[:require_name] && name.to_s.empty?
632
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["NAME_REQUIRED"])
633
- end
634
- if name && !name.to_s.length.between?(config[:minimum_name_length].to_i, config[:maximum_name_length].to_i)
635
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_NAME_LENGTH"])
636
- end
637
- prefix = body[:prefix]
638
- if prefix && !prefix.to_s.length.between?(config[:minimum_prefix_length].to_i, config[:maximum_prefix_length].to_i)
639
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_PREFIX_LENGTH"])
640
- end
641
- if prefix && !prefix.to_s.match?(/\A[a-zA-Z0-9_-]+\z/)
642
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_PREFIX_LENGTH"])
643
- end
644
- if body.key?(:remaining) && !body[:remaining].nil?
645
- minimum = create ? 0 : 1
646
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_REMAINING"]) if body[:remaining].to_i < minimum
647
- end
648
- if body[:metadata] && (create || config[:enable_metadata])
649
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["METADATA_DISABLED"]) unless config[:enable_metadata]
650
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["INVALID_METADATA_TYPE"]) unless body[:metadata].nil? || body[:metadata].is_a?(Hash)
651
- end
652
- server_only_keys = %i[refill_amount refill_interval rate_limit_max rate_limit_time_window rate_limit_enabled remaining permissions]
653
- if client && server_only_keys.any? { |key| (create && key == :remaining) ? !body[:remaining].nil? : body.key?(key) }
654
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["SERVER_ONLY_PROPERTY"])
655
- end
656
- amount_present = body.key?(:refill_amount)
657
- interval_present = body.key?(:refill_interval)
658
- if amount_present && !interval_present
659
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["REFILL_AMOUNT_AND_INTERVAL_REQUIRED"])
660
- end
661
- if interval_present && !amount_present
662
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["REFILL_INTERVAL_AND_AMOUNT_REQUIRED"])
663
- end
664
- if body.key?(:expires_in)
665
- raise APIError.new("BAD_REQUEST", message: API_KEY_ERROR_CODES["KEY_DISABLED_EXPIRATION"]) if config[:key_expiration][:disable_custom_expires_time]
666
- return if body[:expires_in].nil?
667
-
668
- days = body[:expires_in].to_f / 86_400
669
- 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
670
- 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
671
- end
171
+ BetterAuth::APIKey::Validation.validate_create_update!(body, config, create: create, client: client)
672
172
  end
673
173
 
674
174
  def api_key_update_payload(body, config)
675
- update = {}
676
- update[:name] = body[:name] if body.key?(:name)
677
- update[:enabled] = body[:enabled] unless body[:enabled].nil?
678
- update[:remaining] = body[:remaining] if body.key?(:remaining)
679
- update[:refillAmount] = body[:refill_amount] if body.key?(:refill_amount)
680
- update[:refillInterval] = body[:refill_interval] if body.key?(:refill_interval)
681
- update[:rateLimitEnabled] = body[:rate_limit_enabled] if body.key?(:rate_limit_enabled)
682
- update[:rateLimitTimeWindow] = body[:rate_limit_time_window] if body.key?(:rate_limit_time_window)
683
- update[:rateLimitMax] = body[:rate_limit_max] if body.key?(:rate_limit_max)
684
- update[:expiresAt] = body[:expires_in].nil? ? nil : Time.now + body[:expires_in].to_i if body.key?(:expires_in)
685
- update[:metadata] = api_key_encode_json(body[:metadata]) if body.key?(:metadata) && config[:enable_metadata]
686
- update[:permissions] = api_key_encode_json(body[:permissions]) if body.key?(:permissions)
687
- update
175
+ BetterAuth::APIKey::Validation.update_payload(body, config)
688
176
  end
689
177
 
690
178
  def api_key_generate_key(config, prefix)
691
- generator = config[:custom_key_generator]
692
- return generator.call({length: config[:default_key_length], prefix: prefix}) if generator.respond_to?(:call)
693
-
694
- alphabet = [*("a".."z"), *("A".."Z")]
695
- "#{prefix}#{Array.new(config[:default_key_length].to_i) { alphabet[SecureRandom.random_number(alphabet.length)] }.join}"
179
+ BetterAuth::APIKey::Keys.generate(config, prefix)
696
180
  end
697
181
 
698
182
  def api_key_hash(key, config)
699
- config[:disable_key_hashing] ? key.to_s : default_api_key_hasher(key)
183
+ BetterAuth::APIKey::Keys.hash(key, config)
184
+ end
185
+
186
+ def api_key_normalize_body(raw)
187
+ BetterAuth::APIKey::Keys.normalize_body(raw)
700
188
  end
701
189
 
702
190
  def api_key_expires_at(body, config)
703
- if body.key?(:expires_in)
704
- Time.now + body[:expires_in].to_i unless body[:expires_in].nil?
705
- elsif config[:key_expiration][:default_expires_in]
706
- Time.now + config[:key_expiration][:default_expires_in].to_i
707
- end
191
+ BetterAuth::APIKey::Keys.expires_at(body, config)
708
192
  end
709
193
 
710
194
  def api_key_store(ctx, data, config)
711
- record = nil
712
- if config[:storage] == "database" || config[:fallback_to_database]
713
- record = ctx.context.adapter.create(model: API_KEY_TABLE_NAME, data: data)
714
- end
715
- record ||= data.transform_keys { |key| Schema.storage_key(key) }.merge("id" => SecureRandom.hex(16))
716
- api_key_storage_set(ctx, record, config) if config[:storage] == "secondary-storage"
717
- record
195
+ BetterAuth::APIKey::Adapter.store(ctx, data, config)
718
196
  end
719
197
 
720
198
  def api_key_find_by_hash(ctx, hashed, config)
721
- if config[:storage] == "secondary-storage"
722
- record = api_key_storage_get(ctx, "api-key:#{hashed}", config) || api_key_storage_get(ctx, "api-key:key:#{hashed}", config)
723
- return record if record
724
- return nil unless config[:fallback_to_database]
725
- end
726
- record = ctx.context.adapter.find_one(model: API_KEY_TABLE_NAME, where: [{field: "key", value: hashed}])
727
- api_key_storage_set(ctx, record, config) if record && config[:storage] == "secondary-storage" && config[:fallback_to_database]
728
- record
199
+ BetterAuth::APIKey::Adapter.find_by_hash(ctx, hashed, config)
729
200
  end
730
201
 
731
202
  def api_key_find_by_id(ctx, id, config)
732
- if config[:storage] == "secondary-storage"
733
- record = api_key_storage_get(ctx, "api-key:by-id:#{id}", config) || api_key_storage_get(ctx, "api-key:id:#{id}", config)
734
- return record if record
735
- return nil unless config[:fallback_to_database]
736
- end
737
- record = ctx.context.adapter.find_one(model: API_KEY_TABLE_NAME, where: [{field: "id", value: id}])
738
- api_key_storage_set(ctx, record, config) if record && config[:storage] == "secondary-storage" && config[:fallback_to_database]
739
- record
203
+ BetterAuth::APIKey::Adapter.find_by_id(ctx, id, config)
740
204
  end
741
205
 
742
206
  def api_key_list_for_user(ctx, user_id, config)
@@ -744,279 +208,107 @@ module BetterAuth
744
208
  end
745
209
 
746
210
  def api_key_list_for_reference(ctx, reference_id, config)
747
- if config[:storage] == "secondary-storage"
748
- begin
749
- storage = api_key_storage(config, ctx.context)
750
- ids = JSON.parse((storage&.get("api-key:by-ref:#{reference_id}") || storage&.get("api-key:user:#{reference_id}")).to_s)
751
- records = ids.filter_map { |id| api_key_find_by_id(ctx, id, config) }
752
- return records unless records.empty? && config[:fallback_to_database]
753
- rescue JSON::ParserError, NoMethodError
754
- return [] unless config[:fallback_to_database]
755
- end
756
- end
757
- records = ctx.context.adapter.find_many(model: API_KEY_TABLE_NAME, where: [{field: "referenceId", value: reference_id}])
758
- legacy = ctx.context.adapter.find_many(model: API_KEY_TABLE_NAME, where: [{field: "userId", value: reference_id}])
759
- combined = (records + legacy).uniq { |record| record["id"] }
760
- api_key_storage_populate_reference(ctx, reference_id, combined, config) if config[:storage] == "secondary-storage" && config[:fallback_to_database]
761
- combined
211
+ BetterAuth::APIKey::Adapter.list_for_reference(ctx, reference_id, config)
762
212
  end
763
213
 
764
214
  def api_key_update_record(ctx, record, update, config, defer: false)
765
- performer = lambda do
766
- updated = nil
767
- if config[:storage] == "database" || config[:fallback_to_database]
768
- updated = ctx.context.adapter.update(model: API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}], update: update)
769
- end
770
- updated ||= record.merge(update.transform_keys { |key| Schema.storage_key(key) })
771
- api_key_storage_set(ctx, updated, config) if config[:storage] == "secondary-storage"
772
- updated
773
- end
774
-
775
- if defer && config[:defer_updates] && api_key_background_tasks?(ctx)
776
- scheduled = record.merge(update.transform_keys { |key| Schema.storage_key(key) })
777
- ctx.context.run_in_background(performer)
778
- scheduled
779
- else
780
- performer.call
781
- end
215
+ BetterAuth::APIKey::Adapter.update_record(ctx, record, update, config, defer: defer)
782
216
  end
783
217
 
784
218
  def api_key_delete_record(ctx, record, config)
785
- ctx.context.adapter.delete(model: API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}]) if config[:storage] == "database" || config[:fallback_to_database]
786
- api_key_storage_delete(ctx, record, config) if config[:storage] == "secondary-storage"
219
+ BetterAuth::APIKey::Adapter.delete_record(ctx, record, config)
787
220
  end
788
221
 
789
222
  def api_key_schedule_record_delete(ctx, record, config)
790
- task = -> { api_key_delete_record(ctx, record, config) }
791
- if config[:defer_updates] && api_key_background_tasks?(ctx)
792
- ctx.context.run_in_background(task)
793
- else
794
- task.call
795
- end
223
+ BetterAuth::APIKey::Adapter.schedule_record_delete(ctx, record, config)
796
224
  end
797
225
 
798
226
  def api_key_schedule_cleanup(ctx, config)
799
- task = -> { api_key_delete_expired(ctx.context, config) }
800
- if config[:defer_updates] && api_key_background_tasks?(ctx)
801
- ctx.context.run_in_background(task)
802
- else
803
- task.call
804
- end
227
+ BetterAuth::APIKey::Routes.schedule_cleanup(ctx, config)
805
228
  end
806
229
 
807
- @api_key_last_expired_check = nil
808
-
809
230
  def api_key_delete_expired(context, config, bypass_last_check: false)
810
- return unless config[:storage] == "database" || config[:fallback_to_database]
811
- unless bypass_last_check
812
- now = Time.now
813
- return if @api_key_last_expired_check && ((now - @api_key_last_expired_check) * 1000) < 10_000
814
-
815
- @api_key_last_expired_check = now
816
- end
817
-
818
- expired = context.adapter.find_many(model: API_KEY_TABLE_NAME).select do |record|
819
- record["expiresAt"] && record["expiresAt"] < Time.now
820
- end
821
- expired.each do |record|
822
- context.adapter.delete(model: API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}])
823
- end
231
+ BetterAuth::APIKey::Routes.delete_expired(context, config, bypass_last_check: bypass_last_check)
824
232
  end
825
233
 
826
234
  def api_key_storage(config, context = nil)
827
- config[:custom_storage] || context&.options&.secondary_storage
235
+ BetterAuth::APIKey::Adapter.storage(config, context)
828
236
  end
829
237
 
830
238
  def api_key_storage_get(ctx, key, config)
831
- raw = api_key_storage(config, ctx.context)&.get(key)
832
- raw && api_key_deserialize_storage_record(JSON.parse(raw))
833
- rescue JSON::ParserError
834
- nil
239
+ BetterAuth::APIKey::Adapter.get(ctx, key, config)
835
240
  end
836
241
 
837
242
  def api_key_storage_set(ctx, record, config)
838
- storage = api_key_storage(config, ctx.context)
839
- unless storage
840
- raise APIError.new("INTERNAL_SERVER_ERROR", message: "Secondary storage is required when storage mode is 'secondary-storage'")
841
- end
842
-
843
- serialized = JSON.generate(api_key_storage_record(record))
844
- expires_at = api_key_normalize_time(record["expiresAt"])
845
- ttl = expires_at ? [(expires_at - Time.now).to_i, 0].max : nil
846
- reference_id = api_key_record_reference_id(record)
847
- user_key = "api-key:by-ref:#{reference_id}"
848
-
849
- api_key_storage_batch(storage) do
850
- operations = [
851
- -> { storage.set("api-key:#{record["key"]}", serialized, ttl) },
852
- -> { storage.set("api-key:by-id:#{record["id"]}", serialized, ttl) }
853
- ]
854
- operations << if config[:fallback_to_database]
855
- # In fallback mode the ref list is a cache invalidated on writes
856
- # to avoid races with concurrent writers of the same reference.
857
- -> { storage.delete(user_key) }
858
- else
859
- -> { api_key_ref_list_add(storage, user_key, record["id"]) }
860
- end
861
- operations.each(&:call)
862
- end
243
+ BetterAuth::APIKey::Adapter.set(ctx, record, config)
863
244
  end
864
245
 
865
246
  def api_key_storage_delete(ctx, record, config)
866
- storage = api_key_storage(config, ctx.context)
867
- return unless storage
868
-
869
- reference_id = api_key_record_reference_id(record)
870
- user_key = "api-key:by-ref:#{reference_id}"
871
-
872
- api_key_storage_batch(storage) do
873
- operations = [
874
- -> { storage.delete("api-key:#{record["key"]}") },
875
- -> { storage.delete("api-key:by-id:#{record["id"]}") },
876
- # Ruby-only legacy storage layout cleanup; upstream never wrote here.
877
- -> { storage.delete("api-key:key:#{record["key"]}") },
878
- -> { storage.delete("api-key:id:#{record["id"]}") }
879
- ]
880
- operations << if config[:fallback_to_database]
881
- -> { storage.delete(user_key) }
882
- else
883
- -> { api_key_ref_list_remove(storage, user_key, record["id"]) }
884
- end
885
- operations.each(&:call)
886
- end
247
+ BetterAuth::APIKey::Adapter.delete(ctx, record, config)
887
248
  end
888
249
 
889
250
  def api_key_ref_list_add(storage, user_key, id)
890
- ids = api_key_safe_parse_id_list(storage.get(user_key))
891
- ids << id unless ids.include?(id)
892
- storage.set(user_key, JSON.generate(ids))
251
+ BetterAuth::APIKey::Adapter.ref_list_add(storage, user_key, id)
893
252
  end
894
253
 
895
254
  def api_key_ref_list_remove(storage, user_key, id)
896
- ids = api_key_safe_parse_id_list(storage.get(user_key)).reject { |existing| existing == id }
897
- ids.empty? ? storage.delete(user_key) : storage.set(user_key, JSON.generate(ids))
255
+ BetterAuth::APIKey::Adapter.ref_list_remove(storage, user_key, id)
898
256
  end
899
257
 
900
258
  def api_key_safe_parse_id_list(raw)
901
- return [] if raw.nil?
902
-
903
- parsed = JSON.parse(raw.to_s)
904
- parsed.is_a?(Array) ? parsed : []
905
- rescue JSON::ParserError
906
- []
259
+ BetterAuth::APIKey::Adapter.safe_parse_id_list(raw)
907
260
  end
908
261
 
909
262
  def api_key_storage_batch(storage, &block)
910
- if storage.respond_to?(:batch)
911
- storage.batch(&block)
912
- else
913
- block.call
914
- end
263
+ BetterAuth::APIKey::Adapter.batch(storage, &block)
915
264
  end
916
265
 
917
266
  def api_key_storage_populate_reference(ctx, reference_id, records, config)
918
- storage = api_key_storage(config, ctx.context)
919
- return unless storage
920
-
921
- ids = []
922
- records.each do |record|
923
- serialized = JSON.generate(api_key_storage_record(record))
924
- expires_at = api_key_normalize_time(record["expiresAt"])
925
- ttl = expires_at ? [(expires_at - Time.now).to_i, 0].max : nil
926
- storage.set("api-key:#{record["key"]}", serialized, ttl)
927
- storage.set("api-key:by-id:#{record["id"]}", serialized, ttl)
928
- ids << record["id"]
929
- end
930
- 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)
931
268
  end
932
269
 
933
270
  def api_key_storage_record(record)
934
- record.transform_values { |value| value.is_a?(Time) ? value.iso8601 : value }
271
+ BetterAuth::APIKey::Adapter.storage_record(record)
935
272
  end
936
273
 
937
274
  def api_key_deserialize_storage_record(record)
938
- %w[createdAt updatedAt expiresAt lastRefillAt lastRequest].each do |field|
939
- record[field] = api_key_normalize_time(record[field]) if record[field]
940
- end
941
- record
275
+ BetterAuth::APIKey::Adapter.deserialize_record(record)
942
276
  end
943
277
 
944
278
  def api_key_public(record, reveal_key: nil, include_key_field: false)
945
- data = record.transform_keys(&:to_sym)
946
- output = data.except(:key)
947
- output[:configId] ||= api_key_record_config_id(record)
948
- output[:referenceId] ||= api_key_record_reference_id(record)
949
- output[:key] = reveal_key if include_key_field && reveal_key
950
- output[:metadata] = api_key_decode_json(data[:metadata])
951
- output[:permissions] = api_key_decode_json(data[:permissions])
952
- output
279
+ BetterAuth::APIKey::Utils.public_record(record, reveal_key: reveal_key, include_key_field: include_key_field)
953
280
  end
954
281
 
955
282
  def api_key_migrate_legacy_metadata(ctx, record, config)
956
- parsed = api_key_decode_json(record["metadata"])
957
- return record unless parsed.is_a?(Hash)
958
-
959
- encoded = api_key_encode_json(parsed)
960
- return record.merge("metadata" => encoded) if record["metadata"] == encoded
961
-
962
- updated = record.merge("metadata" => encoded)
963
- if config[:storage] == "database" || config[:fallback_to_database]
964
- ctx.context.adapter.update(model: API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}], update: {metadata: encoded})
965
- end
966
- api_key_storage_set(ctx, updated, config) if config[:storage] == "secondary-storage"
967
- updated
283
+ BetterAuth::APIKey::Adapter.migrate_legacy_metadata(ctx, record, config)
968
284
  end
969
285
 
970
286
  def api_key_background_tasks?(ctx)
971
- ctx.context.options.advanced.dig(:background_tasks, :handler).respond_to?(:call)
287
+ BetterAuth::APIKey::Utils.background_tasks?(ctx)
972
288
  end
973
289
 
974
290
  def api_key_auth_required?(ctx)
975
- !!(ctx.request || (ctx.headers && !ctx.headers.empty?))
291
+ BetterAuth::APIKey::Utils.auth_required?(ctx)
976
292
  end
977
293
 
978
294
  def api_key_get_from_headers(ctx, config)
979
- getter = config[:custom_api_key_getter]
980
- return getter.call(ctx) if getter.respond_to?(:call)
981
-
982
- Array(config[:api_key_headers]).each do |header|
983
- value = ctx.headers[header.to_s.downcase]
984
- return value if value
985
- end
986
- nil
295
+ BetterAuth::APIKey::Keys.from_headers(ctx, config)
987
296
  end
988
297
 
989
298
  def api_key_check_permissions!(record, required)
990
- return if required.nil? || required == {}
991
-
992
- actual = api_key_decode_json(record["permissions"]) || {}
993
- result = Role.new(actual).authorize(required)
994
- unless result[:success]
995
- raise APIError.new("UNAUTHORIZED", message: API_KEY_ERROR_CODES["KEY_NOT_FOUND"], code: "KEY_NOT_FOUND")
996
- end
299
+ BetterAuth::APIKey::Validation.check_permissions!(record, required)
997
300
  end
998
301
 
999
302
  def api_key_encode_json(value)
1000
- return nil if value.nil?
1001
-
1002
- JSON.generate(value)
303
+ BetterAuth::APIKey::Utils.encode_json(value)
1003
304
  end
1004
305
 
1005
306
  def api_key_decode_json(value)
1006
- return nil if value.nil?
1007
- return value if value.is_a?(Hash)
1008
-
1009
- parsed = JSON.parse(value.to_s)
1010
- parsed.is_a?(String) ? api_key_decode_json(parsed) : parsed
1011
- rescue JSON::ParserError
1012
- nil
307
+ BetterAuth::APIKey::Utils.decode_json(value)
1013
308
  end
1014
309
 
1015
310
  def api_key_normalize_time(value)
1016
- return value if value.is_a?(Time)
1017
- return nil if value.nil?
1018
-
1019
- Time.parse(value.to_s)
311
+ BetterAuth::APIKey::Utils.normalize_time(value)
1020
312
  end
1021
313
  end
1022
314
  end