better_auth-redis-storage 0.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -1
- data/README.md +110 -4
- data/lib/better_auth/redis_storage/version.rb +1 -1
- data/lib/better_auth/redis_storage.rb +57 -9
- 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: 93c01dd5ff213cf3095641e36d70bd2b4e0d80ce7d344688ed830e01d4ad43e6
|
|
4
|
+
data.tar.gz: 9e416e99e8c8d4df05e71c5cf1af743f43e50d99009beefb502280c49c42e726
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dd78bfd531a72e64fb05a338261f37daaedaf5b3c32cb262b77b92bfdb6b611194e1d64b583f18c1922520affe52f6ab9f247de1613348be13b6b52a7234df41
|
|
7
|
+
data.tar.gz: 2d54b9ffe8517dd5e4729c3ef6a010b60f57015b9b05f52a3e7fab281b56037ee544e0936dca420249d4e3aeff84721524639e01cccc30fa4913184158f4496a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.2.0 - 2026-04-29
|
|
6
|
+
|
|
7
|
+
- Add `BetterAuth.redis_storage` and `BetterAuth::RedisStorage.redisStorage` builders for upstream-shaped Redis storage configuration.
|
|
8
|
+
- Add optional `scan_count:` support to use Redis `SCAN` instead of upstream-compatible `KEYS`.
|
|
9
|
+
- Split real Redis coverage into a `REDIS_INTEGRATION=1` integration suite and expand secondary-storage compatibility tests.
|
|
10
|
+
|
|
3
11
|
## 0.1.0
|
|
4
12
|
|
|
5
13
|
- Initial Redis secondary storage package for Better Auth Ruby.
|
|
6
|
-
|
data/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Redis secondary storage package for Better Auth Ruby.
|
|
4
4
|
|
|
5
|
+
This gem tracks the server-side behavior of upstream `@better-auth/redis-storage`
|
|
6
|
+
pinned at Better Auth `v1.6.9`. The Ruby gem versions independently from the
|
|
7
|
+
upstream npm package; `BetterAuth::RedisStorage::VERSION` is the Ruby gem
|
|
8
|
+
version.
|
|
9
|
+
|
|
5
10
|
## Installation
|
|
6
11
|
|
|
7
12
|
Add the gem and require the package before configuring auth:
|
|
@@ -19,13 +24,114 @@ redis = Redis.new(url: ENV.fetch("REDIS_URL"))
|
|
|
19
24
|
auth = BetterAuth.auth(
|
|
20
25
|
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
21
26
|
database: :memory,
|
|
22
|
-
secondary_storage: BetterAuth
|
|
27
|
+
secondary_storage: BetterAuth.redis_storage(client: redis)
|
|
28
|
+
)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The canonical Ruby form is also supported:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
storage = BetterAuth::RedisStorage.new(client: redis)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For upstream-shaped call sites, use `BetterAuth.redis_storage(client: redis)` or
|
|
38
|
+
the camelCase class alias:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
storage = BetterAuth::RedisStorage.redisStorage(client: redis)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Configuration
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
storage = BetterAuth::RedisStorage.new(
|
|
48
|
+
client: redis,
|
|
49
|
+
key_prefix: "better-auth:",
|
|
50
|
+
scan_count: nil
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`client` must respond to `get`, `set`, `setex`, `del`, and `keys`. It should
|
|
55
|
+
also respond to `scan` when `scan_count:` is configured. This matches the
|
|
56
|
+
interfaces exposed by the `redis` and `redis-namespace` gems.
|
|
57
|
+
|
|
58
|
+
`key_prefix` defaults to `"better-auth:"`. Passing `nil` falls back to the
|
|
59
|
+
default. Any other value, including the empty string, is honored verbatim.
|
|
60
|
+
Redis databases are not isolation boundaries for shared clients; applications
|
|
61
|
+
sharing a Redis instance should use distinct prefixes.
|
|
62
|
+
|
|
63
|
+
`scan_count` is a Ruby-only opt-in for large Redis databases. By default the gem
|
|
64
|
+
uses `KEYS "#{key_prefix}*"` to match upstream exactly. Set `scan_count:` to a
|
|
65
|
+
positive count such as `100`, `500`, or `1000` to use `SCAN` instead:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
storage = BetterAuth::RedisStorage.new(client: redis, scan_count: 500)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Behavior
|
|
72
|
+
|
|
73
|
+
The storage object implements the Better Auth secondary storage contract:
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
storage.get(key)
|
|
77
|
+
storage.set(key, value, ttl = nil)
|
|
78
|
+
storage.delete(key)
|
|
79
|
+
storage.list_keys
|
|
80
|
+
storage.clear
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`listKeys` is available as a camelCase alias for upstream parity.
|
|
84
|
+
|
|
85
|
+
TTL handling for `set(key, value, ttl)`:
|
|
86
|
+
|
|
87
|
+
| TTL value | Redis command |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `nil`, non-numeric strings, `0`, negative numbers | `set(prefixed_key, value)` |
|
|
90
|
+
| Positive `Integer` | `setex(prefixed_key, ttl, value)` |
|
|
91
|
+
| Positive `Float` | `setex(prefixed_key, ttl.to_i, value)` |
|
|
92
|
+
| Positive numeric `String` | `setex(prefixed_key, ttl.to_i, value)` |
|
|
93
|
+
|
|
94
|
+
`set`, `delete`, and `clear` return `nil`, mirroring upstream's `Promise<void>`
|
|
95
|
+
contract in Ruby form. Tests and applications should assert stored values via
|
|
96
|
+
`get` rather than relying on truthy return values.
|
|
97
|
+
|
|
98
|
+
`clear` intentionally differs from upstream when there are no matching keys:
|
|
99
|
+
upstream calls `del(...keys)` even when `keys` is empty, while this Ruby gem
|
|
100
|
+
skips `del` to avoid Redis `ERR wrong number of arguments for 'del'`.
|
|
101
|
+
|
|
102
|
+
## Better Auth Usage
|
|
103
|
+
|
|
104
|
+
`secondary_storage` is used by Better Auth for session payload storage,
|
|
105
|
+
active-session indexes, verification values, and rate limiting when
|
|
106
|
+
`rate_limit: { storage: "secondary-storage" }` is configured.
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
auth = BetterAuth.auth(
|
|
110
|
+
secret: ENV.fetch("BETTER_AUTH_SECRET"),
|
|
111
|
+
database: :memory,
|
|
112
|
+
secondary_storage: BetterAuth.redis_storage(client: redis),
|
|
113
|
+
rate_limit: { storage: "secondary-storage", enabled: true }
|
|
23
114
|
)
|
|
24
115
|
```
|
|
25
116
|
|
|
26
|
-
|
|
117
|
+
Custom secondary storage backends should implement:
|
|
27
118
|
|
|
28
|
-
|
|
119
|
+
- `get(key)`
|
|
120
|
+
- `set(key, value, ttl = nil)`
|
|
121
|
+
- `delete(key)`
|
|
122
|
+
- Optional: `list_keys` or `listKeys`
|
|
123
|
+
- Optional: `clear`
|
|
29
124
|
|
|
30
|
-
|
|
125
|
+
## Testing
|
|
31
126
|
|
|
127
|
+
The normal unit suite skips real Redis unless explicitly enabled:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
bundle exec rake test
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Run the Redis integration suite with:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
REDIS_INTEGRATION=1 REDIS_URL=redis://localhost:6379/15 bundle exec rake test:integration
|
|
137
|
+
```
|
|
@@ -4,18 +4,28 @@ require "better_auth"
|
|
|
4
4
|
require_relative "redis_storage/version"
|
|
5
5
|
|
|
6
6
|
module BetterAuth
|
|
7
|
+
def self.redis_storage(client:, key_prefix: RedisStorage::DEFAULT_KEY_PREFIX, scan_count: nil)
|
|
8
|
+
RedisStorage.new(client: client, key_prefix: key_prefix, scan_count: scan_count)
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
class RedisStorage
|
|
8
12
|
DEFAULT_KEY_PREFIX = "better-auth:"
|
|
13
|
+
SCAN_DEFAULT_COUNT = 100
|
|
9
14
|
|
|
10
|
-
attr_reader :client, :key_prefix
|
|
15
|
+
attr_reader :client, :key_prefix, :scan_count
|
|
16
|
+
|
|
17
|
+
def self.build(client:, key_prefix: DEFAULT_KEY_PREFIX, scan_count: nil)
|
|
18
|
+
new(client: client, key_prefix: key_prefix, scan_count: scan_count)
|
|
19
|
+
end
|
|
11
20
|
|
|
12
|
-
def self.
|
|
13
|
-
new(client: client, key_prefix: key_prefix)
|
|
21
|
+
def self.redisStorage(client:, key_prefix: DEFAULT_KEY_PREFIX, scan_count: nil)
|
|
22
|
+
new(client: client, key_prefix: key_prefix, scan_count: scan_count)
|
|
14
23
|
end
|
|
15
24
|
|
|
16
|
-
def initialize(client:, key_prefix: DEFAULT_KEY_PREFIX)
|
|
25
|
+
def initialize(client:, key_prefix: DEFAULT_KEY_PREFIX, scan_count: nil)
|
|
17
26
|
@client = client
|
|
18
|
-
@key_prefix = key_prefix.to_s
|
|
27
|
+
@key_prefix = key_prefix.nil? ? DEFAULT_KEY_PREFIX : key_prefix.to_s
|
|
28
|
+
@scan_count = scan_count
|
|
19
29
|
end
|
|
20
30
|
|
|
21
31
|
def get(key)
|
|
@@ -24,24 +34,30 @@ module BetterAuth
|
|
|
24
34
|
|
|
25
35
|
def set(key, value, ttl = nil)
|
|
26
36
|
prefixed_key = prefix_key(key)
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
coerced_ttl = coerce_ttl(ttl)
|
|
38
|
+
if coerced_ttl
|
|
39
|
+
client.setex(prefixed_key, coerced_ttl, value)
|
|
29
40
|
else
|
|
30
41
|
client.set(prefixed_key, value)
|
|
31
42
|
end
|
|
43
|
+
nil
|
|
32
44
|
end
|
|
33
45
|
|
|
34
46
|
def delete(key)
|
|
35
47
|
client.del(prefix_key(key))
|
|
48
|
+
nil
|
|
36
49
|
end
|
|
37
50
|
|
|
38
51
|
def list_keys
|
|
39
|
-
|
|
52
|
+
storage_keys.map { |key| unprefix_key(key) }
|
|
40
53
|
end
|
|
41
54
|
|
|
42
55
|
def clear
|
|
43
|
-
keys =
|
|
56
|
+
keys = storage_keys
|
|
57
|
+
# Upstream calls del(...keys) unconditionally; Ruby keeps this guard to
|
|
58
|
+
# avoid Redis ERR wrong number of arguments when no prefixed keys exist.
|
|
44
59
|
client.del(*keys) unless keys.empty?
|
|
60
|
+
nil
|
|
45
61
|
end
|
|
46
62
|
|
|
47
63
|
alias_method :listKeys, :list_keys
|
|
@@ -55,5 +71,37 @@ module BetterAuth
|
|
|
55
71
|
def unprefix_key(key)
|
|
56
72
|
key.sub(/\A#{Regexp.escape(key_prefix)}/, "")
|
|
57
73
|
end
|
|
74
|
+
|
|
75
|
+
def storage_keys
|
|
76
|
+
return scan_keys if scan_count
|
|
77
|
+
|
|
78
|
+
client.keys("#{key_prefix}*")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def scan_keys
|
|
82
|
+
cursor = "0"
|
|
83
|
+
matches = []
|
|
84
|
+
loop do
|
|
85
|
+
cursor, keys = client.scan(cursor, match: "#{key_prefix}*", count: scan_count)
|
|
86
|
+
matches.concat(keys)
|
|
87
|
+
break if cursor.to_s == "0"
|
|
88
|
+
end
|
|
89
|
+
matches
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def coerce_ttl(ttl)
|
|
93
|
+
numeric = case ttl
|
|
94
|
+
when nil
|
|
95
|
+
nil
|
|
96
|
+
when Integer
|
|
97
|
+
ttl
|
|
98
|
+
when Float
|
|
99
|
+
ttl.to_i
|
|
100
|
+
when String
|
|
101
|
+
Integer(ttl, exception: false)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
numeric&.positive? ? numeric : nil
|
|
105
|
+
end
|
|
58
106
|
end
|
|
59
107
|
end
|