better_auth-api-key 0.6.2 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +28 -0
- data/lib/better_auth/api_key/adapter.rb +41 -16
- data/lib/better_auth/api_key/routes/delete_all_expired_api_keys.rb +1 -1
- data/lib/better_auth/api_key/routes/index.rb +9 -7
- data/lib/better_auth/api_key/routes/list_api_keys.rb +14 -2
- data/lib/better_auth/api_key/session.rb +1 -1
- data/lib/better_auth/api_key/utils.rb +10 -0
- data/lib/better_auth/api_key/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4685ca8ec56b74b3bfb4a83e24a1bc38c4366fa3419b0b32362fa719e1aee8b1
|
|
4
|
+
data.tar.gz: 86575631ac666a0edc77a5bb4eb693cf6754856fec26d92f6371351d2843349e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6437621c3f7b089604ef61588576b144a9ef996e07808e6ffacff8073900ecf007723c4e3221283ffdebbce7e841b208c56948d30ec950aabdcc61b06a226405
|
|
7
|
+
data.tar.gz: a57b0e0d4c36a030387420d07d5fd914f70777c9a61cac62e914bc24c1d7f1d060158cbde1df5d789de6fca748d927d7d0f9681ae11f7022f1228fe4fffe6156
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.7.0 - 2026-05-05
|
|
6
|
+
|
|
7
|
+
- Changed API-key-backed sessions to expose `tokenFingerprint` instead of storing the raw API key in `session["token"]`.
|
|
8
|
+
- Hardened API key listing and expired-key cleanup behavior.
|
|
9
|
+
- Improved API key metadata handling and added regression coverage for session fingerprint behavior.
|
|
10
|
+
|
|
3
11
|
## 0.2.1 - 2026-04-30
|
|
4
12
|
|
|
5
13
|
- Fixed API key metadata normalization so symbol and string metadata keys preserve nested metadata payloads.
|
data/README.md
CHANGED
|
@@ -44,6 +44,31 @@ export const authClient = createAuthClient({
|
|
|
44
44
|
|
|
45
45
|
Ruby does not expose a separate `apiKeyClient()` equivalent; the public Ruby surface is the server plugin and route contract.
|
|
46
46
|
|
|
47
|
+
| Method | Path | Ruby API method |
|
|
48
|
+
| --- | --- | --- |
|
|
49
|
+
| `POST` | `/api-key/create` | `auth.api.create_api_key` |
|
|
50
|
+
| `POST` | `/api-key/verify` | `auth.api.verify_api_key` |
|
|
51
|
+
| `GET` | `/api-key/get` | `auth.api.get_api_key` |
|
|
52
|
+
| `GET` | `/api-key/list` | `auth.api.list_api_keys` |
|
|
53
|
+
| `POST` | `/api-key/update` | `auth.api.update_api_key` |
|
|
54
|
+
| `POST` | `/api-key/delete` | `auth.api.delete_api_key` |
|
|
55
|
+
| `POST` | `/api-key/delete-all-expired-api-keys` | `auth.api.delete_all_expired_api_keys` |
|
|
56
|
+
|
|
57
|
+
## Operational notes
|
|
58
|
+
|
|
59
|
+
Expired API key cleanup runs against the database when `storage` is `"database"`
|
|
60
|
+
or `fallback_to_database` is true. Secondary-storage-only deployments should
|
|
61
|
+
align Redis or KV TTLs with API key expiration because database cleanup does not
|
|
62
|
+
purge secondary-only keys.
|
|
63
|
+
|
|
64
|
+
The scheduled expired-key cleanup throttle is per Ruby process. It is not
|
|
65
|
+
coordinated across web workers, hosts, or background job runners.
|
|
66
|
+
|
|
67
|
+
When `defer_updates` is combined with `advanced.background_tasks.handler`, usage
|
|
68
|
+
updates such as request counts, remaining limits, refill state, and scheduled
|
|
69
|
+
cleanup can be reordered under concurrency. Use database transactions or
|
|
70
|
+
deployment-level coordination if strict counters are required.
|
|
71
|
+
|
|
47
72
|
## Configuration
|
|
48
73
|
|
|
49
74
|
```ruby
|
|
@@ -165,6 +190,9 @@ Endpoint requests/responses always use the upstream `camelCase` field names, so
|
|
|
165
190
|
TypeScript clients targeting `@better-auth/api-key/client` interoperate without
|
|
166
191
|
configuration changes.
|
|
167
192
|
|
|
193
|
+
The cleanup route is also exposed through `auth.api.delete_all_expired_api_keys`
|
|
194
|
+
and returns `{success: true, error: nil}` on success.
|
|
195
|
+
|
|
168
196
|
## Organization-owned API keys
|
|
169
197
|
|
|
170
198
|
Setting `references: "organization"` on a configuration delegates ownership to
|
|
@@ -61,10 +61,14 @@ module BetterAuth
|
|
|
61
61
|
if config[:storage] == "secondary-storage"
|
|
62
62
|
begin
|
|
63
63
|
storage_instance = storage(config, ctx.context)
|
|
64
|
-
|
|
64
|
+
raw_ids = storage_instance&.get(storage_key_by_reference(reference_id)) || storage_instance&.get("api-key:user:#{reference_id}")
|
|
65
|
+
ids = parse_id_list!(raw_ids)
|
|
65
66
|
records = ids.filter_map { |id| find_by_id(ctx, id, config) }
|
|
66
67
|
return records unless records.empty? && config[:fallback_to_database]
|
|
67
|
-
rescue JSON::ParserError, NoMethodError
|
|
68
|
+
rescue JSON::ParserError, NoMethodError => error
|
|
69
|
+
if ctx.context.respond_to?(:logger) && ctx.context.logger.respond_to?(:warn)
|
|
70
|
+
ctx.context.logger.warn("[API KEY PLUGIN] Corrupt api-key reference index for #{reference_id.inspect}: #{error.class}: #{error.message}")
|
|
71
|
+
end
|
|
68
72
|
return [] unless config[:fallback_to_database]
|
|
69
73
|
end
|
|
70
74
|
end
|
|
@@ -88,7 +92,7 @@ module BetterAuth
|
|
|
88
92
|
|
|
89
93
|
if defer && config[:defer_updates] && BetterAuth::Plugins.api_key_background_tasks?(ctx)
|
|
90
94
|
scheduled = record.merge(update.transform_keys { |key| BetterAuth::Schema.storage_key(key) })
|
|
91
|
-
|
|
95
|
+
BetterAuth::APIKey::Utils.run_background_task(ctx, "Deferred API key update", performer)
|
|
92
96
|
scheduled
|
|
93
97
|
else
|
|
94
98
|
performer.call
|
|
@@ -103,7 +107,7 @@ module BetterAuth
|
|
|
103
107
|
def schedule_record_delete(ctx, record, config)
|
|
104
108
|
task = -> { delete_record(ctx, record, config) }
|
|
105
109
|
if config[:defer_updates] && BetterAuth::APIKey::Utils.background_tasks?(ctx)
|
|
106
|
-
|
|
110
|
+
BetterAuth::APIKey::Utils.run_background_task(ctx, "Deferred API key delete", task)
|
|
107
111
|
else
|
|
108
112
|
task.call
|
|
109
113
|
end
|
|
@@ -124,6 +128,11 @@ module BetterAuth
|
|
|
124
128
|
updated
|
|
125
129
|
end
|
|
126
130
|
|
|
131
|
+
def legacy_metadata_migration_needed?(record)
|
|
132
|
+
parsed = BetterAuth::APIKey::Utils.decode_json(record["metadata"])
|
|
133
|
+
parsed.is_a?(Hash) && BetterAuth::APIKey::Utils.encode_json(parsed) != record["metadata"]
|
|
134
|
+
end
|
|
135
|
+
|
|
127
136
|
def storage(config, context = nil)
|
|
128
137
|
config[:custom_storage] || context&.options&.secondary_storage
|
|
129
138
|
end
|
|
@@ -198,13 +207,21 @@ module BetterAuth
|
|
|
198
207
|
|
|
199
208
|
def safe_parse_id_list(raw)
|
|
200
209
|
return [] if raw.nil?
|
|
210
|
+
return raw.dup if raw.is_a?(Array)
|
|
201
211
|
|
|
202
|
-
|
|
203
|
-
parsed.is_a?(Array) ? parsed : []
|
|
212
|
+
parse_id_list!(raw)
|
|
204
213
|
rescue JSON::ParserError
|
|
205
214
|
[]
|
|
206
215
|
end
|
|
207
216
|
|
|
217
|
+
def parse_id_list!(raw)
|
|
218
|
+
return [] if raw.nil?
|
|
219
|
+
return raw.dup if raw.is_a?(Array)
|
|
220
|
+
|
|
221
|
+
parsed = JSON.parse(raw.to_s)
|
|
222
|
+
parsed.is_a?(Array) ? parsed : []
|
|
223
|
+
end
|
|
224
|
+
|
|
208
225
|
def batch(storage_instance, &block)
|
|
209
226
|
if storage_instance.respond_to?(:batch)
|
|
210
227
|
storage_instance.batch(&block)
|
|
@@ -217,17 +234,25 @@ module BetterAuth
|
|
|
217
234
|
storage_instance = storage(config, ctx.context)
|
|
218
235
|
return unless storage_instance
|
|
219
236
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
237
|
+
batch(storage_instance) do
|
|
238
|
+
ids = []
|
|
239
|
+
records.each do |record|
|
|
240
|
+
serialized = JSON.generate(storage_record(record))
|
|
241
|
+
expires_at = BetterAuth::APIKey::Utils.normalize_time(record["expiresAt"])
|
|
242
|
+
ttl = expires_at ? [(expires_at - Time.now).to_i, 0].max : nil
|
|
243
|
+
storage_instance.set(storage_key_by_hash(record["key"]), serialized, ttl)
|
|
244
|
+
storage_instance.set(storage_key_by_id(record["id"]), serialized, ttl)
|
|
245
|
+
ids << record["id"]
|
|
246
|
+
end
|
|
247
|
+
reference_key = storage_key_by_reference(reference_id)
|
|
248
|
+
ids.empty? ? storage_instance.delete(reference_key) : storage_instance.set(reference_key, JSON.generate(ids))
|
|
228
249
|
end
|
|
229
|
-
|
|
230
|
-
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def batch_migrate_legacy_metadata(ctx, records, config)
|
|
253
|
+
return records unless config[:storage] == "database" || config[:fallback_to_database]
|
|
254
|
+
|
|
255
|
+
records.map { |record| migrate_legacy_metadata(ctx, record, config) }
|
|
231
256
|
end
|
|
232
257
|
|
|
233
258
|
def storage_record(record)
|
|
@@ -14,7 +14,7 @@ module BetterAuth
|
|
|
14
14
|
ctx.json({success: true, error: nil})
|
|
15
15
|
rescue => error
|
|
16
16
|
ctx.context.logger.error("[API KEY PLUGIN] Failed to delete expired API keys: #{error.message}") if ctx.context.logger.respond_to?(:error)
|
|
17
|
-
ctx.json({success: false, error: error})
|
|
17
|
+
ctx.json({success: false, error: {message: error.message.to_s, name: error.class.name}})
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -51,18 +51,20 @@ module BetterAuth
|
|
|
51
51
|
@last_expired_check = now
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
now = Time.now
|
|
55
|
+
context.adapter.delete_many(
|
|
56
|
+
model: BetterAuth::Plugins::API_KEY_TABLE_NAME,
|
|
57
|
+
where: [
|
|
58
|
+
{field: "expiresAt", value: now, operator: "lt"},
|
|
59
|
+
{field: "expiresAt", value: nil, operator: "ne"}
|
|
60
|
+
]
|
|
61
|
+
)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
def schedule_cleanup(ctx, config)
|
|
63
65
|
task = -> { delete_expired(ctx.context, config) }
|
|
64
66
|
if config[:defer_updates] && BetterAuth::APIKey::Utils.background_tasks?(ctx)
|
|
65
|
-
|
|
67
|
+
BetterAuth::APIKey::Utils.run_background_task(ctx, "Deferred API key cleanup", task)
|
|
66
68
|
else
|
|
67
69
|
task.call
|
|
68
70
|
end
|
|
@@ -31,9 +31,21 @@ module BetterAuth
|
|
|
31
31
|
records = records.drop(offset) if offset
|
|
32
32
|
records = records.first(limit) if limit
|
|
33
33
|
records.each { |record| BetterAuth::Plugins.api_key_delete_expired(ctx.context, BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))) }
|
|
34
|
+
migration_records = records.select { |record| BetterAuth::APIKey::Adapter.legacy_metadata_migration_needed?(record) }
|
|
35
|
+
if migration_records.any?
|
|
36
|
+
BetterAuth::APIKey::Utils.run_background_task(
|
|
37
|
+
ctx,
|
|
38
|
+
"API key metadata migration",
|
|
39
|
+
lambda do
|
|
40
|
+
migration_records.each do |record|
|
|
41
|
+
record_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))
|
|
42
|
+
BetterAuth::APIKey::Adapter.batch_migrate_legacy_metadata(ctx, [record], record_config)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
)
|
|
46
|
+
end
|
|
34
47
|
api_keys = records.map do |record|
|
|
35
|
-
|
|
36
|
-
BetterAuth::Plugins.api_key_public(BetterAuth::Plugins.api_key_migrate_legacy_metadata(ctx, record, record_config), include_key_field: false)
|
|
48
|
+
BetterAuth::Plugins.api_key_public(record, include_key_field: false)
|
|
37
49
|
end
|
|
38
50
|
ctx.json({apiKeys: api_keys, total: total, limit: limit, offset: offset})
|
|
39
51
|
end
|
|
@@ -46,7 +46,7 @@ module BetterAuth
|
|
|
46
46
|
user: user,
|
|
47
47
|
session: {
|
|
48
48
|
"id" => record["id"],
|
|
49
|
-
"
|
|
49
|
+
"tokenFingerprint" => BetterAuth::Plugins.default_api_key_hasher(key),
|
|
50
50
|
"userId" => reference_id,
|
|
51
51
|
"userAgent" => ctx.headers["user-agent"],
|
|
52
52
|
"ipAddress" => BetterAuth::RequestIP.client_ip(ctx.request || ctx.headers, ctx.context.options),
|
|
@@ -85,6 +85,16 @@ module BetterAuth
|
|
|
85
85
|
ctx.context.options.advanced.dig(:background_tasks, :handler).respond_to?(:call)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
+
def run_background_task(ctx, label, task)
|
|
89
|
+
wrapped = lambda do
|
|
90
|
+
task.call
|
|
91
|
+
rescue => error
|
|
92
|
+
logger = ctx.context.logger if ctx.context.respond_to?(:logger)
|
|
93
|
+
logger.error("[API KEY PLUGIN] #{label} failed: #{error.message}") if logger.respond_to?(:error)
|
|
94
|
+
end
|
|
95
|
+
ctx.context.run_in_background(wrapped)
|
|
96
|
+
end
|
|
97
|
+
|
|
88
98
|
def auth_required?(ctx)
|
|
89
99
|
!!(ctx.request || (ctx.headers && !ctx.headers.empty?))
|
|
90
100
|
end
|