Module: BetterAuth::APIKey::Adapter
- Defined in:
- lib/better_auth/api_key/adapter.rb
Constant Summary collapse
- HASH_STORAGE_PREFIX =
"api-key:"- ID_STORAGE_PREFIX =
"api-key:by-id:"- REFERENCE_STORAGE_PREFIX =
"api-key:by-ref:"- REFERENCE_LOCKS_GUARD =
Mutex.new
- REFERENCE_LOCK_STRIPE_COUNT =
256
Class Method Summary collapse
- .batch(storage_instance, &block) ⇒ Object
- .batch_migrate_legacy_metadata(ctx, records, config) ⇒ Object
- .delete(ctx, record, config) ⇒ Object
- .delete_record(ctx, record, config) ⇒ Object
- .deserialize_record(record) ⇒ Object
- .find_by_hash(ctx, hashed, config) ⇒ Object
- .find_by_id(ctx, id, config) ⇒ Object
- .get(ctx, key, config) ⇒ Object
- .legacy_metadata_migration_needed?(record) ⇒ Boolean
- .list_for_reference(ctx, reference_id, config) ⇒ Object
- .migrate_legacy_metadata(ctx, record, config) ⇒ Object
- .parse_id_list!(raw) ⇒ Object
- .populate_reference(ctx, reference_id, records, config) ⇒ Object
- .ref_list_add(storage_instance, reference_key, id) ⇒ Object
- .ref_list_remove(storage_instance, reference_key, id) ⇒ Object
- .safe_parse_id_list(raw) ⇒ Object
- .schedule_record_delete(ctx, record, config) ⇒ Object
- .set(ctx, record, config) ⇒ Object
- .storage(config, context = nil) ⇒ Object
- .storage_key_by_hash(hashed_key) ⇒ Object
- .storage_key_by_id(id) ⇒ Object
- .storage_key_by_reference(reference_id) ⇒ Object
- .storage_record(record) ⇒ Object
- .store(ctx, data, config) ⇒ Object
- .update_record(ctx, record, update, config, defer: false) ⇒ Object
-
.with_reference_lock(reference_key) ⇒ Object
Custom secondary-storage implementations expose only get/set/delete, so serialize reference-list read/modify/write operations within this Ruby process.
Class Method Details
.batch(storage_instance, &block) ⇒ Object
255 256 257 258 259 260 261 |
# File 'lib/better_auth/api_key/adapter.rb', line 255 def batch(storage_instance, &block) if storage_instance.respond_to?(:batch) storage_instance.batch(&block) else block.call end end |
.batch_migrate_legacy_metadata(ctx, records, config) ⇒ Object
282 283 284 285 286 |
# File 'lib/better_auth/api_key/adapter.rb', line 282 def (ctx, records, config) return records unless config[:storage] == "database" || config[:fallback_to_database] records.map { |record| (ctx, record, config) } end |
.delete(ctx, record, config) ⇒ Object
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/better_auth/api_key/adapter.rb', line 177 def delete(ctx, record, config) storage_instance = storage(config, ctx.context) return unless storage_instance reference_id = BetterAuth::Plugins.api_key_record_reference_id(record) reference_key = storage_key_by_reference(reference_id) batch(storage_instance) do operations = [ -> { storage_instance.delete(storage_key_by_hash(record["key"])) }, -> { storage_instance.delete(storage_key_by_id(record["id"])) }, # Ruby-only legacy storage layout cleanup; upstream never wrote here. -> { storage_instance.delete("api-key:key:#{record["key"]}") }, -> { storage_instance.delete("api-key:id:#{record["id"]}") } ] operations << if config[:fallback_to_database] -> { storage_instance.delete(reference_key) } else -> { ref_list_remove(storage_instance, reference_key, record["id"]) } end operations.each(&:call) end end |
.delete_record(ctx, record, config) ⇒ Object
106 107 108 109 |
# File 'lib/better_auth/api_key/adapter.rb', line 106 def delete_record(ctx, record, config) ctx.context.adapter.delete(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}]) if config[:storage] == "database" || config[:fallback_to_database] delete(ctx, record, config) if config[:storage] == "secondary-storage" end |
.deserialize_record(record) ⇒ Object
292 293 294 295 296 297 |
# File 'lib/better_auth/api_key/adapter.rb', line 292 def deserialize_record(record) %w[createdAt updatedAt expiresAt lastRefillAt lastRequest].each do |field| record[field] = BetterAuth::APIKey::Utils.normalize_time(record[field]) if record[field] end record end |
.find_by_hash(ctx, hashed, config) ⇒ Object
40 41 42 43 44 45 46 47 48 49 |
# File 'lib/better_auth/api_key/adapter.rb', line 40 def find_by_hash(ctx, hashed, config) if config[:storage] == "secondary-storage" record = get(ctx, storage_key_by_hash(hashed), config) || get(ctx, "api-key:key:#{hashed}", config) return record if record return nil unless config[:fallback_to_database] end record = ctx.context.adapter.find_one(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "key", value: hashed}]) set(ctx, record, config) if record && config[:storage] == "secondary-storage" && config[:fallback_to_database] record end |
.find_by_id(ctx, id, config) ⇒ Object
51 52 53 54 55 56 57 58 59 60 |
# File 'lib/better_auth/api_key/adapter.rb', line 51 def find_by_id(ctx, id, config) if config[:storage] == "secondary-storage" record = get(ctx, storage_key_by_id(id), config) || get(ctx, "api-key:id:#{id}", config) return record if record return nil unless config[:fallback_to_database] end record = ctx.context.adapter.find_one(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: id}]) set(ctx, record, config) if record && config[:storage] == "secondary-storage" && config[:fallback_to_database] record end |
.get(ctx, key, config) ⇒ Object
144 145 146 147 148 149 |
# File 'lib/better_auth/api_key/adapter.rb', line 144 def get(ctx, key, config) raw = storage(config, ctx.context)&.get(key) raw && deserialize_record(JSON.parse(raw)) rescue JSON::ParserError nil end |
.legacy_metadata_migration_needed?(record) ⇒ Boolean
135 136 137 138 |
# File 'lib/better_auth/api_key/adapter.rb', line 135 def (record) parsed = BetterAuth::APIKey::Utils.decode_json(record["metadata"]) parsed.is_a?(Hash) && BetterAuth::APIKey::Utils.encode_json(parsed) != record["metadata"] end |
.list_for_reference(ctx, reference_id, config) ⇒ Object
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/better_auth/api_key/adapter.rb', line 62 def list_for_reference(ctx, reference_id, config) if config[:storage] == "secondary-storage" begin storage_instance = storage(config, ctx.context) raw_ids = storage_instance&.get(storage_key_by_reference(reference_id)) || storage_instance&.get("api-key:user:#{reference_id}") ids = parse_id_list!(raw_ids) records = ids.filter_map { |id| find_by_id(ctx, id, config) } return records unless records.empty? && config[:fallback_to_database] rescue JSON::ParserError, NoMethodError => error if ctx.context.respond_to?(:logger) && ctx.context.logger.respond_to?(:warn) ctx.context.logger.warn("[API KEY PLUGIN] Corrupt api-key reference index for #{reference_id.inspect}: #{error.class}: #{error.}") end return [] unless config[:fallback_to_database] end end records = ctx.context.adapter.find_many(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "referenceId", value: reference_id}]) legacy = ctx.context.adapter.find_many(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "userId", value: reference_id}]) combined = (records + legacy).uniq { |record| record["id"] } populate_reference(ctx, reference_id, combined, config) if config[:storage] == "secondary-storage" && config[:fallback_to_database] combined end |
.migrate_legacy_metadata(ctx, record, config) ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/better_auth/api_key/adapter.rb', line 120 def (ctx, record, config) parsed = BetterAuth::APIKey::Utils.decode_json(record["metadata"]) return record unless parsed.is_a?(Hash) encoded = BetterAuth::APIKey::Utils.encode_json(parsed) return record.merge("metadata" => encoded) if record["metadata"] == encoded updated = record.merge("metadata" => encoded) if config[:storage] == "database" || config[:fallback_to_database] ctx.context.adapter.update(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}], update: {metadata: encoded}) end set(ctx, updated, config) if config[:storage] == "secondary-storage" updated end |
.parse_id_list!(raw) ⇒ Object
247 248 249 250 251 252 253 |
# File 'lib/better_auth/api_key/adapter.rb', line 247 def parse_id_list!(raw) return [] if raw.nil? return raw.dup if raw.is_a?(Array) parsed = JSON.parse(raw.to_s) parsed.is_a?(Array) ? parsed : [] end |
.populate_reference(ctx, reference_id, records, config) ⇒ Object
263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 |
# File 'lib/better_auth/api_key/adapter.rb', line 263 def populate_reference(ctx, reference_id, records, config) storage_instance = storage(config, ctx.context) return unless storage_instance batch(storage_instance) do ids = [] records.each do |record| serialized = JSON.generate(storage_record(record)) expires_at = BetterAuth::APIKey::Utils.normalize_time(record["expiresAt"]) ttl = expires_at ? [(expires_at - Time.now).to_i, 0].max : nil storage_instance.set(storage_key_by_hash(record["key"]), serialized, ttl) storage_instance.set(storage_key_by_id(record["id"]), serialized, ttl) ids << record["id"] end reference_key = storage_key_by_reference(reference_id) ids.empty? ? storage_instance.delete(reference_key) : storage_instance.set(reference_key, JSON.generate(ids)) end end |
.ref_list_add(storage_instance, reference_key, id) ⇒ Object
201 202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/better_auth/api_key/adapter.rb', line 201 def ref_list_add(storage_instance, reference_key, id) if storage_instance.respond_to?(:json_list_add) storage_instance.json_list_add(reference_key, id) return end with_reference_lock(reference_key) do ids = safe_parse_id_list(storage_instance.get(reference_key)) ids << id unless ids.include?(id) storage_instance.set(reference_key, JSON.generate(ids)) end end |
.ref_list_remove(storage_instance, reference_key, id) ⇒ Object
214 215 216 217 218 219 220 221 222 223 224 |
# File 'lib/better_auth/api_key/adapter.rb', line 214 def ref_list_remove(storage_instance, reference_key, id) if storage_instance.respond_to?(:json_list_remove) storage_instance.json_list_remove(reference_key, id) return end with_reference_lock(reference_key) do ids = safe_parse_id_list(storage_instance.get(reference_key)).reject { |existing| existing == id } ids.empty? ? storage_instance.delete(reference_key) : storage_instance.set(reference_key, JSON.generate(ids)) end end |
.safe_parse_id_list(raw) ⇒ Object
238 239 240 241 242 243 244 245 |
# File 'lib/better_auth/api_key/adapter.rb', line 238 def safe_parse_id_list(raw) return [] if raw.nil? return raw.dup if raw.is_a?(Array) parse_id_list!(raw) rescue JSON::ParserError [] end |
.schedule_record_delete(ctx, record, config) ⇒ Object
111 112 113 114 115 116 117 118 |
# File 'lib/better_auth/api_key/adapter.rb', line 111 def schedule_record_delete(ctx, record, config) task = -> { delete_record(ctx, record, config) } if config[:defer_updates] && BetterAuth::APIKey::Utils.background_tasks?(ctx) BetterAuth::APIKey::Utils.run_background_task(ctx, "Deferred API key delete", task) else task.call end end |
.set(ctx, record, config) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/better_auth/api_key/adapter.rb', line 151 def set(ctx, record, config) storage_instance = storage(config, ctx.context) unless storage_instance raise BetterAuth::APIError.new("INTERNAL_SERVER_ERROR", message: "Secondary storage is required when storage mode is 'secondary-storage'") end serialized = JSON.generate(storage_record(record)) expires_at = BetterAuth::APIKey::Utils.normalize_time(record["expiresAt"]) ttl = expires_at ? [(expires_at - Time.now).to_i, 0].max : nil reference_id = BetterAuth::Plugins.api_key_record_reference_id(record) reference_key = storage_key_by_reference(reference_id) batch(storage_instance) do operations = [ -> { storage_instance.set(storage_key_by_hash(record["key"]), serialized, ttl) }, -> { storage_instance.set(storage_key_by_id(record["id"]), serialized, ttl) } ] operations << if config[:fallback_to_database] -> { storage_instance.delete(reference_key) } else -> { ref_list_add(storage_instance, reference_key, record["id"]) } end operations.each(&:call) end end |
.storage(config, context = nil) ⇒ Object
140 141 142 |
# File 'lib/better_auth/api_key/adapter.rb', line 140 def storage(config, context = nil) config[:custom_storage] || context&.&.secondary_storage end |
.storage_key_by_hash(hashed_key) ⇒ Object
18 19 20 |
# File 'lib/better_auth/api_key/adapter.rb', line 18 def storage_key_by_hash(hashed_key) "#{HASH_STORAGE_PREFIX}#{hashed_key}" end |
.storage_key_by_id(id) ⇒ Object
22 23 24 |
# File 'lib/better_auth/api_key/adapter.rb', line 22 def storage_key_by_id(id) "#{ID_STORAGE_PREFIX}#{id}" end |
.storage_key_by_reference(reference_id) ⇒ Object
26 27 28 |
# File 'lib/better_auth/api_key/adapter.rb', line 26 def storage_key_by_reference(reference_id) "#{REFERENCE_STORAGE_PREFIX}#{reference_id}" end |
.storage_record(record) ⇒ Object
288 289 290 |
# File 'lib/better_auth/api_key/adapter.rb', line 288 def storage_record(record) record.transform_values { |value| value.is_a?(Time) ? value.iso8601 : value } end |
.store(ctx, data, config) ⇒ Object
30 31 32 33 34 35 36 37 38 |
# File 'lib/better_auth/api_key/adapter.rb', line 30 def store(ctx, data, config) record = nil if config[:storage] == "database" || config[:fallback_to_database] record = ctx.context.adapter.create(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, data: data) end record ||= data.transform_keys { |key| BetterAuth::Schema.storage_key(key) }.merge("id" => SecureRandom.hex(16)) set(ctx, record, config) if config[:storage] == "secondary-storage" record end |
.update_record(ctx, record, update, config, defer: false) ⇒ Object
84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/better_auth/api_key/adapter.rb', line 84 def update_record(ctx, record, update, config, defer: false) performer = lambda do updated = nil if config[:storage] == "database" || config[:fallback_to_database] updated = ctx.context.adapter.update(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}], update: update) return nil unless updated else updated = record.merge(update.transform_keys { |key| BetterAuth::Schema.storage_key(key) }) end set(ctx, updated, config) if config[:storage] == "secondary-storage" updated end if defer && config[:defer_updates] && BetterAuth::Plugins.api_key_background_tasks?(ctx) scheduled = record.merge(update.transform_keys { |key| BetterAuth::Schema.storage_key(key) }) BetterAuth::APIKey::Utils.run_background_task(ctx, "Deferred API key update", performer) scheduled else performer.call end end |
.with_reference_lock(reference_key) ⇒ Object
Custom secondary-storage implementations expose only get/set/delete, so serialize reference-list read/modify/write operations within this Ruby process. This lock is deliberately not treated as distributed safety; RedisStorage uses its atomic JSON-list scripts above.
230 231 232 233 234 235 236 |
# File 'lib/better_auth/api_key/adapter.rb', line 230 def with_reference_lock(reference_key) lock = REFERENCE_LOCKS_GUARD.synchronize do @reference_lock_stripes ||= Array.new(REFERENCE_LOCK_STRIPE_COUNT) { Mutex.new } @reference_lock_stripes[reference_key.hash % REFERENCE_LOCK_STRIPE_COUNT] end lock.synchronize { yield } end |