better_auth-api-key 0.8.0 → 0.11.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 +35 -0
- data/LICENSE.md +20 -0
- data/README.md +19 -5
- data/lib/better_auth/api_key/adapter.rb +36 -6
- data/lib/better_auth/api_key/rate_limit.rb +30 -0
- data/lib/better_auth/api_key/routes/create_api_key.rb +9 -3
- data/lib/better_auth/api_key/routes/delete_all_expired_api_keys.rb +3 -3
- data/lib/better_auth/api_key/routes/delete_api_key.rb +2 -2
- data/lib/better_auth/api_key/routes/get_api_key.rb +2 -2
- data/lib/better_auth/api_key/routes/index.rb +228 -1
- data/lib/better_auth/api_key/routes/list_api_keys.rb +78 -15
- data/lib/better_auth/api_key/routes/update_api_key.rb +14 -3
- data/lib/better_auth/api_key/routes/verify_api_key.rb +39 -7
- data/lib/better_auth/api_key/schema.rb +1 -0
- data/lib/better_auth/api_key/validation.rb +217 -19
- data/lib/better_auth/api_key/version.rb +1 -1
- data/lib/better_auth/plugins/api_key.rb +2 -0
- metadata +119 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e054d9e429e2c199d594467dcc924a9ef44c22c0be63f96e240f47308e4444ef
|
|
4
|
+
data.tar.gz: d34741924ce2951654646c1127f0d3caa09d16cd765f58647220ff6c71221e57
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b8220f1cc0645105e0666c7d8184d43c72bb4e050094f4aa6cfb411b61f0622e2b0c2c4c21d50b7c586b82a80d96b7855c36b6e93b0dbbb94db2d87fd0456f2d
|
|
7
|
+
data.tar.gz: 5c1d00a8dc01502bc6709d0419195548eec8ad816846f9be05f58a3fa8c477305df53ba3722344f268b223b19b7e4ededd4bd138d1c1eca36a060c42872f6e02
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
- Hardened API-key verification and counter updates where authoritative adapter
|
|
6
|
+
operations are available, with regression coverage for stale-write cases.
|
|
7
|
+
- Added a checked upstream test inventory for this plugin's pinned reference
|
|
8
|
+
files.
|
|
9
|
+
|
|
10
|
+
## [0.11.0](https://github.com/salasebas/better-auth-rb/compare/better_auth-api-key-v0.10.0...better_auth-api-key/v0.11.0) (2026-07-23)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### ⚠ BREAKING CHANGES
|
|
14
|
+
|
|
15
|
+
* **release:** OpenAuth alias gems, the openauth executable, and the better_auth_rails alias gem and require path are removed.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* **schema:** align migration plugin schema parity ([c0261dc](https://github.com/salasebas/better-auth-rb/commit/c0261dc1e5fd649557cd8a57b92c0609620c3767))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* **api-key:** load access plugin before permission checks ([985e56c](https://github.com/salasebas/better-auth-rb/commit/985e56cdc29298aac995ff7d0f04a7f1bf13a7cd))
|
|
25
|
+
* **api-key:** make verification counters authoritative ([6550ae7](https://github.com/salasebas/better-auth-rb/commit/6550ae753f104f771e313360f56e6fdcff82d005))
|
|
26
|
+
* **ci:** restore package .ruby-version files for setup-ruby ([c18d220](https://github.com/salasebas/better-auth-rb/commit/c18d220065f3cb7cd2504bef7aa407e2c0c027eb))
|
|
27
|
+
* load external plugin gems without stub recursion ([4926bae](https://github.com/salasebas/better-auth-rb/commit/4926bae7520c17438de25e99cb2155839c177493))
|
|
28
|
+
* **saml:** fail closed without response parser ([#35](https://github.com/salasebas/better-auth-rb/issues/35)) ([0f9a7a4](https://github.com/salasebas/better-auth-rb/commit/0f9a7a4fb841153951b5f00ce42f01eee41b3112))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Miscellaneous Chores
|
|
32
|
+
|
|
33
|
+
* **release:** retire aliases and refresh parity tooling ([e099464](https://github.com/salasebas/better-auth-rb/commit/e0994643694267508a3c4d9be020bb1fd0e2e5a3))
|
|
34
|
+
|
|
35
|
+
## 0.10.0 - 2026-05-21
|
|
36
|
+
|
|
37
|
+
- Improved adapter coverage and Redis-backed storage behavior for API key flows.
|
|
38
|
+
- Tightened API key listing behavior so responses stay consistent across supported adapters.
|
|
39
|
+
|
|
5
40
|
## 0.7.0 - 2026-05-05
|
|
6
41
|
|
|
7
42
|
- Changed API-key-backed sessions to expose `tokenFingerprint` instead of storing the raw API key in `session["token"]`.
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
Copyright (c) 2024 - present
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
5
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
6
|
+
the Software without restriction, including without limitation the rights to
|
|
7
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
8
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
9
|
+
subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
16
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
18
|
+
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
19
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
20
|
+
DEALINGS IN THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -64,10 +64,10 @@ purge secondary-only keys.
|
|
|
64
64
|
The scheduled expired-key cleanup throttle is per Ruby process. It is not
|
|
65
65
|
coordinated across web workers, hosts, or background job runners.
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
`defer_updates` can defer explicit API-key updates and cleanup when a background
|
|
68
|
+
task handler is configured. Verification counter claims remain synchronous;
|
|
69
|
+
database-backed claims use guarded atomic operations, while pure secondary-only
|
|
70
|
+
claims retain the best-effort limitation described below.
|
|
71
71
|
|
|
72
72
|
## Configuration
|
|
73
73
|
|
|
@@ -119,7 +119,21 @@ BetterAuth::Plugins.api_key([
|
|
|
119
119
|
|
|
120
120
|
Organization-owned keys require `BetterAuth::Plugins.organization` and use organization permissions for `apiKey` actions: `create`, `read`, `update`, and `delete`.
|
|
121
121
|
|
|
122
|
-
Secondary-storage mode uses upstream storage keys such as `api-key:<hash>`, `api-key:by-id:<id>`, and `api-key:by-ref:<referenceId>`.
|
|
122
|
+
Secondary-storage mode uses upstream storage keys such as `api-key:<hash>`, `api-key:by-id:<id>`, and `api-key:by-ref:<referenceId>`. With `fallback_to_database: true`, the database is authoritative: verification re-reads the row after a cache hit, and a missing authoritative row invalidates the cache and can never authorize. A cache entry may remain stale, or be briefly re-created from a row, during the unavoidable cross-store window after a database update and before its cache write; that does not weaken verification because authorization always requires the authoritative row. Generic custom storage gets an in-process lock for reference-list updates only; RedisStorage uses atomic cross-process JSON-list scripts.
|
|
123
|
+
|
|
124
|
+
Verification rate-limit failures return HTTP `429` with error code
|
|
125
|
+
`RATE_LIMITED` and `details.tryAgainIn` (authentication failures remain `401`).
|
|
126
|
+
An exhausted non-refillable key remains an inert authoritative row and returns
|
|
127
|
+
`USAGE_EXCEEDED`; Ruby deliberately avoids eager deletion during verification
|
|
128
|
+
because deletion cannot be made cross-process atomic with the winning counter
|
|
129
|
+
update. Cleanup routes/jobs can remove such rows separately.
|
|
130
|
+
Database-backed keys, including `secondary-storage` with
|
|
131
|
+
`fallback_to_database: true`, consume remaining and rate-limit counters with
|
|
132
|
+
guarded atomic updates; concurrent requests cannot drive `remaining` below zero
|
|
133
|
+
or exceed the configured window. Pure secondary-storage deployments do not have
|
|
134
|
+
a guarded JSON-record counter primitive and are therefore best-effort under
|
|
135
|
+
cross-process concurrency. Use `fallback_to_database` when strict enforcement
|
|
136
|
+
is required.
|
|
123
137
|
|
|
124
138
|
## Storage layout
|
|
125
139
|
|
|
@@ -10,6 +10,8 @@ module BetterAuth
|
|
|
10
10
|
HASH_STORAGE_PREFIX = "api-key:"
|
|
11
11
|
ID_STORAGE_PREFIX = "api-key:by-id:"
|
|
12
12
|
REFERENCE_STORAGE_PREFIX = "api-key:by-ref:"
|
|
13
|
+
REFERENCE_LOCKS_GUARD = Mutex.new
|
|
14
|
+
REFERENCE_LOCK_STRIPE_COUNT = 256
|
|
13
15
|
|
|
14
16
|
module_function
|
|
15
17
|
|
|
@@ -84,8 +86,10 @@ module BetterAuth
|
|
|
84
86
|
updated = nil
|
|
85
87
|
if config[:storage] == "database" || config[:fallback_to_database]
|
|
86
88
|
updated = ctx.context.adapter.update(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}], update: update)
|
|
89
|
+
return nil unless updated
|
|
90
|
+
else
|
|
91
|
+
updated = record.merge(update.transform_keys { |key| BetterAuth::Schema.storage_key(key) })
|
|
87
92
|
end
|
|
88
|
-
updated ||= record.merge(update.transform_keys { |key| BetterAuth::Schema.storage_key(key) })
|
|
89
93
|
set(ctx, updated, config) if config[:storage] == "secondary-storage"
|
|
90
94
|
updated
|
|
91
95
|
end
|
|
@@ -195,14 +199,40 @@ module BetterAuth
|
|
|
195
199
|
end
|
|
196
200
|
|
|
197
201
|
def ref_list_add(storage_instance, reference_key, id)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
202
|
+
if storage_instance.respond_to?(:json_list_add)
|
|
203
|
+
storage_instance.json_list_add(reference_key, id)
|
|
204
|
+
return
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
with_reference_lock(reference_key) do
|
|
208
|
+
ids = safe_parse_id_list(storage_instance.get(reference_key))
|
|
209
|
+
ids << id unless ids.include?(id)
|
|
210
|
+
storage_instance.set(reference_key, JSON.generate(ids))
|
|
211
|
+
end
|
|
201
212
|
end
|
|
202
213
|
|
|
203
214
|
def ref_list_remove(storage_instance, reference_key, id)
|
|
204
|
-
|
|
205
|
-
|
|
215
|
+
if storage_instance.respond_to?(:json_list_remove)
|
|
216
|
+
storage_instance.json_list_remove(reference_key, id)
|
|
217
|
+
return
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
with_reference_lock(reference_key) do
|
|
221
|
+
ids = safe_parse_id_list(storage_instance.get(reference_key)).reject { |existing| existing == id }
|
|
222
|
+
ids.empty? ? storage_instance.delete(reference_key) : storage_instance.set(reference_key, JSON.generate(ids))
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Custom secondary-storage implementations expose only get/set/delete,
|
|
227
|
+
# so serialize reference-list read/modify/write operations within this
|
|
228
|
+
# Ruby process. This lock is deliberately not treated as distributed
|
|
229
|
+
# safety; RedisStorage uses its atomic JSON-list scripts above.
|
|
230
|
+
def with_reference_lock(reference_key)
|
|
231
|
+
lock = REFERENCE_LOCKS_GUARD.synchronize do
|
|
232
|
+
@reference_lock_stripes ||= Array.new(REFERENCE_LOCK_STRIPE_COUNT) { Mutex.new }
|
|
233
|
+
@reference_lock_stripes[reference_key.hash % REFERENCE_LOCK_STRIPE_COUNT]
|
|
234
|
+
end
|
|
235
|
+
lock.synchronize { yield }
|
|
206
236
|
end
|
|
207
237
|
|
|
208
238
|
def safe_parse_id_list(raw)
|
|
@@ -5,6 +5,36 @@ module BetterAuth
|
|
|
5
5
|
module RateLimit
|
|
6
6
|
module_function
|
|
7
7
|
|
|
8
|
+
# Pure decision function used by the verifier before applying a guarded
|
|
9
|
+
# counter mutation. Keeping the decision separate from the write avoids
|
|
10
|
+
# persisting a stale snapshot when another verifier wins the race.
|
|
11
|
+
def evaluate(record, config, now = Time.now)
|
|
12
|
+
return {type: :skip, last_request: now} if config[:rate_limit][:enabled] == false
|
|
13
|
+
return {type: :skip, last_request: now} if record["rateLimitEnabled"] == false
|
|
14
|
+
|
|
15
|
+
window = record["rateLimitTimeWindow"]
|
|
16
|
+
max = record["rateLimitMax"]
|
|
17
|
+
return {type: :skip, last_request: nil} if window.nil? || max.nil?
|
|
18
|
+
|
|
19
|
+
last = Utils.normalize_time(record["lastRequest"])
|
|
20
|
+
return {type: :start, now: now} unless last
|
|
21
|
+
|
|
22
|
+
elapsed_ms = (now - last) * 1000
|
|
23
|
+
if elapsed_ms > window.to_i
|
|
24
|
+
return {type: :reset, now: now, window_start: now - (window.to_i / 1000.0)}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if record["requestCount"].to_i >= max.to_i
|
|
28
|
+
return {
|
|
29
|
+
type: :deny,
|
|
30
|
+
try_again_in: [(window.to_i - elapsed_ms).ceil, 0].max,
|
|
31
|
+
message: BetterAuth::Plugins::API_KEY_ERROR_CODES["RATE_LIMIT_EXCEEDED"]
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
{type: :increment, now: now, max: max.to_i, window_start: now - (window.to_i / 1000.0)}
|
|
36
|
+
end
|
|
37
|
+
|
|
8
38
|
def try_again_in(record, config, now)
|
|
9
39
|
return nil if config[:rate_limit][:enabled] == false || record["rateLimitEnabled"] == false
|
|
10
40
|
|
|
@@ -4,15 +4,21 @@ module BetterAuth
|
|
|
4
4
|
module APIKey
|
|
5
5
|
module Routes
|
|
6
6
|
module CreateAPIKey
|
|
7
|
-
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/create-api-key.ts"
|
|
7
|
+
UPSTREAM_SOURCE = "reference/upstream-src/1.6.9/repository/packages/api-key/src/routes/create-api-key.ts"
|
|
8
8
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
def endpoint(config)
|
|
12
|
-
BetterAuth::Endpoint.new(path: "/api-key/create", method: "POST") do |ctx|
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/create", method: "POST", metadata: Routes.openapi_for(:create_api_key)) do |ctx|
|
|
13
13
|
body = BetterAuth::Plugins.api_key_normalize_body(ctx.body)
|
|
14
14
|
resolved_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, body[:config_id])
|
|
15
|
-
|
|
15
|
+
# API-key ownership and authorization are sensitive operations. A
|
|
16
|
+
# cookie-cache snapshot may outlive a revoked session, so force an
|
|
17
|
+
# authoritative session read whenever stateful storage is active.
|
|
18
|
+
session = BetterAuth::Routes.current_session(ctx, allow_nil: true, sensitive: true)
|
|
19
|
+
if !session && BetterAuth::Plugins.api_key_auth_required?(ctx)
|
|
20
|
+
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"])
|
|
21
|
+
end
|
|
16
22
|
reference_id = BetterAuth::Plugins.api_key_create_reference_id!(ctx, body, session, resolved_config)
|
|
17
23
|
|
|
18
24
|
BetterAuth::Plugins.api_key_validate_create_update!(body, resolved_config, create: true, client: !ctx.headers.empty?)
|
|
@@ -4,13 +4,13 @@ module BetterAuth
|
|
|
4
4
|
module APIKey
|
|
5
5
|
module Routes
|
|
6
6
|
module DeleteAllExpiredAPIKeys
|
|
7
|
-
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/delete-all-expired-api-keys.ts"
|
|
7
|
+
UPSTREAM_SOURCE = "reference/upstream-src/1.6.9/repository/packages/api-key/src/routes/delete-all-expired-api-keys.ts"
|
|
8
8
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
def endpoint(config)
|
|
12
|
-
BetterAuth::Endpoint.new(path: "/api-key/delete-all-expired-api-keys", method: "POST") do |ctx|
|
|
13
|
-
BetterAuth::
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/delete-all-expired-api-keys", method: "POST", metadata: Routes.openapi_for(:delete_all_expired_api_keys)) do |ctx|
|
|
13
|
+
BetterAuth::APIKey::Routes.delete_expired(ctx.context, config, bypass_last_check: true, raise_on_error: true)
|
|
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)
|
|
@@ -4,12 +4,12 @@ module BetterAuth
|
|
|
4
4
|
module APIKey
|
|
5
5
|
module Routes
|
|
6
6
|
module DeleteAPIKey
|
|
7
|
-
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/delete-api-key.ts"
|
|
7
|
+
UPSTREAM_SOURCE = "reference/upstream-src/1.6.9/repository/packages/api-key/src/routes/delete-api-key.ts"
|
|
8
8
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
def endpoint(config)
|
|
12
|
-
BetterAuth::Endpoint.new(path: "/api-key/delete", method: "POST") do |ctx|
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/delete", method: "POST", metadata: Routes.openapi_for(:delete_api_key)) do |ctx|
|
|
13
13
|
session = BetterAuth::Routes.current_session(ctx)
|
|
14
14
|
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["USER_BANNED"]) if session[:user]["banned"] == true
|
|
15
15
|
|
|
@@ -4,12 +4,12 @@ module BetterAuth
|
|
|
4
4
|
module APIKey
|
|
5
5
|
module Routes
|
|
6
6
|
module GetAPIKey
|
|
7
|
-
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/get-api-key.ts"
|
|
7
|
+
UPSTREAM_SOURCE = "reference/upstream-src/1.6.9/repository/packages/api-key/src/routes/get-api-key.ts"
|
|
8
8
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
def endpoint(config)
|
|
12
|
-
BetterAuth::Endpoint.new(path: "/api-key/get", method: "GET") do |ctx|
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/get", method: "GET", metadata: Routes.openapi_for(:get_api_key)) do |ctx|
|
|
13
13
|
session = BetterAuth::Routes.current_session(ctx)
|
|
14
14
|
query = BetterAuth::Plugins.normalize_hash(ctx.query)
|
|
15
15
|
resolved_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, query[:config_id])
|
|
@@ -42,7 +42,7 @@ module BetterAuth
|
|
|
42
42
|
|
|
43
43
|
@last_expired_check = nil
|
|
44
44
|
|
|
45
|
-
def delete_expired(context, config, bypass_last_check: false)
|
|
45
|
+
def delete_expired(context, config, bypass_last_check: false, raise_on_error: false)
|
|
46
46
|
return unless config[:storage] == "database" || config[:fallback_to_database]
|
|
47
47
|
unless bypass_last_check
|
|
48
48
|
now = Time.now
|
|
@@ -59,6 +59,9 @@ module BetterAuth
|
|
|
59
59
|
{field: "expiresAt", value: nil, operator: "ne"}
|
|
60
60
|
]
|
|
61
61
|
)
|
|
62
|
+
rescue => error
|
|
63
|
+
context.logger.error("[API KEY PLUGIN] Failed to delete expired API keys: #{error.message}") if context.respond_to?(:logger) && context.logger.respond_to?(:error)
|
|
64
|
+
raise if raise_on_error
|
|
62
65
|
end
|
|
63
66
|
|
|
64
67
|
def schedule_cleanup(ctx, config)
|
|
@@ -69,6 +72,230 @@ module BetterAuth
|
|
|
69
72
|
task.call
|
|
70
73
|
end
|
|
71
74
|
end
|
|
75
|
+
|
|
76
|
+
def openapi_for(route)
|
|
77
|
+
{
|
|
78
|
+
create_api_key: create_api_key_openapi,
|
|
79
|
+
verify_api_key: verify_api_key_openapi,
|
|
80
|
+
get_api_key: get_api_key_openapi,
|
|
81
|
+
update_api_key: update_api_key_openapi,
|
|
82
|
+
delete_api_key: delete_api_key_openapi,
|
|
83
|
+
list_api_keys: list_api_keys_openapi,
|
|
84
|
+
delete_all_expired_api_keys: delete_all_expired_api_keys_openapi
|
|
85
|
+
}.fetch(route)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def create_api_key_openapi
|
|
89
|
+
{
|
|
90
|
+
openapi: {
|
|
91
|
+
description: "Create a new API key for a user",
|
|
92
|
+
requestBody: BetterAuth::OpenAPI.json_request_body(api_key_create_body_schema, required: true),
|
|
93
|
+
responses: {
|
|
94
|
+
"200" => BetterAuth::OpenAPI.json_response("API key created successfully", api_key_record_schema(include_secret: true))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def verify_api_key_openapi
|
|
101
|
+
response_schema = BetterAuth::OpenAPI.object_schema(
|
|
102
|
+
{
|
|
103
|
+
valid: {type: "boolean"},
|
|
104
|
+
error: {type: ["object", "null"], additionalProperties: true},
|
|
105
|
+
key: api_key_record_schema(include_secret: false).merge(type: ["object", "null"])
|
|
106
|
+
},
|
|
107
|
+
required: ["valid", "error", "key"]
|
|
108
|
+
)
|
|
109
|
+
{
|
|
110
|
+
openapi: {
|
|
111
|
+
description: "Verify and rate-limit an API key",
|
|
112
|
+
requestBody: BetterAuth::OpenAPI.json_request_body(
|
|
113
|
+
BetterAuth::OpenAPI.object_schema(
|
|
114
|
+
{
|
|
115
|
+
key: {type: "string", description: "The API key to verify"},
|
|
116
|
+
configId: {type: "string", description: "Configuration ID to use for the lookup"},
|
|
117
|
+
permissions: api_key_permissions_schema.merge(description: "Permissions required for the request")
|
|
118
|
+
},
|
|
119
|
+
required: ["key"]
|
|
120
|
+
)
|
|
121
|
+
),
|
|
122
|
+
responses: {
|
|
123
|
+
"200" => BetterAuth::OpenAPI.json_response(
|
|
124
|
+
"API key verification result",
|
|
125
|
+
response_schema
|
|
126
|
+
),
|
|
127
|
+
"401" => BetterAuth::OpenAPI.json_response("API key authentication failed", response_schema),
|
|
128
|
+
"429" => BetterAuth::OpenAPI.json_response("API key usage or rate limit exceeded", response_schema)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def get_api_key_openapi
|
|
135
|
+
{
|
|
136
|
+
openapi: {
|
|
137
|
+
description: "Get an API key by ID",
|
|
138
|
+
parameters: [
|
|
139
|
+
BetterAuth::OpenAPI.query_parameter("id", required: true, description: "The API key ID"),
|
|
140
|
+
BetterAuth::OpenAPI.query_parameter("configId", description: "Configuration ID to use for the lookup")
|
|
141
|
+
],
|
|
142
|
+
responses: {
|
|
143
|
+
"200" => BetterAuth::OpenAPI.json_response("API key retrieved successfully", api_key_record_schema(include_secret: false))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def update_api_key_openapi
|
|
150
|
+
{
|
|
151
|
+
openapi: {
|
|
152
|
+
description: "Update an existing API key by ID",
|
|
153
|
+
requestBody: BetterAuth::OpenAPI.json_request_body(api_key_update_body_schema, required: true),
|
|
154
|
+
responses: {
|
|
155
|
+
"200" => BetterAuth::OpenAPI.json_response("API key updated successfully", api_key_record_schema(include_secret: false))
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def delete_api_key_openapi
|
|
162
|
+
{
|
|
163
|
+
openapi: {
|
|
164
|
+
description: "Delete an API key by ID",
|
|
165
|
+
requestBody: BetterAuth::OpenAPI.json_request_body(
|
|
166
|
+
BetterAuth::OpenAPI.object_schema(
|
|
167
|
+
{
|
|
168
|
+
keyId: {type: "string", description: "The API key ID"},
|
|
169
|
+
configId: {type: "string", description: "Configuration ID to use for the lookup"}
|
|
170
|
+
},
|
|
171
|
+
required: ["keyId"]
|
|
172
|
+
)
|
|
173
|
+
),
|
|
174
|
+
responses: {
|
|
175
|
+
"200" => BetterAuth::OpenAPI.json_response("API key deleted successfully", BetterAuth::OpenAPI.success_response_schema)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def list_api_keys_openapi
|
|
182
|
+
{
|
|
183
|
+
openapi: {
|
|
184
|
+
description: "List all API keys for the authenticated user or for a specific organization",
|
|
185
|
+
parameters: [
|
|
186
|
+
BetterAuth::OpenAPI.query_parameter("configId", description: "Filter by configuration ID"),
|
|
187
|
+
BetterAuth::OpenAPI.query_parameter("organizationId", description: "Organization ID to list keys for"),
|
|
188
|
+
BetterAuth::OpenAPI.query_parameter("limit", schema: {type: "number"}, description: "The number of API keys to return"),
|
|
189
|
+
BetterAuth::OpenAPI.query_parameter("offset", schema: {type: "number"}, description: "The offset to start from"),
|
|
190
|
+
BetterAuth::OpenAPI.query_parameter("sortBy", description: "The field to sort by"),
|
|
191
|
+
BetterAuth::OpenAPI.query_parameter("sortDirection", schema: {type: "string", enum: ["asc", "desc"]}, description: "The direction to sort by")
|
|
192
|
+
],
|
|
193
|
+
responses: {
|
|
194
|
+
"200" => BetterAuth::OpenAPI.json_response(
|
|
195
|
+
"API keys retrieved successfully",
|
|
196
|
+
BetterAuth::OpenAPI.object_schema(
|
|
197
|
+
{
|
|
198
|
+
apiKeys: BetterAuth::OpenAPI.array_schema(api_key_record_schema(include_secret: false)),
|
|
199
|
+
total: {type: "number"},
|
|
200
|
+
limit: {type: ["number", "null"]},
|
|
201
|
+
offset: {type: ["number", "null"]}
|
|
202
|
+
},
|
|
203
|
+
required: ["apiKeys", "total"]
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def delete_all_expired_api_keys_openapi
|
|
212
|
+
{
|
|
213
|
+
openapi: {
|
|
214
|
+
description: "Delete all expired API keys",
|
|
215
|
+
requestBody: BetterAuth::OpenAPI.empty_request_body,
|
|
216
|
+
responses: {
|
|
217
|
+
"200" => BetterAuth::OpenAPI.json_response(
|
|
218
|
+
"Expired API key cleanup result",
|
|
219
|
+
BetterAuth::OpenAPI.object_schema(
|
|
220
|
+
{
|
|
221
|
+
success: {type: "boolean"},
|
|
222
|
+
error: {type: ["object", "null"], additionalProperties: true}
|
|
223
|
+
},
|
|
224
|
+
required: ["success", "error"]
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def api_key_create_body_schema
|
|
233
|
+
BetterAuth::OpenAPI.object_schema(
|
|
234
|
+
{
|
|
235
|
+
configId: {type: "string", description: "The configuration ID to use for the API key"},
|
|
236
|
+
name: {type: "string", description: "Name of the API key"},
|
|
237
|
+
expiresIn: {type: ["number", "null"], description: "Expiration time of the API key in seconds"},
|
|
238
|
+
prefix: {type: "string", description: "Prefix of the API key"},
|
|
239
|
+
remaining: {type: ["number", "null"], description: "Remaining number of requests"},
|
|
240
|
+
metadata: {nullable: true, description: "Metadata associated with the API key"},
|
|
241
|
+
refillAmount: {type: "number", description: "Amount to refill the remaining count"},
|
|
242
|
+
refillInterval: {type: "number", description: "Interval to refill the API key in milliseconds"},
|
|
243
|
+
rateLimitTimeWindow: {type: "number", description: "Rate limit time window in milliseconds"},
|
|
244
|
+
rateLimitMax: {type: "number", description: "Maximum requests allowed within a window"},
|
|
245
|
+
rateLimitEnabled: {type: "boolean", description: "Whether the key has rate limiting enabled"},
|
|
246
|
+
permissions: api_key_permissions_schema.merge(description: "Permissions of the API key"),
|
|
247
|
+
userId: {type: "string", description: "User ID that the API key belongs to"},
|
|
248
|
+
organizationId: {type: "string", description: "Organization ID that the API key belongs to"}
|
|
249
|
+
}
|
|
250
|
+
)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def api_key_update_body_schema
|
|
254
|
+
BetterAuth::OpenAPI.object_schema(
|
|
255
|
+
api_key_create_body_schema[:properties].merge(
|
|
256
|
+
keyId: {type: "string", description: "The API key ID"},
|
|
257
|
+
enabled: {type: "boolean", description: "Whether the API key is enabled"}
|
|
258
|
+
).except(:prefix, :organizationId),
|
|
259
|
+
required: ["keyId"]
|
|
260
|
+
)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def api_key_permissions_schema
|
|
264
|
+
{
|
|
265
|
+
type: "object",
|
|
266
|
+
additionalProperties: {
|
|
267
|
+
type: "array",
|
|
268
|
+
items: {type: "string"}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def api_key_record_schema(include_secret:)
|
|
274
|
+
properties = {
|
|
275
|
+
id: {type: "string", description: "Unique identifier of the API key"},
|
|
276
|
+
createdAt: {type: "string", format: "date-time", description: "Creation timestamp"},
|
|
277
|
+
updatedAt: {type: "string", format: "date-time", description: "Last update timestamp"},
|
|
278
|
+
name: {type: ["string", "null"], description: "Name of the API key"},
|
|
279
|
+
start: {type: ["string", "null"], description: "Starting characters of the key"},
|
|
280
|
+
prefix: {type: ["string", "null"], description: "Prefix of the API key"},
|
|
281
|
+
enabled: {type: "boolean", description: "Whether the key is enabled"},
|
|
282
|
+
expiresAt: {type: ["string", "null"], format: "date-time", description: "Expiration timestamp"},
|
|
283
|
+
referenceId: {type: "string", description: "ID of the reference owning the key"},
|
|
284
|
+
lastRefillAt: {type: ["string", "null"], format: "date-time", description: "Last refill timestamp"},
|
|
285
|
+
lastRequest: {type: ["string", "null"], format: "date-time", description: "Last request timestamp"},
|
|
286
|
+
metadata: {type: ["object", "null"], additionalProperties: true, description: "Metadata associated with the key"},
|
|
287
|
+
rateLimitMax: {type: ["number", "null"], description: "Maximum requests in time window"},
|
|
288
|
+
rateLimitTimeWindow: {type: ["number", "null"], description: "Rate limit time window in milliseconds"},
|
|
289
|
+
rateLimitEnabled: {type: "boolean", description: "Whether rate limiting is enabled"},
|
|
290
|
+
remaining: {type: ["number", "null"], description: "Remaining number of requests"},
|
|
291
|
+
refillAmount: {type: ["number", "null"], description: "Amount to refill"},
|
|
292
|
+
refillInterval: {type: ["number", "null"], description: "Refill interval in milliseconds"},
|
|
293
|
+
permissions: api_key_permissions_schema.merge(nullable: true, description: "Permissions of the API key"),
|
|
294
|
+
userId: {type: ["string", "null"], description: "ID of the user owning the key"}
|
|
295
|
+
}
|
|
296
|
+
properties[:key] = {type: "string", description: "The full API key"} if include_secret
|
|
297
|
+
BetterAuth::OpenAPI.object_schema(properties)
|
|
298
|
+
end
|
|
72
299
|
end
|
|
73
300
|
end
|
|
74
301
|
end
|
|
@@ -4,33 +4,40 @@ module BetterAuth
|
|
|
4
4
|
module APIKey
|
|
5
5
|
module Routes
|
|
6
6
|
module ListAPIKeys
|
|
7
|
-
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/list-api-keys.ts"
|
|
7
|
+
UPSTREAM_SOURCE = "reference/upstream-src/1.6.9/repository/packages/api-key/src/routes/list-api-keys.ts"
|
|
8
8
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
def endpoint(config)
|
|
12
|
-
BetterAuth::Endpoint.new(path: "/api-key/list", method: "GET") do |ctx|
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/list", method: "GET", metadata: Routes.openapi_for(:list_api_keys)) do |ctx|
|
|
13
13
|
session = BetterAuth::Routes.current_session(ctx)
|
|
14
14
|
query = BetterAuth::Plugins.normalize_hash(ctx.query)
|
|
15
15
|
BetterAuth::Plugins.api_key_validate_list_query!(query)
|
|
16
|
-
configs = query[:config_id] ? [BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, query[:config_id])] : config.fetch(:configurations, [config])
|
|
16
|
+
configs = query[:config_id] ? [BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, query[:config_id])] : storage_groups(config.fetch(:configurations, [config]))
|
|
17
17
|
reference_id = query[:organization_id] || session[:user]["id"]
|
|
18
18
|
expected_reference = query[:organization_id] ? "organization" : "user"
|
|
19
19
|
BetterAuth::Plugins.api_key_check_org_permission!(ctx, session[:user]["id"], reference_id, "read") if query[:organization_id]
|
|
20
|
-
records = configs.flat_map { |entry| BetterAuth::Plugins.api_key_list_for_reference(ctx, reference_id, entry) }.uniq { |record| record["id"] }
|
|
21
|
-
records = records.select do |record|
|
|
22
|
-
record_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))
|
|
23
|
-
record_config[:references].to_s == expected_reference &&
|
|
24
|
-
BetterAuth::Plugins.api_key_record_reference_id(record) == reference_id &&
|
|
25
|
-
(!query[:config_id] || BetterAuth::Plugins.api_key_config_id_matches?(BetterAuth::Plugins.api_key_record_config_id(record), query[:config_id]))
|
|
26
|
-
end
|
|
27
|
-
total = records.length
|
|
28
|
-
records = BetterAuth::Plugins.api_key_sort_records(records, query[:sort_by], query[:sort_direction])
|
|
29
20
|
offset = query.key?(:offset) ? query[:offset].to_i : nil
|
|
30
21
|
limit = query.key?(:limit) ? query[:limit].to_i : nil
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
pushed = database_paginated_records(ctx, configs.first, reference_id, expected_reference, query, limit, offset)
|
|
23
|
+
if pushed
|
|
24
|
+
records = pushed.fetch(:records)
|
|
25
|
+
total = pushed.fetch(:total)
|
|
26
|
+
else
|
|
27
|
+
records = configs.flat_map { |entry| BetterAuth::Plugins.api_key_list_for_reference(ctx, reference_id, entry) }.uniq { |record| record["id"] }
|
|
28
|
+
records = records.select do |record|
|
|
29
|
+
record_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))
|
|
30
|
+
record_config[:references].to_s == expected_reference &&
|
|
31
|
+
BetterAuth::Plugins.api_key_record_reference_id(record) == reference_id &&
|
|
32
|
+
(!query[:config_id] || BetterAuth::Plugins.api_key_config_id_matches?(BetterAuth::Plugins.api_key_record_config_id(record), query[:config_id]))
|
|
33
|
+
end
|
|
34
|
+
total = records.length
|
|
35
|
+
records = BetterAuth::Plugins.api_key_sort_records(records, query[:sort_by], query[:sort_direction])
|
|
36
|
+
records = records.drop(offset) if offset
|
|
37
|
+
records = records.first(limit) if limit
|
|
38
|
+
end
|
|
39
|
+
cleanup_config = query[:config_id] ? configs.first : config
|
|
40
|
+
BetterAuth::Plugins.api_key_delete_expired(ctx.context, cleanup_config)
|
|
34
41
|
migration_records = records.select { |record| BetterAuth::APIKey::Adapter.legacy_metadata_migration_needed?(record) }
|
|
35
42
|
if migration_records.any?
|
|
36
43
|
BetterAuth::APIKey::Utils.run_background_task(
|
|
@@ -50,6 +57,62 @@ module BetterAuth
|
|
|
50
57
|
ctx.json({apiKeys: api_keys, total: total, limit: limit, offset: offset})
|
|
51
58
|
end
|
|
52
59
|
end
|
|
60
|
+
|
|
61
|
+
def storage_groups(configurations)
|
|
62
|
+
seen = {}
|
|
63
|
+
configurations.each_with_object([]) do |entry, groups|
|
|
64
|
+
key = storage_identifier(entry)
|
|
65
|
+
next if seen[key]
|
|
66
|
+
|
|
67
|
+
seen[key] = true
|
|
68
|
+
groups << entry
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def storage_identifier(config)
|
|
73
|
+
return "database" if config[:storage].to_s == "database"
|
|
74
|
+
return "custom:#{config[:config_id] || "default"}" if config[:custom_storage]
|
|
75
|
+
|
|
76
|
+
config[:fallback_to_database] ? "secondary-storage-with-fallback" : "secondary-storage"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def database_paginated_records(ctx, config, reference_id, expected_reference, query, limit, offset)
|
|
80
|
+
return nil unless query[:config_id] && config
|
|
81
|
+
return nil unless config[:storage].to_s == "database"
|
|
82
|
+
return nil if BetterAuth::APIKey::Routes.default_config_id?(query[:config_id])
|
|
83
|
+
return nil unless config[:references].to_s == expected_reference
|
|
84
|
+
return nil if expected_reference == "user" && legacy_user_id_records?(ctx, reference_id, query[:config_id])
|
|
85
|
+
|
|
86
|
+
where = [
|
|
87
|
+
{field: "referenceId", value: reference_id},
|
|
88
|
+
{field: "configId", value: query[:config_id]}
|
|
89
|
+
]
|
|
90
|
+
sort_by = query[:sort_by] ? {field: query[:sort_by].to_s, direction: (query[:sort_direction] || "asc").to_s} : nil
|
|
91
|
+
{
|
|
92
|
+
records: ctx.context.adapter.find_many(
|
|
93
|
+
model: BetterAuth::Plugins::API_KEY_TABLE_NAME,
|
|
94
|
+
where: where,
|
|
95
|
+
sort_by: sort_by,
|
|
96
|
+
limit: limit,
|
|
97
|
+
offset: offset
|
|
98
|
+
),
|
|
99
|
+
total: ctx.context.adapter.count(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: where)
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def legacy_user_id_records?(ctx, reference_id, config_id)
|
|
104
|
+
where = [
|
|
105
|
+
{field: "userId", value: reference_id},
|
|
106
|
+
{field: "configId", value: config_id}
|
|
107
|
+
]
|
|
108
|
+
ctx.context.adapter.count(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: where).positive?
|
|
109
|
+
rescue KeyError, NoMethodError
|
|
110
|
+
false
|
|
111
|
+
rescue BetterAuth::Error => error
|
|
112
|
+
raise unless error.message.include?("Field userId not found")
|
|
113
|
+
|
|
114
|
+
false
|
|
115
|
+
end
|
|
53
116
|
end
|
|
54
117
|
end
|
|
55
118
|
end
|
|
@@ -4,15 +4,19 @@ module BetterAuth
|
|
|
4
4
|
module APIKey
|
|
5
5
|
module Routes
|
|
6
6
|
module UpdateAPIKey
|
|
7
|
-
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/update-api-key.ts"
|
|
7
|
+
UPSTREAM_SOURCE = "reference/upstream-src/1.6.9/repository/packages/api-key/src/routes/update-api-key.ts"
|
|
8
8
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
def endpoint(config)
|
|
12
|
-
BetterAuth::Endpoint.new(path: "/api-key/update", method: "POST") do |ctx|
|
|
12
|
+
BetterAuth::Endpoint.new(path: "/api-key/update", method: "POST", metadata: Routes.openapi_for(:update_api_key)) do |ctx|
|
|
13
13
|
body = BetterAuth::Plugins.api_key_normalize_body(ctx.body)
|
|
14
14
|
resolved_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, body[:config_id])
|
|
15
|
-
|
|
15
|
+
# Updates must not authorize from a stale cookie-cache session.
|
|
16
|
+
session = BetterAuth::Routes.current_session(ctx, allow_nil: true, sensitive: true)
|
|
17
|
+
if !session && BetterAuth::Plugins.api_key_auth_required?(ctx)
|
|
18
|
+
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"])
|
|
19
|
+
end
|
|
16
20
|
user_id = session&.dig(:user, "id") || body[:user_id]
|
|
17
21
|
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["UNAUTHORIZED_SESSION"]) unless user_id
|
|
18
22
|
if session && body[:user_id] && body[:user_id] != session[:user]["id"]
|
|
@@ -34,6 +38,13 @@ module BetterAuth
|
|
|
34
38
|
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["NO_VALUES_TO_UPDATE"]) if update.empty?
|
|
35
39
|
|
|
36
40
|
updated = BetterAuth::Plugins.api_key_update_record(ctx, record, update.merge(updatedAt: Time.now), record_config)
|
|
41
|
+
unless updated
|
|
42
|
+
raise BetterAuth::APIError.new(
|
|
43
|
+
"INTERNAL_SERVER_ERROR",
|
|
44
|
+
message: BetterAuth::Plugins::API_KEY_ERROR_CODES["FAILED_TO_UPDATE_API_KEY"],
|
|
45
|
+
code: "FAILED_TO_UPDATE_API_KEY"
|
|
46
|
+
)
|
|
47
|
+
end
|
|
37
48
|
updated = BetterAuth::Plugins.api_key_migrate_legacy_metadata(ctx, updated, record_config)
|
|
38
49
|
BetterAuth::Plugins.api_key_delete_expired(ctx.context, record_config)
|
|
39
50
|
ctx.json(BetterAuth::Plugins.api_key_public(updated, include_key_field: false))
|
|
@@ -4,39 +4,71 @@ module BetterAuth
|
|
|
4
4
|
module APIKey
|
|
5
5
|
module Routes
|
|
6
6
|
module VerifyAPIKey
|
|
7
|
-
UPSTREAM_SOURCE = "upstream/packages/api-key/src/routes/verify-api-key.ts"
|
|
7
|
+
UPSTREAM_SOURCE = "reference/upstream-src/1.6.9/repository/packages/api-key/src/routes/verify-api-key.ts"
|
|
8
8
|
|
|
9
9
|
module_function
|
|
10
10
|
|
|
11
11
|
def endpoint(config)
|
|
12
|
-
BetterAuth::Endpoint.new(
|
|
12
|
+
BetterAuth::Endpoint.new(
|
|
13
|
+
path: "/api-key/verify",
|
|
14
|
+
method: "POST",
|
|
15
|
+
body_schema: ->(value) { value },
|
|
16
|
+
metadata: Routes.openapi_for(:verify_api_key)
|
|
17
|
+
) do |ctx|
|
|
13
18
|
body = BetterAuth::Plugins.normalize_hash(ctx.body)
|
|
14
19
|
resolved_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, body[:config_id])
|
|
15
20
|
key = body[:key]
|
|
16
21
|
if key.to_s.empty?
|
|
17
22
|
raise BetterAuth::APIError.new(
|
|
18
|
-
"
|
|
23
|
+
"UNAUTHORIZED",
|
|
19
24
|
message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"],
|
|
20
25
|
code: "INVALID_API_KEY"
|
|
21
26
|
)
|
|
22
27
|
end
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
validation_config = body[:config_id] ? resolved_config : config_for_key(ctx, key, config)
|
|
30
|
+
validation_config ||= resolved_config
|
|
31
|
+
validator = validation_config[:custom_api_key_validator]
|
|
32
|
+
if validator.respond_to?(:call) && !validator.call({ctx: ctx, key: key})
|
|
33
|
+
raise BetterAuth::APIError.new(
|
|
34
|
+
"UNAUTHORIZED",
|
|
35
|
+
message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"],
|
|
36
|
+
code: "KEY_NOT_FOUND"
|
|
37
|
+
)
|
|
26
38
|
else
|
|
27
|
-
record = BetterAuth::Plugins.api_key_validate!(ctx, key,
|
|
39
|
+
record = BetterAuth::Plugins.api_key_validate!(ctx, key, validation_config, permissions: body[:permissions])
|
|
28
40
|
record_config = BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, BetterAuth::Plugins.api_key_record_config_id(record))
|
|
29
41
|
BetterAuth::Plugins.api_key_schedule_cleanup(ctx, record_config)
|
|
30
42
|
ctx.json({valid: true, error: nil, key: BetterAuth::Plugins.api_key_public(record, include_key_field: false)})
|
|
31
43
|
end
|
|
32
44
|
rescue BetterAuth::APIError => error
|
|
33
45
|
ctx.context.logger.error("Failed to validate API key: #{error.message}") if ctx.context.logger.respond_to?(:error)
|
|
34
|
-
|
|
46
|
+
payload = BetterAuth::Plugins.api_key_error_payload(error)
|
|
47
|
+
payload[:code] = error.code if error.code == "KEY_NOT_FOUND"
|
|
48
|
+
ctx.json(
|
|
49
|
+
{valid: false, error: payload, key: nil},
|
|
50
|
+
status: error.status_code,
|
|
51
|
+
headers: error.headers
|
|
52
|
+
)
|
|
35
53
|
rescue => error
|
|
36
54
|
ctx.context.logger.error("Failed to validate API key: #{error.message}") if ctx.context.logger.respond_to?(:error)
|
|
37
55
|
ctx.json({valid: false, error: {message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"], code: "INVALID_API_KEY"}, key: nil})
|
|
38
56
|
end
|
|
39
57
|
end
|
|
58
|
+
|
|
59
|
+
def config_for_key(ctx, key, config)
|
|
60
|
+
config.fetch(:configurations, [config]).each do |entry|
|
|
61
|
+
hashed = BetterAuth::Plugins.api_key_hash(key, entry)
|
|
62
|
+
record = BetterAuth::Plugins.api_key_find_by_hash(ctx, hashed, entry)
|
|
63
|
+
next unless record
|
|
64
|
+
|
|
65
|
+
record_config_id = BetterAuth::Plugins.api_key_record_config_id(record)
|
|
66
|
+
return entry if BetterAuth::Plugins.api_key_config_id_matches?(record_config_id, entry[:config_id])
|
|
67
|
+
|
|
68
|
+
return BetterAuth::Plugins.api_key_resolve_config(ctx.context, config, record_config_id)
|
|
69
|
+
end
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
40
72
|
end
|
|
41
73
|
end
|
|
42
74
|
end
|
|
@@ -24,6 +24,9 @@ module BetterAuth
|
|
|
24
24
|
minimum = create ? 0 : 1
|
|
25
25
|
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_REMAINING"]) if body[:remaining].to_i < minimum
|
|
26
26
|
end
|
|
27
|
+
if body.key?(:refill_amount) && !body[:refill_amount].nil? && body[:refill_amount].to_i < 1
|
|
28
|
+
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_REMAINING"])
|
|
29
|
+
end
|
|
27
30
|
if body[:metadata] && (create || config[:enable_metadata])
|
|
28
31
|
raise BetterAuth::APIError.new("BAD_REQUEST", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["METADATA_DISABLED"]) unless config[:enable_metadata]
|
|
29
32
|
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)
|
|
@@ -69,63 +72,258 @@ module BetterAuth
|
|
|
69
72
|
def validate_api_key!(ctx, key, config, permissions: nil)
|
|
70
73
|
hashed = BetterAuth::APIKey::Keys.hash(key, config)
|
|
71
74
|
record = BetterAuth::APIKey::Adapter.find_by_hash(ctx, hashed, config)
|
|
72
|
-
|
|
75
|
+
|
|
76
|
+
# In fallback mode the database is authoritative. A cache hit must be
|
|
77
|
+
# re-read so revocation, expiry, permission, and counter updates cannot
|
|
78
|
+
# be hidden by a stale secondary snapshot.
|
|
79
|
+
if record && config[:storage] == "secondary-storage" && config[:fallback_to_database]
|
|
80
|
+
authoritative = ctx.context.adapter.find_one(
|
|
81
|
+
model: BetterAuth::Plugins::API_KEY_TABLE_NAME,
|
|
82
|
+
where: [{field: "key", value: hashed}]
|
|
83
|
+
)
|
|
84
|
+
unless authoritative
|
|
85
|
+
BetterAuth::APIKey::Adapter.delete(ctx, record, config)
|
|
86
|
+
record = nil
|
|
87
|
+
end
|
|
88
|
+
record = authoritative if authoritative
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
raise invalid_api_key_error unless record
|
|
92
|
+
# Adapters are expected to return row snapshots. The in-memory test
|
|
93
|
+
# adapter exposes its backing hash directly, so copy the top-level row
|
|
94
|
+
# before evaluating guards to avoid one request mutating another's
|
|
95
|
+
# decision snapshot.
|
|
96
|
+
record = record.dup
|
|
73
97
|
unless BetterAuth::APIKey::Routes.config_id_matches?(BetterAuth::APIKey::Types.record_config_id(record), config[:config_id])
|
|
74
|
-
raise
|
|
98
|
+
raise invalid_api_key_error
|
|
75
99
|
end
|
|
76
100
|
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
|
|
101
|
+
if record["expiresAt"] && BetterAuth::APIKey::Utils.normalize_time(record["expiresAt"]) <= Time.now
|
|
78
102
|
BetterAuth::APIKey::Adapter.schedule_record_delete(ctx, record, config)
|
|
79
103
|
raise BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["KEY_EXPIRED"])
|
|
80
104
|
end
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
105
|
+
# Keep an exhausted row inert until an explicit cleanup pass. Eager
|
|
106
|
+
# deletion here can race a concurrent winner between its guarded quota
|
|
107
|
+
# decrement and final row refresh, causing a valid request to observe a
|
|
108
|
+
# missing row. The database remains authoritative and the next request
|
|
109
|
+
# is still rejected with USAGE_EXCEEDED.
|
|
110
|
+
if record["remaining"].to_i <= 0 && !record["remaining"].nil? && record["refillAmount"].to_i <= 0
|
|
111
|
+
raise usage_exceeded_error
|
|
84
112
|
end
|
|
85
113
|
|
|
86
114
|
check_permissions!(record, permissions)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
115
|
+
updated = if config[:storage] == "database" || config[:fallback_to_database]
|
|
116
|
+
claim_usage_in_database(ctx, record, config)
|
|
117
|
+
else
|
|
118
|
+
warn_best_effort_secondary(ctx, config)
|
|
119
|
+
claim_usage_in_secondary(ctx, record, config, hashed)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
BetterAuth::APIKey::Adapter.migrate_legacy_metadata(ctx, updated, config)
|
|
90
123
|
end
|
|
91
124
|
|
|
92
|
-
def
|
|
125
|
+
def claim_usage_in_database(ctx, record, config)
|
|
126
|
+
row = record
|
|
127
|
+
row = consume_remaining_database(ctx, row) unless row["remaining"].nil?
|
|
128
|
+
row = consume_rate_limit_database(ctx, row, config)
|
|
129
|
+
|
|
130
|
+
final_row = ctx.context.adapter.update(
|
|
131
|
+
model: BetterAuth::Plugins::API_KEY_TABLE_NAME,
|
|
132
|
+
where: [{field: "id", value: row["id"]}],
|
|
133
|
+
update: {updatedAt: Time.now}
|
|
134
|
+
)
|
|
135
|
+
unless final_row
|
|
136
|
+
if ctx.context.adapter.find_one(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: row["id"]}])
|
|
137
|
+
raise BetterAuth::APIError.new(
|
|
138
|
+
"INTERNAL_SERVER_ERROR",
|
|
139
|
+
message: BetterAuth::Plugins::API_KEY_ERROR_CODES["FAILED_TO_UPDATE_API_KEY"],
|
|
140
|
+
code: "FAILED_TO_UPDATE_API_KEY"
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
BetterAuth::APIKey::Adapter.delete(ctx, record, config)
|
|
144
|
+
raise invalid_api_key_error
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
if config[:storage] == "secondary-storage"
|
|
148
|
+
BetterAuth::APIKey::Adapter.set(ctx, final_row, config)
|
|
149
|
+
# A delete can commit after the counter update but before this cache
|
|
150
|
+
# publication. Re-check after publishing so that interleaving cannot
|
|
151
|
+
# leave a non-expiring ghost entry behind.
|
|
152
|
+
authoritative = ctx.context.adapter.find_one(
|
|
153
|
+
model: BetterAuth::Plugins::API_KEY_TABLE_NAME,
|
|
154
|
+
where: [{field: "id", value: final_row["id"]}]
|
|
155
|
+
)
|
|
156
|
+
unless authoritative
|
|
157
|
+
BetterAuth::APIKey::Adapter.delete(ctx, final_row, config)
|
|
158
|
+
raise invalid_api_key_error
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
final_row
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def consume_remaining_database(ctx, record)
|
|
93
165
|
now = Time.now
|
|
94
|
-
|
|
166
|
+
refill_interval = record["refillInterval"]
|
|
167
|
+
refill_amount = record["refillAmount"]
|
|
168
|
+
if refill_interval && refill_amount.to_i.positive?
|
|
169
|
+
last_refill = BetterAuth::APIKey::Utils.normalize_time(record["lastRefillAt"] || record["createdAt"])
|
|
170
|
+
if last_refill && ((now - last_refill) * 1000) > refill_interval.to_i
|
|
171
|
+
refilled = ctx.context.adapter.increment_one(
|
|
172
|
+
model: BetterAuth::Plugins::API_KEY_TABLE_NAME,
|
|
173
|
+
where: [
|
|
174
|
+
{field: "id", value: record["id"]},
|
|
175
|
+
{field: "lastRefillAt", value: record["lastRefillAt"]}
|
|
176
|
+
],
|
|
177
|
+
increment: {},
|
|
178
|
+
set: {remaining: refill_amount.to_i - 1, lastRefillAt: now},
|
|
179
|
+
allow_server_managed: true
|
|
180
|
+
)
|
|
181
|
+
return refilled if refilled
|
|
182
|
+
end
|
|
183
|
+
end
|
|
95
184
|
|
|
96
|
-
|
|
185
|
+
decremented = ctx.context.adapter.increment_one(
|
|
186
|
+
model: BetterAuth::Plugins::API_KEY_TABLE_NAME,
|
|
187
|
+
where: [{field: "id", value: record["id"]}, {field: "remaining", operator: "gt", value: 0}],
|
|
188
|
+
increment: {remaining: -1},
|
|
189
|
+
allow_server_managed: true
|
|
190
|
+
)
|
|
191
|
+
return decremented if decremented
|
|
192
|
+
|
|
193
|
+
raise usage_exceeded_error
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def consume_rate_limit_database(ctx, record, config)
|
|
197
|
+
now = Time.now
|
|
198
|
+
decision = BetterAuth::APIKey::RateLimit.evaluate(record, config, now)
|
|
199
|
+
if decision[:type] == :deny
|
|
97
200
|
raise BetterAuth::APIError.new(
|
|
98
|
-
"
|
|
99
|
-
message:
|
|
201
|
+
"TOO_MANY_REQUESTS",
|
|
202
|
+
message: decision[:message],
|
|
100
203
|
code: "RATE_LIMITED",
|
|
101
204
|
body: {
|
|
102
|
-
message:
|
|
205
|
+
message: decision[:message],
|
|
103
206
|
code: "RATE_LIMITED",
|
|
104
|
-
details: {tryAgainIn: try_again_in}
|
|
207
|
+
details: {tryAgainIn: decision[:try_again_in]}
|
|
105
208
|
}
|
|
106
209
|
)
|
|
107
210
|
end
|
|
108
|
-
|
|
211
|
+
|
|
212
|
+
case decision[:type]
|
|
213
|
+
when :skip
|
|
214
|
+
return record unless decision[:last_request]
|
|
215
|
+
|
|
216
|
+
updated = ctx.context.adapter.update(
|
|
217
|
+
model: BetterAuth::Plugins::API_KEY_TABLE_NAME,
|
|
218
|
+
where: [{field: "id", value: record["id"]}],
|
|
219
|
+
update: {lastRequest: decision[:last_request]}
|
|
220
|
+
)
|
|
221
|
+
return updated || record
|
|
222
|
+
when :start, :reset
|
|
223
|
+
guard = if decision[:type] == :reset
|
|
224
|
+
{field: "lastRequest", operator: "lte", value: decision[:window_start]}
|
|
225
|
+
else
|
|
226
|
+
{field: "lastRequest", value: nil}
|
|
227
|
+
end
|
|
228
|
+
started = ctx.context.adapter.increment_one(
|
|
229
|
+
model: BetterAuth::Plugins::API_KEY_TABLE_NAME,
|
|
230
|
+
where: [{field: "id", value: record["id"]}, guard],
|
|
231
|
+
increment: {},
|
|
232
|
+
set: {requestCount: 1, lastRequest: decision[:now]},
|
|
233
|
+
allow_server_managed: true
|
|
234
|
+
)
|
|
235
|
+
return started if started
|
|
236
|
+
when :increment
|
|
237
|
+
incremented = ctx.context.adapter.increment_one(
|
|
238
|
+
model: BetterAuth::Plugins::API_KEY_TABLE_NAME,
|
|
239
|
+
where: [
|
|
240
|
+
{field: "id", value: record["id"]},
|
|
241
|
+
{field: "lastRequest", operator: "gt", value: decision[:window_start]},
|
|
242
|
+
{field: "requestCount", operator: "lt", value: decision[:max]}
|
|
243
|
+
],
|
|
244
|
+
increment: {requestCount: 1},
|
|
245
|
+
set: {lastRequest: decision[:now]},
|
|
246
|
+
allow_server_managed: true
|
|
247
|
+
)
|
|
248
|
+
return incremented if incremented
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
fresh = ctx.context.adapter.find_one(model: BetterAuth::Plugins::API_KEY_TABLE_NAME, where: [{field: "id", value: record["id"]}])
|
|
252
|
+
raise invalid_api_key_error unless fresh
|
|
253
|
+
|
|
254
|
+
# One concurrent writer may have opened/reset the window. Re-evaluate
|
|
255
|
+
# against the fresh row; this recursion is bounded by the finite race.
|
|
256
|
+
consume_rate_limit_database(ctx, fresh, config)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def claim_usage_in_secondary(ctx, record, config, hashed)
|
|
260
|
+
now = Time.now
|
|
261
|
+
fresh = BetterAuth::APIKey::Adapter.find_by_hash(ctx, hashed, config)
|
|
262
|
+
raise invalid_api_key_error unless fresh
|
|
263
|
+
|
|
264
|
+
update = usage_update(fresh, config, now)
|
|
265
|
+
merged = fresh.merge(update.transform_keys { |key_name| BetterAuth::Schema.storage_key(key_name) })
|
|
266
|
+
BetterAuth::APIKey::Adapter.set(ctx, merged, config)
|
|
267
|
+
merged
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def usage_update(record, config, now = Time.now)
|
|
271
|
+
update = {lastRequest: now, updatedAt: now}
|
|
272
|
+
decision = BetterAuth::APIKey::RateLimit.evaluate(record, config, now)
|
|
273
|
+
if decision[:type] == :deny
|
|
274
|
+
raise BetterAuth::APIError.new(
|
|
275
|
+
"TOO_MANY_REQUESTS",
|
|
276
|
+
message: decision[:message],
|
|
277
|
+
code: "RATE_LIMITED",
|
|
278
|
+
body: {message: decision[:message], code: "RATE_LIMITED", details: {tryAgainIn: decision[:try_again_in]}}
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
case decision[:type]
|
|
282
|
+
when :start, :reset
|
|
283
|
+
update[:requestCount] = 1
|
|
284
|
+
when :increment
|
|
285
|
+
update[:requestCount] = record["requestCount"].to_i + 1
|
|
286
|
+
when :skip
|
|
287
|
+
update[:lastRequest] = decision[:last_request] if decision[:last_request]
|
|
288
|
+
end
|
|
109
289
|
|
|
110
290
|
remaining = record["remaining"]
|
|
111
291
|
if !remaining.nil?
|
|
112
|
-
if remaining.to_i <= 0 && record["refillAmount"] && record["refillInterval"]
|
|
292
|
+
if remaining.to_i <= 0 && record["refillAmount"].to_i.positive? && record["refillInterval"]
|
|
113
293
|
last_refill = BetterAuth::APIKey::Utils.normalize_time(record["lastRefillAt"] || record["createdAt"])
|
|
114
294
|
if !last_refill || ((now - last_refill) * 1000) > record["refillInterval"].to_i
|
|
115
295
|
remaining = record["refillAmount"].to_i
|
|
116
296
|
update[:lastRefillAt] = now
|
|
117
297
|
end
|
|
118
298
|
end
|
|
119
|
-
raise
|
|
299
|
+
raise usage_exceeded_error if remaining.to_i <= 0
|
|
120
300
|
|
|
121
301
|
update[:remaining] = remaining.to_i - 1
|
|
122
302
|
end
|
|
123
303
|
update
|
|
124
304
|
end
|
|
125
305
|
|
|
306
|
+
def invalid_api_key_error
|
|
307
|
+
BetterAuth::APIError.new("UNAUTHORIZED", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["INVALID_API_KEY"])
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def usage_exceeded_error
|
|
311
|
+
BetterAuth::APIError.new("TOO_MANY_REQUESTS", message: BetterAuth::Plugins::API_KEY_ERROR_CODES["USAGE_EXCEEDED"])
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def warn_best_effort_secondary(ctx, config)
|
|
315
|
+
return if config[:storage] != "secondary-storage" || config[:fallback_to_database]
|
|
316
|
+
return if ctx.context.respond_to?(:runtime_fetch) && ctx.context.runtime_fetch(:api_key_secondary_warning, false)
|
|
317
|
+
|
|
318
|
+
logger = ctx.context.logger if ctx.context.respond_to?(:logger)
|
|
319
|
+
logger.warn("[API KEY PLUGIN] Secondary-storage-only API-key counters are best-effort; use fallback_to_database for atomic enforcement.") if logger.respond_to?(:warn)
|
|
320
|
+
ctx.context.runtime_store(:api_key_secondary_warning, true) if ctx.context.respond_to?(:runtime_store)
|
|
321
|
+
end
|
|
322
|
+
|
|
126
323
|
def check_permissions!(record, required)
|
|
127
324
|
return if required.nil? || required == {}
|
|
128
325
|
|
|
326
|
+
BetterAuth::Plugins.load_plugin!(:access)
|
|
129
327
|
actual = BetterAuth::APIKey::Utils.decode_json(record["permissions"]) || {}
|
|
130
328
|
result = BetterAuth::Plugins::Role.new(actual).authorize(required)
|
|
131
329
|
unless result[:success]
|
|
@@ -43,6 +43,8 @@ module BetterAuth
|
|
|
43
43
|
BetterAuth::APIKey::PluginFactory.build(configurations, options)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
API_KEY_PLUGIN_IMPLEMENTATION = true
|
|
47
|
+
|
|
46
48
|
def api_key_config(configurations, options = nil)
|
|
47
49
|
BetterAuth::APIKey::Configuration.normalize(configurations, options)
|
|
48
50
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: better_auth-api-key
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sebastian Sala
|
|
@@ -37,6 +37,34 @@ dependencies:
|
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '2.5'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: better_auth-mongodb
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0.9'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0.9'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: better_auth-redis-storage
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.9'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0.9'
|
|
40
68
|
- !ruby/object:Gem::Dependency
|
|
41
69
|
name: minitest
|
|
42
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -51,6 +79,34 @@ dependencies:
|
|
|
51
79
|
- - "~>"
|
|
52
80
|
- !ruby/object:Gem::Version
|
|
53
81
|
version: '5.25'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: mysql2
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0.5'
|
|
89
|
+
type: :development
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0.5'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: pg
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '1.5'
|
|
103
|
+
type: :development
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '1.5'
|
|
54
110
|
- !ruby/object:Gem::Dependency
|
|
55
111
|
name: rake
|
|
56
112
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,6 +121,48 @@ dependencies:
|
|
|
65
121
|
- - "~>"
|
|
66
122
|
- !ruby/object:Gem::Version
|
|
67
123
|
version: '13.2'
|
|
124
|
+
- !ruby/object:Gem::Dependency
|
|
125
|
+
name: redis
|
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
|
127
|
+
requirements:
|
|
128
|
+
- - "~>"
|
|
129
|
+
- !ruby/object:Gem::Version
|
|
130
|
+
version: '5.0'
|
|
131
|
+
type: :development
|
|
132
|
+
prerelease: false
|
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - "~>"
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: '5.0'
|
|
138
|
+
- !ruby/object:Gem::Dependency
|
|
139
|
+
name: sequel
|
|
140
|
+
requirement: !ruby/object:Gem::Requirement
|
|
141
|
+
requirements:
|
|
142
|
+
- - "~>"
|
|
143
|
+
- !ruby/object:Gem::Version
|
|
144
|
+
version: '5.83'
|
|
145
|
+
type: :development
|
|
146
|
+
prerelease: false
|
|
147
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
148
|
+
requirements:
|
|
149
|
+
- - "~>"
|
|
150
|
+
- !ruby/object:Gem::Version
|
|
151
|
+
version: '5.83'
|
|
152
|
+
- !ruby/object:Gem::Dependency
|
|
153
|
+
name: sqlite3
|
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
|
155
|
+
requirements:
|
|
156
|
+
- - "~>"
|
|
157
|
+
- !ruby/object:Gem::Version
|
|
158
|
+
version: '2.0'
|
|
159
|
+
type: :development
|
|
160
|
+
prerelease: false
|
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
162
|
+
requirements:
|
|
163
|
+
- - "~>"
|
|
164
|
+
- !ruby/object:Gem::Version
|
|
165
|
+
version: '2.0'
|
|
68
166
|
- !ruby/object:Gem::Dependency
|
|
69
167
|
name: standardrb
|
|
70
168
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -79,6 +177,20 @@ dependencies:
|
|
|
79
177
|
- - "~>"
|
|
80
178
|
- !ruby/object:Gem::Version
|
|
81
179
|
version: '1.0'
|
|
180
|
+
- !ruby/object:Gem::Dependency
|
|
181
|
+
name: tiny_tds
|
|
182
|
+
requirement: !ruby/object:Gem::Requirement
|
|
183
|
+
requirements:
|
|
184
|
+
- - "~>"
|
|
185
|
+
- !ruby/object:Gem::Version
|
|
186
|
+
version: '2.1'
|
|
187
|
+
type: :development
|
|
188
|
+
prerelease: false
|
|
189
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
190
|
+
requirements:
|
|
191
|
+
- - "~>"
|
|
192
|
+
- !ruby/object:Gem::Version
|
|
193
|
+
version: '2.1'
|
|
82
194
|
description: Adds API key creation, verification, management, quotas, metadata, and
|
|
83
195
|
permissions for Better Auth Ruby. Includes storage modes. Also supports API-key-backed
|
|
84
196
|
sessions. Better Auth Ruby is an independent modern authentication framework for
|
|
@@ -90,6 +202,7 @@ extensions: []
|
|
|
90
202
|
extra_rdoc_files: []
|
|
91
203
|
files:
|
|
92
204
|
- CHANGELOG.md
|
|
205
|
+
- LICENSE.md
|
|
93
206
|
- README.md
|
|
94
207
|
- lib/better_auth/api_key.rb
|
|
95
208
|
- lib/better_auth/api_key/adapter.rb
|
|
@@ -114,14 +227,14 @@ files:
|
|
|
114
227
|
- lib/better_auth/api_key/validation.rb
|
|
115
228
|
- lib/better_auth/api_key/version.rb
|
|
116
229
|
- lib/better_auth/plugins/api_key.rb
|
|
117
|
-
homepage: https://github.com/sebasxsala/better-auth
|
|
230
|
+
homepage: https://github.com/sebasxsala/better-auth-rb
|
|
118
231
|
licenses:
|
|
119
232
|
- MIT
|
|
120
233
|
metadata:
|
|
121
|
-
homepage_uri: https://github.com/sebasxsala/better-auth
|
|
122
|
-
source_code_uri: https://github.com/sebasxsala/better-auth
|
|
123
|
-
changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-api-key/CHANGELOG.md
|
|
124
|
-
bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
|
|
234
|
+
homepage_uri: https://github.com/sebasxsala/better-auth-rb
|
|
235
|
+
source_code_uri: https://github.com/sebasxsala/better-auth-rb
|
|
236
|
+
changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-api-key/CHANGELOG.md
|
|
237
|
+
bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
|
|
125
238
|
rdoc_options: []
|
|
126
239
|
require_paths:
|
|
127
240
|
- lib
|