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.
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module APIKey
5
+ module Validation
6
+ module_function
7
+
8
+ def validate_create_update!(body, config, create:, client:)
9
+ name = body[:name]
10
+ if create && config[:require_name] && name.to_s.empty?
11
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["NAME_REQUIRED"])
12
+ end
13
+ if name && !name.to_s.length.between?(config[:minimum_name_length].to_i, config[:maximum_name_length].to_i)
14
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_NAME_LENGTH"])
15
+ end
16
+ prefix = body[:prefix]
17
+ if prefix && !prefix.to_s.length.between?(config[:minimum_prefix_length].to_i, config[:maximum_prefix_length].to_i)
18
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_PREFIX_LENGTH"])
19
+ end
20
+ if prefix && !prefix.to_s.match?(/\A[a-zA-Z0-9_-]+\z/)
21
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_PREFIX_LENGTH"])
22
+ end
23
+ if body.key?(:remaining) && !body[:remaining].nil?
24
+ minimum = create ? 0 : 1
25
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_REMAINING"]) if body[:remaining].to_i < minimum
26
+ end
27
+ if body[:metadata] && (create || config[:enable_metadata])
28
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["METADATA_DISABLED"]) unless config[:enable_metadata]
29
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_METADATA_TYPE"]) unless body[:metadata].nil? || body[:metadata].is_a?(Hash)
30
+ end
31
+ server_only_keys = %i[refill_amount refill_interval rate_limit_max rate_limit_time_window rate_limit_enabled remaining permissions]
32
+ if client && server_only_keys.any? { |key| (create && key == :remaining) ? !body[:remaining].nil? : body.key?(key) }
33
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["SERVER_ONLY_PROPERTY"])
34
+ end
35
+ amount_present = body.key?(:refill_amount)
36
+ interval_present = body.key?(:refill_interval)
37
+ if amount_present && !interval_present
38
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["REFILL_AMOUNT_AND_INTERVAL_REQUIRED"])
39
+ end
40
+ if interval_present && !amount_present
41
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["REFILL_INTERVAL_AND_AMOUNT_REQUIRED"])
42
+ end
43
+ if body.key?(:expires_in)
44
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_DISABLED_EXPIRATION"]) if config[:key_expiration][:disable_custom_expires_time]
45
+ return if body[:expires_in].nil?
46
+
47
+ days = body[:expires_in].to_f / 86_400
48
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["EXPIRES_IN_IS_TOO_SMALL"]) if days < config[:key_expiration][:min_expires_in].to_f
49
+ raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["EXPIRES_IN_IS_TOO_LARGE"]) if days > config[:key_expiration][:max_expires_in].to_f
50
+ end
51
+ end
52
+
53
+ def update_payload(body, config)
54
+ update = {}
55
+ update[:name] = body[:name] if body.key?(:name)
56
+ update[:enabled] = body[:enabled] unless body[:enabled].nil?
57
+ update[:remaining] = body[:remaining] if body.key?(:remaining)
58
+ update[:refillAmount] = body[:refill_amount] if body.key?(:refill_amount)
59
+ update[:refillInterval] = body[:refill_interval] if body.key?(:refill_interval)
60
+ update[:rateLimitEnabled] = body[:rate_limit_enabled] if body.key?(:rate_limit_enabled)
61
+ update[:rateLimitTimeWindow] = body[:rate_limit_time_window] if body.key?(:rate_limit_time_window)
62
+ update[:rateLimitMax] = body[:rate_limit_max] if body.key?(:rate_limit_max)
63
+ update[:expiresAt] = body[:expires_in].nil? ? nil : Time.now + body[:expires_in].to_i if body.key?(:expires_in)
64
+ update[:metadata] = BetterAuth::APIKey::Utils.encode_json(body[:metadata]) if body.key?(:metadata) && config[:enable_metadata]
65
+ update[:permissions] = BetterAuth::APIKey::Utils.encode_json(body[:permissions]) if body.key?(:permissions)
66
+ update
67
+ end
68
+
69
+ def validate_api_key!(ctx, key, config, permissions: nil)
70
+ hashed = BetterAuth::APIKey::Keys.hash(key, config)
71
+ record = BetterAuth::APIKey::Adapter.find_by_hash(ctx, hashed, config)
72
+ raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"]) unless record
73
+ unless BetterAuth::APIKey::Routes.config_id_matches?(BetterAuth::APIKey::Types.record_config_id(record), config[:config_id])
74
+ raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"])
75
+ end
76
+ raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_DISABLED"]) if record["enabled"] == false
77
+ if record["expiresAt"] && record["expiresAt"] <= Time.now
78
+ BetterAuth::APIKey::Adapter.schedule_record_delete(ctx, record, config)
79
+ raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_EXPIRED"])
80
+ end
81
+ if record["remaining"].to_i <= 0 && !record["remaining"].nil? && record["refillAmount"].nil?
82
+ BetterAuth::APIKey::Adapter.schedule_record_delete(ctx, record, config)
83
+ raise BetterAuth::APIError.new("TOO_MANY_REQUESTS", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["USAGE_EXCEEDED"])
84
+ end
85
+
86
+ check_permissions!(record, permissions)
87
+ update = usage_update(record, config)
88
+ updated = BetterAuth::APIKey::Adapter.update_record(ctx, record, update, config, defer: true)
89
+ BetterAuth::APIKey::Adapter.migrate_legacy_metadata(ctx, updated || record.merge(update.transform_keys { |key_name| BetterAuth::Schema.storage_key(key_name) }), config)
90
+ end
91
+
92
+ def usage_update(record, config)
93
+ now = Time.now
94
+ update = {lastRequest: now, updatedAt: now}
95
+
96
+ if (try_again_in = BetterAuth::APIKey::RateLimit.try_again_in(record, config, now))
97
+ raise BetterAuth::APIError.new(
98
+ "UNAUTHORIZED",
99
+ message: BetterAuth::Plugins::API_KEY_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
100
+ code: "RATE_LIMITED",
101
+ body: {
102
+ message: BetterAuth::Plugins::API_KEY_ERROR_CODES["RATE_LIMIT_EXCEEDED"],
103
+ code: "RATE_LIMITED",
104
+ details: {tryAgainIn: try_again_in}
105
+ }
106
+ )
107
+ end
108
+ update[:requestCount] = BetterAuth::APIKey::RateLimit.next_request_count(record, now) if BetterAuth::APIKey::RateLimit.counts_requests?(record, config)
109
+
110
+ remaining = record["remaining"]
111
+ if !remaining.nil?
112
+ if remaining.to_i <= 0 && record["refillAmount"] && record["refillInterval"]
113
+ last_refill = BetterAuth::APIKey::Utils.normalize_time(record["lastRefillAt"] || record["createdAt"])
114
+ if !last_refill || ((now - last_refill) * 1000) > record["refillInterval"].to_i
115
+ remaining = record["refillAmount"].to_i
116
+ update[:lastRefillAt] = now
117
+ end
118
+ end
119
+ raise BetterAuth::APIError.new("TOO_MANY_REQUESTS", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["USAGE_EXCEEDED"]) if remaining.to_i <= 0
120
+
121
+ update[:remaining] = remaining.to_i - 1
122
+ end
123
+ update
124
+ end
125
+
126
+ def check_permissions!(record, required)
127
+ return if required.nil? || required == {}
128
+
129
+ actual = BetterAuth::APIKey::Utils.decode_json(record["permissions"]) || {}
130
+ result = BetterAuth::Plugins::Role.new(actual).authorize(required)
131
+ unless result[:success]
132
+ raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_NOT_FOUND"], code: "KEY_NOT_FOUND")
133
+ end
134
+ end
135
+ end
136
+ end
137
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module APIKey
5
- VERSION = "0.2.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -2,6 +2,26 @@
2
2
 
3
3
  require "better_auth"
4
4
  require_relative "api_key/version"
5
+ require_relative "api_key/error_codes"
6
+ require_relative "api_key/types"
7
+ require_relative "api_key/utils"
8
+ require_relative "api_key/rate_limit"
9
+ require_relative "api_key/keys"
10
+ require_relative "api_key/adapter"
11
+ require_relative "api_key/schema"
12
+ require_relative "api_key/org_authorization"
13
+ require_relative "api_key/validation"
14
+ require_relative "api_key/configuration"
15
+ require_relative "api_key/session"
16
+ require_relative "api_key/plugin_factory"
17
+ require_relative "api_key/routes/index"
18
+ require_relative "api_key/routes/create_api_key"
19
+ require_relative "api_key/routes/verify_api_key"
20
+ require_relative "api_key/routes/get_api_key"
21
+ require_relative "api_key/routes/update_api_key"
22
+ require_relative "api_key/routes/delete_api_key"
23
+ require_relative "api_key/routes/list_api_keys"
24
+ require_relative "api_key/routes/delete_all_expired_api_keys"
5
25
  require_relative "plugins/api_key"
6
26
 
7
27
  module BetterAuth