better_auth-redis-storage 0.8.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fe62175b11c22a9391598a8252c11a2ea849420d57d8c243054e1025f44ab1c
4
- data.tar.gz: ab84235280a0d49103e9f95b178f581cec1b0539b31f962abf49cdee183dc252
3
+ metadata.gz: f34a3a4a6ddeac7517500d5f886fd427bc7f9473ef0ee101a1d0e00d36f9a5f5
4
+ data.tar.gz: e00a5743378df1e4712ae318b3787762362d3a2077f8c7163978f518a2b5fc57
5
5
  SHA512:
6
- metadata.gz: 3764616d585fff3071e54317958feec3613e67e5c73d9cf4e5fac5760a8510990d64bf4161810da1bf35709ff3f396b367f6c9351e81a1acbdbcf57f6bc4d68e
7
- data.tar.gz: d26df3f575a4413f14083235edd7092f4a1a077c229faae8babdf3f2104a87830437a3480a91f77f1fb2a3a19e73bdd1bb6175430f3b473ff58f48d1f0a14ebe
6
+ metadata.gz: 2227f0cf222b1919fa63aa2fd4492b7f0ed8085677216372924492d2f42af701503adf50f4ffe50c3ffd412578e99e64074087aa232a5a4e693ac5c7b174e99c
7
+ data.tar.gz: a6e673a666c903a19b1ee752d5678f7da2ab2be02380e1e4e8c7710004ff7d7349acfb96e648fbf2bee318e4c95817f4e72e1c8d79b4114be4d0490c16398cb1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.10.0 - 2026-05-21
6
+
7
+ - Released in sync with the full Better Auth package set.
8
+
5
9
  ## 0.7.0 - 2026-05-05
6
10
 
7
11
  - Validate `scan_count` as either `nil` or a positive `Integer`.
data/README.md CHANGED
@@ -34,11 +34,13 @@ The canonical Ruby form is also supported:
34
34
  storage = BetterAuth::RedisStorage.new(client: redis)
35
35
  ```
36
36
 
37
- For upstream-shaped call sites, use `BetterAuth.redis_storage(client: redis)` or
38
- the camelCase class alias:
37
+ For upstream-shaped call sites, use `BetterAuth.redisStorage(client: redis)` or
38
+ the camelCase class alias. The upstream `keyPrefix:` keyword is accepted
39
+ alongside the canonical Ruby `key_prefix:` keyword:
39
40
 
40
41
  ```ruby
41
- storage = BetterAuth::RedisStorage.redisStorage(client: redis)
42
+ storage = BetterAuth.redisStorage(client: redis, keyPrefix: "my-app:")
43
+ storage = BetterAuth::RedisStorage.redisStorage(client: redis, keyPrefix: "my-app:")
42
44
  ```
43
45
 
44
46
  ## Configuration
@@ -47,20 +49,25 @@ storage = BetterAuth::RedisStorage.redisStorage(client: redis)
47
49
  storage = BetterAuth::RedisStorage.new(
48
50
  client: redis,
49
51
  key_prefix: "better-auth:",
50
- scan_count: nil,
52
+ scan_count: BetterAuth::RedisStorage::SCAN_DEFAULT_COUNT,
51
53
  atomic_clear: false
52
54
  )
53
55
  ```
54
56
 
55
- `client` must respond to `get`, `set`, `setex`, `del`, and `keys`. It should
56
- also respond to `scan` when `scan_count:` is configured, and to `incr` when
57
- `atomic_clear:` is enabled. This matches the interfaces exposed by the `redis`
58
- and `redis-namespace` gems.
57
+ `client` must respond to `get`, `set`, `setex`, `del`, and `scan`. It should
58
+ also respond to `keys` only when `scan_count: nil` is configured, and to `incr`
59
+ when `atomic_clear:` is enabled. This matches the interfaces exposed by the
60
+ `redis` and `redis-namespace` gems.
59
61
 
60
- `key_prefix` defaults to `"better-auth:"`. Passing `nil` falls back to the
61
- default. Any other value, including the empty string, is honored verbatim.
62
- Redis databases are not isolation boundaries for shared clients; applications
63
- sharing a Redis instance should use distinct prefixes.
62
+ `key_prefix` defaults to `"better-auth:"`. `keyPrefix:` is accepted for
63
+ upstream-shaped call sites. Passing `nil` falls back to the default. Any other
64
+ value, including the empty string, is honored verbatim. Redis databases are not
65
+ isolation boundaries for shared clients; applications sharing a Redis instance
66
+ should use distinct prefixes.
67
+
68
+ `list_keys` and `clear` escape Redis glob metacharacters in `key_prefix` before
69
+ matching keys, so prefixes containing characters such as `*`, `?`, `[`, `]`, or
70
+ `\` are treated as literal namespace bytes.
64
71
 
65
72
  > **Warning:** Passing `key_prefix: ""` puts Better Auth keys at the root of
66
73
  > the selected Redis logical namespace. `list_keys` and `clear` then match `*`,
@@ -68,14 +75,22 @@ sharing a Redis instance should use distinct prefixes.
68
75
  > key in that Redis database. Use an application-specific prefix unless the
69
76
  > Redis database is fully dedicated to Better Auth.
70
77
 
71
- `scan_count` is a Ruby-only opt-in for large Redis databases. By default the gem
72
- uses `KEYS "#{key_prefix}*"` to match upstream exactly. Set `scan_count:` to a
73
- positive count such as `100`, `500`, or `1000` to use `SCAN` instead:
78
+ `scan_count` defaults to `BetterAuth::RedisStorage::SCAN_DEFAULT_COUNT` and uses
79
+ Redis `SCAN` for `list_keys` and `clear`. Set `scan_count:` to a larger positive
80
+ count such as `500` or `1000` to tune scan page size:
74
81
 
75
82
  ```ruby
76
83
  storage = BetterAuth::RedisStorage.new(client: redis, scan_count: 500)
77
84
  ```
78
85
 
86
+ For exact legacy upstream behavior, pass `scan_count: nil` to use blocking
87
+ `KEYS "#{key_prefix}*"`. This is intended only for small or dedicated Redis
88
+ databases because `KEYS` can block Redis while it walks the keyspace:
89
+
90
+ ```ruby
91
+ storage = BetterAuth::RedisStorage.new(client: redis, scan_count: nil)
92
+ ```
93
+
79
94
  `atomic_clear` is a Ruby-only opt-in for applications that need `clear` to be
80
95
  logically atomic under concurrent writers:
81
96
 
@@ -108,7 +123,8 @@ storage.clear
108
123
  `listKeys` is available as a camelCase alias for upstream parity.
109
124
 
110
125
  `list_keys` returns every matching logical key but Redis does not guarantee key
111
- order for `KEYS` or `SCAN`. Sort the returned array in application code when a
126
+ order for `KEYS` or `SCAN`. The SCAN path removes duplicate cursor results while
127
+ preserving first-seen order. Sort the returned array in application code when a
112
128
  stable order matters.
113
129
 
114
130
  TTL handling for `set(key, value, ttl)`:
@@ -130,12 +146,13 @@ upstream calls `del(...keys)` even when `keys` is empty, while this Ruby gem
130
146
  skips `del` to avoid Redis `ERR wrong number of arguments for 'del'`.
131
147
  When keys do exist, `clear` deletes them in batches of
132
148
  `BetterAuth::RedisStorage::DELETE_CHUNK_SIZE` keys per `del` call to avoid very
133
- large Redis argument lists.
149
+ large Redis argument lists. The SCAN path collects the matched key set before
150
+ deleting it so cursor iteration is not affected by mutating the keyspace.
134
151
 
135
152
  With `atomic_clear: true`, `clear` increments a generation key with Redis
136
153
  `INCR`, making old generation keys immediately invisible to `get`, `set`,
137
154
  `delete`, `list_keys`, and Better Auth itself. Cleanup of the old generation is
138
- best-effort and uses `SCAN` when `scan_count:` is configured.
155
+ best-effort and uses `SCAN` by default.
139
156
 
140
157
  Redis Cluster users should treat `list_keys` and `clear` as operationally
141
158
  constrained helpers. This adapter does not scan every cluster node, and
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  class RedisStorage
5
- VERSION = "0.8.0"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
@@ -4,28 +4,32 @@ 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, atomic_clear: false)
8
- RedisStorage.new(client: client, key_prefix: key_prefix, scan_count: scan_count, atomic_clear: atomic_clear)
9
- end
10
-
11
7
  class RedisStorage
8
+ UNSET = Object.new.freeze
12
9
  DEFAULT_KEY_PREFIX = "better-auth:"
13
10
  SCAN_DEFAULT_COUNT = 100
14
11
  DELETE_CHUNK_SIZE = 500
15
12
 
16
13
  attr_reader :client, :key_prefix, :scan_count, :atomic_clear
17
14
 
18
- def self.build(client:, key_prefix: DEFAULT_KEY_PREFIX, scan_count: nil, atomic_clear: false)
19
- new(client: client, key_prefix: key_prefix, scan_count: scan_count, atomic_clear: atomic_clear)
15
+ def self.build(client:, key_prefix: UNSET, scan_count: UNSET, atomic_clear: false, **options)
16
+ key_prefix_camel = extract_key_prefix_camel!(options)
17
+ reject_unknown_keywords!(options)
18
+ new(client: client, key_prefix: key_prefix, key_prefix_camel: key_prefix_camel, scan_count: scan_count, atomic_clear: atomic_clear)
20
19
  end
21
20
 
22
- def self.redisStorage(client:, key_prefix: DEFAULT_KEY_PREFIX, scan_count: nil, atomic_clear: false)
23
- new(client: client, key_prefix: key_prefix, scan_count: scan_count, atomic_clear: atomic_clear)
21
+ def self.redisStorage(client:, key_prefix: UNSET, scan_count: UNSET, atomic_clear: false, **options)
22
+ key_prefix_camel = extract_key_prefix_camel!(options)
23
+ reject_unknown_keywords!(options)
24
+ new(client: client, key_prefix: key_prefix, key_prefix_camel: key_prefix_camel, scan_count: scan_count, atomic_clear: atomic_clear)
24
25
  end
25
26
 
26
- def initialize(client:, key_prefix: DEFAULT_KEY_PREFIX, scan_count: nil, atomic_clear: false)
27
+ def initialize(client:, key_prefix: UNSET, key_prefix_camel: UNSET, scan_count: UNSET, atomic_clear: false, **options)
28
+ key_prefix_camel = self.class.extract_key_prefix_camel!(options) if key_prefix_camel.equal?(UNSET)
29
+ self.class.reject_unknown_keywords!(options)
27
30
  @client = client
28
- @key_prefix = key_prefix.nil? ? DEFAULT_KEY_PREFIX : key_prefix.to_s
31
+ @key_prefix = self.class.resolve_key_prefix(key_prefix, key_prefix_camel)
32
+ scan_count = SCAN_DEFAULT_COUNT if scan_count.equal?(UNSET)
29
33
  if !scan_count.nil? && !(scan_count.is_a?(Integer) && scan_count.positive?)
30
34
  raise ArgumentError, "scan_count must be nil or a positive Integer; got #{scan_count.inspect}"
31
35
  end
@@ -33,6 +37,28 @@ module BetterAuth
33
37
  @atomic_clear = !!atomic_clear
34
38
  end
35
39
 
40
+ def self.extract_key_prefix_camel!(options)
41
+ options.key?(:keyPrefix) ? options.delete(:keyPrefix) : UNSET
42
+ end
43
+
44
+ def self.reject_unknown_keywords!(options)
45
+ return if options.empty?
46
+
47
+ unknown = options.keys.map(&:inspect).join(", ")
48
+ label = (options.length == 1) ? "keyword" : "keywords"
49
+ raise ArgumentError, "unknown #{label}: #{unknown}"
50
+ end
51
+
52
+ def self.resolve_key_prefix(key_prefix, key_prefix_camel)
53
+ if !key_prefix.equal?(UNSET) && !key_prefix_camel.equal?(UNSET) && key_prefix != key_prefix_camel
54
+ raise ArgumentError, "key_prefix and keyPrefix cannot both be provided with different values"
55
+ end
56
+
57
+ selected = key_prefix.equal?(UNSET) ? key_prefix_camel : key_prefix
58
+ selected = DEFAULT_KEY_PREFIX if selected.equal?(UNSET) || selected.nil?
59
+ selected.to_s
60
+ end
61
+
36
62
  def get(key)
37
63
  client.get(prefix_key(key))
38
64
  end
@@ -113,30 +139,34 @@ module BetterAuth
113
139
  def storage_keys(prefix = storage_prefix)
114
140
  return scan_keys(prefix) if scan_count
115
141
 
116
- client.keys("#{prefix}*")
142
+ client.keys(match_pattern(prefix))
117
143
  end
118
144
 
119
145
  def scan_keys(prefix = storage_prefix)
146
+ seen = {}
120
147
  matches = []
121
- each_scan_batch(prefix) { |keys| matches.concat(keys) }
148
+ each_scan_batch(prefix) do |keys|
149
+ keys.each do |key|
150
+ next if seen[key]
151
+
152
+ seen[key] = true
153
+ matches << key
154
+ end
155
+ end
122
156
  matches
123
157
  end
124
158
 
125
159
  def each_scan_batch(prefix = storage_prefix)
126
160
  cursor = "0"
127
161
  loop do
128
- cursor, keys = client.scan(cursor, match: "#{prefix}*", count: scan_count)
162
+ cursor, keys = client.scan(cursor, match: match_pattern(prefix), count: scan_count)
129
163
  yield keys
130
164
  break if cursor.to_s == "0"
131
165
  end
132
166
  end
133
167
 
134
168
  def delete_matching_keys(prefix, single_key: false)
135
- if scan_count
136
- each_scan_batch(prefix) { |keys| delete_keys(keys, single_key: single_key) }
137
- else
138
- delete_keys(storage_keys(prefix), single_key: single_key)
139
- end
169
+ delete_keys(storage_keys(prefix), single_key: single_key)
140
170
  end
141
171
 
142
172
  def delete_keys(keys, single_key: false)
@@ -149,6 +179,14 @@ module BetterAuth
149
179
  end
150
180
  end
151
181
 
182
+ def match_pattern(prefix)
183
+ "#{redis_glob_escape(prefix)}*"
184
+ end
185
+
186
+ def redis_glob_escape(value)
187
+ value.to_s.gsub(/[\\*?\[\]]/) { |character| "\\#{character}" }
188
+ end
189
+
152
190
  def coerce_ttl(ttl)
153
191
  numeric = case ttl
154
192
  when nil
@@ -171,4 +209,16 @@ module BetterAuth
171
209
  seconds.positive? ? seconds : nil
172
210
  end
173
211
  end
212
+
213
+ def self.redis_storage(client:, key_prefix: RedisStorage::UNSET, scan_count: RedisStorage::UNSET, atomic_clear: false, **options)
214
+ key_prefix_camel = RedisStorage.extract_key_prefix_camel!(options)
215
+ RedisStorage.reject_unknown_keywords!(options)
216
+ RedisStorage.new(client: client, key_prefix: key_prefix, key_prefix_camel: key_prefix_camel, scan_count: scan_count, atomic_clear: atomic_clear)
217
+ end
218
+
219
+ def self.redisStorage(client:, key_prefix: RedisStorage::UNSET, scan_count: RedisStorage::UNSET, atomic_clear: false, **options)
220
+ key_prefix_camel = RedisStorage.extract_key_prefix_camel!(options)
221
+ RedisStorage.reject_unknown_keywords!(options)
222
+ RedisStorage.new(client: client, key_prefix: key_prefix, key_prefix_camel: key_prefix_camel, scan_count: scan_count, atomic_clear: atomic_clear)
223
+ end
174
224
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth-redis-storage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Sala
@@ -112,14 +112,14 @@ files:
112
112
  - README.md
113
113
  - lib/better_auth/redis_storage.rb
114
114
  - lib/better_auth/redis_storage/version.rb
115
- homepage: https://github.com/sebasxsala/better-auth
115
+ homepage: https://github.com/sebasxsala/better-auth-rb
116
116
  licenses:
117
117
  - MIT
118
118
  metadata:
119
- homepage_uri: https://github.com/sebasxsala/better-auth
120
- source_code_uri: https://github.com/sebasxsala/better-auth
121
- changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-redis-storage/CHANGELOG.md
122
- bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
119
+ homepage_uri: https://github.com/sebasxsala/better-auth-rb
120
+ source_code_uri: https://github.com/sebasxsala/better-auth-rb
121
+ changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-redis-storage/CHANGELOG.md
122
+ bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
123
123
  rdoc_options: []
124
124
  require_paths:
125
125
  - lib