better_auth 0.4.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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/README.md +24 -0
  4. data/lib/better_auth/adapters/internal_adapter.rb +5 -5
  5. data/lib/better_auth/adapters/sql.rb +96 -18
  6. data/lib/better_auth/api.rb +113 -13
  7. data/lib/better_auth/configuration.rb +97 -7
  8. data/lib/better_auth/context.rb +165 -12
  9. data/lib/better_auth/cookies.rb +6 -4
  10. data/lib/better_auth/core.rb +2 -0
  11. data/lib/better_auth/crypto/jwe.rb +27 -5
  12. data/lib/better_auth/crypto.rb +32 -0
  13. data/lib/better_auth/database_hooks.rb +5 -5
  14. data/lib/better_auth/endpoint.rb +87 -3
  15. data/lib/better_auth/error.rb +8 -1
  16. data/lib/better_auth/plugins/admin/schema.rb +2 -2
  17. data/lib/better_auth/plugins/admin.rb +344 -16
  18. data/lib/better_auth/plugins/anonymous.rb +37 -3
  19. data/lib/better_auth/plugins/device_authorization.rb +102 -5
  20. data/lib/better_auth/plugins/dub.rb +148 -0
  21. data/lib/better_auth/plugins/email_otp.rb +246 -15
  22. data/lib/better_auth/plugins/expo.rb +17 -1
  23. data/lib/better_auth/plugins/generic_oauth.rb +53 -7
  24. data/lib/better_auth/plugins/jwt.rb +37 -4
  25. data/lib/better_auth/plugins/last_login_method.rb +2 -2
  26. data/lib/better_auth/plugins/magic_link.rb +66 -3
  27. data/lib/better_auth/plugins/mcp/authorization.rb +111 -0
  28. data/lib/better_auth/plugins/mcp/config.rb +51 -0
  29. data/lib/better_auth/plugins/mcp/consent.rb +31 -0
  30. data/lib/better_auth/plugins/mcp/legacy_aliases.rb +39 -0
  31. data/lib/better_auth/plugins/mcp/metadata.rb +81 -0
  32. data/lib/better_auth/plugins/mcp/registration.rb +31 -0
  33. data/lib/better_auth/plugins/mcp/resource_handler.rb +37 -0
  34. data/lib/better_auth/plugins/mcp/schema.rb +91 -0
  35. data/lib/better_auth/plugins/mcp/token.rb +108 -0
  36. data/lib/better_auth/plugins/mcp/userinfo.rb +37 -0
  37. data/lib/better_auth/plugins/mcp.rb +111 -263
  38. data/lib/better_auth/plugins/multi_session.rb +61 -3
  39. data/lib/better_auth/plugins/oauth_protocol.rb +2 -2
  40. data/lib/better_auth/plugins/oauth_proxy.rb +26 -6
  41. data/lib/better_auth/plugins/oidc_provider.rb +118 -14
  42. data/lib/better_auth/plugins/one_tap.rb +7 -2
  43. data/lib/better_auth/plugins/one_time_token.rb +42 -2
  44. data/lib/better_auth/plugins/open_api.rb +163 -318
  45. data/lib/better_auth/plugins/organization.rb +135 -36
  46. data/lib/better_auth/plugins/phone_number.rb +141 -6
  47. data/lib/better_auth/plugins/siwe.rb +69 -3
  48. data/lib/better_auth/plugins/two_factor.rb +65 -23
  49. data/lib/better_auth/plugins/username.rb +57 -2
  50. data/lib/better_auth/rate_limiter.rb +20 -0
  51. data/lib/better_auth/response.rb +42 -0
  52. data/lib/better_auth/router.rb +7 -1
  53. data/lib/better_auth/routes/account.rb +204 -38
  54. data/lib/better_auth/routes/email_verification.rb +98 -14
  55. data/lib/better_auth/routes/password.rb +125 -8
  56. data/lib/better_auth/routes/session.rb +128 -13
  57. data/lib/better_auth/routes/sign_in.rb +24 -2
  58. data/lib/better_auth/routes/sign_out.rb +13 -1
  59. data/lib/better_auth/routes/sign_up.rb +62 -4
  60. data/lib/better_auth/routes/social.rb +102 -7
  61. data/lib/better_auth/routes/user.rb +222 -20
  62. data/lib/better_auth/routes/validation.rb +50 -0
  63. data/lib/better_auth/secret_config.rb +115 -0
  64. data/lib/better_auth/session.rb +1 -1
  65. data/lib/better_auth/url_helpers.rb +12 -1
  66. data/lib/better_auth/version.rb +1 -1
  67. data/lib/better_auth.rb +4 -0
  68. metadata +15 -1
@@ -24,7 +24,37 @@ module BetterAuth
24
24
  end
25
25
 
26
26
  def get_siwe_nonce_endpoint(config)
27
- Endpoint.new(path: "/siwe/nonce", method: "POST", body_schema: ->(body) { siwe_nonce_body(body) }) do |ctx|
27
+ Endpoint.new(
28
+ path: "/siwe/nonce",
29
+ method: "POST",
30
+ body_schema: ->(body) { siwe_nonce_body(body) },
31
+ metadata: {
32
+ openapi: {
33
+ operationId: "getSiweNonce",
34
+ description: "Generate a nonce for Sign-In with Ethereum",
35
+ requestBody: OpenAPI.json_request_body(
36
+ OpenAPI.object_schema(
37
+ {
38
+ walletAddress: {type: "string"},
39
+ chainId: {type: ["number", "string", "null"]}
40
+ },
41
+ required: ["walletAddress"]
42
+ )
43
+ ),
44
+ responses: {
45
+ "200" => OpenAPI.json_response(
46
+ "SIWE nonce",
47
+ OpenAPI.object_schema(
48
+ {
49
+ nonce: {type: "string"}
50
+ },
51
+ required: ["nonce"]
52
+ )
53
+ )
54
+ }
55
+ }
56
+ }
57
+ ) do |ctx|
28
58
  body = normalize_hash(ctx.body)
29
59
  wallet_address = siwe_normalize_wallet!(body[:wallet_address])
30
60
  chain_id = siwe_chain_id(body[:chain_id])
@@ -42,7 +72,42 @@ module BetterAuth
42
72
  end
43
73
 
44
74
  def verify_siwe_message_endpoint(config)
45
- Endpoint.new(path: "/siwe/verify", method: "POST", body_schema: ->(body) { siwe_verify_body(body, config) }) do |ctx|
75
+ Endpoint.new(
76
+ path: "/siwe/verify",
77
+ method: "POST",
78
+ body_schema: ->(body) { siwe_verify_body(body, config) },
79
+ metadata: {
80
+ openapi: {
81
+ operationId: "verifySiweMessage",
82
+ description: "Verify a Sign-In with Ethereum message",
83
+ requestBody: OpenAPI.json_request_body(
84
+ OpenAPI.object_schema(
85
+ {
86
+ walletAddress: {type: "string"},
87
+ chainId: {type: ["number", "string", "null"]},
88
+ message: {type: "string"},
89
+ signature: {type: "string"},
90
+ email: {type: ["string", "null"]}
91
+ },
92
+ required: ["walletAddress", "message", "signature"]
93
+ )
94
+ ),
95
+ responses: {
96
+ "200" => OpenAPI.json_response(
97
+ "SIWE message verified",
98
+ OpenAPI.object_schema(
99
+ {
100
+ token: {type: "string"},
101
+ success: {type: "boolean"},
102
+ user: {type: "object"}
103
+ },
104
+ required: ["token", "success", "user"]
105
+ )
106
+ )
107
+ }
108
+ }
109
+ }
110
+ ) do |ctx|
46
111
  body = normalize_hash(ctx.body)
47
112
  wallet_address = siwe_normalize_wallet!(body[:wallet_address])
48
113
  chain_id = siwe_chain_id(body[:chain_id])
@@ -203,7 +268,8 @@ module BetterAuth
203
268
  ctx.context.internal_adapter.create_user(
204
269
  name: ens[:name] || wallet_address,
205
270
  email: (anonymous == false && !email.empty?) ? email : "#{wallet_address}@#{domain}",
206
- image: ens[:avatar] || ""
271
+ image: ens[:avatar] || "",
272
+ context: ctx
207
273
  )
208
274
  end
209
275
 
@@ -79,13 +79,13 @@ module BetterAuth
79
79
  end
80
80
 
81
81
  def two_factor_enable_endpoint(config)
82
- Endpoint.new(path: "/two-factor/enable", method: "POST") do |ctx|
83
- session = Routes.current_session(ctx, sensitive: true)
82
+ Endpoint.new(path: "/two-factor/enable", method: "POST", metadata: two_factor_openapi("enableTwoFactor", "Enable two factor authentication", two_factor_enable_response_schema)) do |ctx|
83
+ session = Routes.current_session(ctx)
84
84
  body = normalize_hash(ctx.body)
85
85
  two_factor_check_password!(ctx, session[:user]["id"], body[:password], allow_passwordless: config[:allow_passwordless])
86
86
 
87
87
  secret = two_factor_generate_secret
88
- backup = two_factor_generate_backup_codes(ctx.context.secret, config[:backup_code_options])
88
+ backup = two_factor_generate_backup_codes(ctx.context.secret_config, config[:backup_code_options])
89
89
  if config[:skip_verification_on_enable]
90
90
  updated_user = ctx.context.internal_adapter.update_user(session[:user]["id"], twoFactorEnabled: true)
91
91
  new_session = ctx.context.internal_adapter.create_session(updated_user["id"], false)
@@ -99,7 +99,7 @@ module BetterAuth
99
99
  ctx.context.adapter.create(
100
100
  model: TWO_FACTOR_MODEL,
101
101
  data: {
102
- secret: Crypto.symmetric_encrypt(key: ctx.context.secret, data: secret),
102
+ secret: Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: secret),
103
103
  backupCodes: backup[:stored],
104
104
  userId: session[:user]["id"],
105
105
  verified: verified
@@ -115,8 +115,8 @@ module BetterAuth
115
115
  end
116
116
 
117
117
  def two_factor_disable_endpoint(config)
118
- Endpoint.new(path: "/two-factor/disable", method: "POST") do |ctx|
119
- session = Routes.current_session(ctx, sensitive: true)
118
+ Endpoint.new(path: "/two-factor/disable", method: "POST", metadata: two_factor_openapi("disableTwoFactor", "Disable two factor authentication", OpenAPI.status_response_schema)) do |ctx|
119
+ session = Routes.current_session(ctx)
120
120
  body = normalize_hash(ctx.body)
121
121
  two_factor_check_password!(ctx, session[:user]["id"], body[:password], allow_passwordless: config[:allow_passwordless])
122
122
 
@@ -138,7 +138,7 @@ module BetterAuth
138
138
  end
139
139
 
140
140
  def two_factor_generate_totp_endpoint(config)
141
- Endpoint.new(path: "/totp/generate", method: "POST") do |ctx|
141
+ Endpoint.new(path: "/totp/generate", method: "POST", metadata: two_factor_openapi("generateTOTP", "Generate a TOTP code", OpenAPI.object_schema({code: {type: "string"}}, required: ["code"]))) do |ctx|
142
142
  two_factor_totp_enabled!(config)
143
143
  body = normalize_hash(ctx.body)
144
144
  ctx.json({code: two_factor_totp(body[:secret], options: config[:totp_options])})
@@ -146,20 +146,20 @@ module BetterAuth
146
146
  end
147
147
 
148
148
  def two_factor_get_totp_uri_endpoint(config)
149
- Endpoint.new(path: "/two-factor/get-totp-uri", method: "POST") do |ctx|
149
+ Endpoint.new(path: "/two-factor/get-totp-uri", method: "POST", metadata: two_factor_openapi("getTOTPURI", "Get the TOTP URI", OpenAPI.object_schema({totpURI: {type: "string"}}, required: ["totpURI"]))) do |ctx|
150
150
  two_factor_totp_enabled!(config)
151
- session = Routes.current_session(ctx, sensitive: true)
151
+ session = Routes.current_session(ctx)
152
152
  two_factor_check_password!(ctx, session[:user]["id"], normalize_hash(ctx.body)[:password], allow_passwordless: config[:totp_options][:allow_passwordless])
153
153
  record = two_factor_record(ctx, config, session[:user]["id"])
154
154
  raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TOTP_NOT_ENABLED"]) unless record
155
155
 
156
- secret = Crypto.symmetric_decrypt(key: ctx.context.secret, data: record["secret"])
156
+ secret = Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: record["secret"])
157
157
  ctx.json({totpURI: two_factor_totp_uri(secret, issuer: config[:issuer] || ctx.context.app_name, account: session[:user]["email"], options: config[:totp_options])})
158
158
  end
159
159
  end
160
160
 
161
161
  def two_factor_verify_totp_endpoint(config)
162
- Endpoint.new(path: "/two-factor/verify-totp", method: "POST") do |ctx|
162
+ Endpoint.new(path: "/two-factor/verify-totp", method: "POST", metadata: two_factor_openapi("verifyTOTP", "Verify a TOTP code", two_factor_verification_response_schema)) do |ctx|
163
163
  two_factor_totp_enabled!(config)
164
164
  body = normalize_hash(ctx.body)
165
165
  data = two_factor_verification_context(ctx, config)
@@ -169,7 +169,7 @@ module BetterAuth
169
169
  raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TOTP_NOT_ENABLED"])
170
170
  end
171
171
 
172
- secret = Crypto.symmetric_decrypt(key: ctx.context.secret, data: record["secret"])
172
+ secret = Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: record["secret"])
173
173
  raise APIError.new("UNAUTHORIZED", message: TWO_FACTOR_ERROR_CODES["INVALID_CODE"]) unless two_factor_totp_valid?(secret, body[:code], options: config[:totp_options])
174
174
 
175
175
  if record["verified"] != true
@@ -191,7 +191,7 @@ module BetterAuth
191
191
  end
192
192
 
193
193
  def two_factor_send_otp_endpoint(config)
194
- Endpoint.new(path: "/two-factor/send-otp", method: "POST") do |ctx|
194
+ Endpoint.new(path: "/two-factor/send-otp", method: "POST", metadata: two_factor_openapi("sendTwoFactorOTP", "Send a two factor OTP", OpenAPI.status_response_schema)) do |ctx|
195
195
  otp_config = config[:otp_options]
196
196
  sender = otp_config[:send_otp]
197
197
  unless sender.respond_to?(:call)
@@ -212,7 +212,7 @@ module BetterAuth
212
212
  end
213
213
 
214
214
  def two_factor_verify_otp_endpoint(config)
215
- Endpoint.new(path: "/two-factor/verify-otp", method: "POST") do |ctx|
215
+ Endpoint.new(path: "/two-factor/verify-otp", method: "POST", metadata: two_factor_openapi("verifyTwoFactorOTP", "Verify a two factor OTP", two_factor_verification_response_schema)) do |ctx|
216
216
  body = normalize_hash(ctx.body)
217
217
  data = two_factor_verification_context(ctx, config)
218
218
  verification = ctx.context.internal_adapter.find_verification_value("2fa-otp-#{data[:key]}")
@@ -246,19 +246,19 @@ module BetterAuth
246
246
  end
247
247
 
248
248
  def two_factor_verify_backup_code_endpoint(config)
249
- Endpoint.new(path: "/two-factor/verify-backup-code", method: "POST") do |ctx|
249
+ Endpoint.new(path: "/two-factor/verify-backup-code", method: "POST", metadata: two_factor_openapi("verifyBackupCode", "Verify a two factor backup code", two_factor_verification_response_schema)) do |ctx|
250
250
  body = normalize_hash(ctx.body)
251
251
  data = two_factor_verification_context(ctx, config)
252
252
  record = two_factor_record(ctx, config, data[:session][:user]["id"])
253
253
  raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["BACKUP_CODES_NOT_ENABLED"]) unless record
254
254
 
255
- codes = two_factor_read_backup_codes(ctx.context.secret, record["backupCodes"], config[:backup_code_options])
255
+ codes = two_factor_read_backup_codes(ctx.context.secret_config, record["backupCodes"], config[:backup_code_options])
256
256
  unless codes.include?(body[:code].to_s)
257
257
  raise APIError.new("UNAUTHORIZED", message: TWO_FACTOR_ERROR_CODES["INVALID_BACKUP_CODE"])
258
258
  end
259
259
 
260
260
  remaining = codes.reject { |code| code == body[:code].to_s }
261
- stored = two_factor_store_backup_codes(ctx.context.secret, remaining, config[:backup_code_options])
261
+ stored = two_factor_store_backup_codes(ctx.context.secret_config, remaining, config[:backup_code_options])
262
262
  updated = ctx.context.adapter.update(
263
263
  model: TWO_FACTOR_MODEL,
264
264
  where: [{field: "id", value: record["id"]}, {field: "backupCodes", value: record["backupCodes"]}],
@@ -271,15 +271,15 @@ module BetterAuth
271
271
  end
272
272
 
273
273
  def two_factor_generate_backup_codes_endpoint(config)
274
- Endpoint.new(path: "/two-factor/generate-backup-codes", method: "POST") do |ctx|
275
- session = Routes.current_session(ctx, sensitive: true)
274
+ Endpoint.new(path: "/two-factor/generate-backup-codes", method: "POST", metadata: two_factor_openapi("generateBackupCodes", "Generate two factor backup codes", two_factor_backup_codes_response_schema)) do |ctx|
275
+ session = Routes.current_session(ctx)
276
276
  raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TWO_FACTOR_NOT_ENABLED"]) unless session[:user]["twoFactorEnabled"]
277
277
 
278
278
  two_factor_check_password!(ctx, session[:user]["id"], normalize_hash(ctx.body)[:password], allow_passwordless: config[:backup_code_options][:allow_passwordless])
279
279
  record = two_factor_record(ctx, config, session[:user]["id"])
280
280
  raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["TWO_FACTOR_NOT_ENABLED"]) unless record
281
281
 
282
- backup = two_factor_generate_backup_codes(ctx.context.secret, config[:backup_code_options])
282
+ backup = two_factor_generate_backup_codes(ctx.context.secret_config, config[:backup_code_options])
283
283
  ctx.context.adapter.update(model: TWO_FACTOR_MODEL, where: [{field: "id", value: record["id"]}], update: {backupCodes: backup[:stored]})
284
284
  ctx.json({status: true, backupCodes: backup[:codes]})
285
285
  end
@@ -291,10 +291,52 @@ module BetterAuth
291
291
  record = two_factor_record(ctx, config, body[:user_id])
292
292
  raise APIError.new("BAD_REQUEST", message: TWO_FACTOR_ERROR_CODES["BACKUP_CODES_NOT_ENABLED"]) unless record
293
293
 
294
- ctx.json({status: true, backupCodes: two_factor_read_backup_codes(ctx.context.secret, record["backupCodes"], config[:backup_code_options])})
294
+ ctx.json({status: true, backupCodes: two_factor_read_backup_codes(ctx.context.secret_config, record["backupCodes"], config[:backup_code_options])})
295
295
  end
296
296
  end
297
297
 
298
+ def two_factor_openapi(operation_id, description, response_schema)
299
+ {
300
+ openapi: {
301
+ operationId: operation_id,
302
+ description: description,
303
+ responses: {
304
+ "200" => OpenAPI.json_response("Success", response_schema)
305
+ }
306
+ }
307
+ }
308
+ end
309
+
310
+ def two_factor_enable_response_schema
311
+ OpenAPI.object_schema(
312
+ {
313
+ totpURI: {type: "string"},
314
+ backupCodes: {type: "array", items: {type: "string"}}
315
+ },
316
+ required: ["totpURI", "backupCodes"]
317
+ )
318
+ end
319
+
320
+ def two_factor_verification_response_schema
321
+ OpenAPI.object_schema(
322
+ {
323
+ token: {type: ["string", "null"]},
324
+ user: {type: ["object", "null"], "$ref": "#/components/schemas/User"},
325
+ status: {type: ["boolean", "null"]}
326
+ }
327
+ )
328
+ end
329
+
330
+ def two_factor_backup_codes_response_schema
331
+ OpenAPI.object_schema(
332
+ {
333
+ status: {type: "boolean"},
334
+ backupCodes: {type: "array", items: {type: "string"}}
335
+ },
336
+ required: ["status", "backupCodes"]
337
+ )
338
+ end
339
+
298
340
  def two_factor_schema(config = {})
299
341
  custom_schema = config[:schema]
300
342
  base = {
@@ -507,7 +549,7 @@ module BetterAuth
507
549
  if storage == "hashed"
508
550
  Crypto.sha256(code, encoding: :base64url)
509
551
  elsif storage == "encrypted"
510
- Crypto.symmetric_encrypt(key: ctx.context.secret, data: code)
552
+ Crypto.symmetric_encrypt(key: ctx.context.secret_config, data: code)
511
553
  elsif storage.is_a?(Hash) && storage[:hash].respond_to?(:call)
512
554
  storage[:hash].call(code)
513
555
  elsif storage.is_a?(Hash) && storage[:encrypt].respond_to?(:call)
@@ -522,7 +564,7 @@ module BetterAuth
522
564
  expected, actual = if storage == "hashed"
523
565
  [stored, Crypto.sha256(input, encoding: :base64url)]
524
566
  elsif storage == "encrypted"
525
- [Crypto.symmetric_decrypt(key: ctx.context.secret, data: stored), input]
567
+ [Crypto.symmetric_decrypt(key: ctx.context.secret_config, data: stored), input]
526
568
  elsif storage.is_a?(Hash) && storage[:hash].respond_to?(:call)
527
569
  [stored, storage[:hash].call(input)]
528
570
  elsif storage.is_a?(Hash) && storage[:decrypt].respond_to?(:call)
@@ -53,7 +53,34 @@ module BetterAuth
53
53
  allowed_media_types: [
54
54
  "application/x-www-form-urlencoded",
55
55
  "application/json"
56
- ]
56
+ ],
57
+ openapi: {
58
+ operationId: "signInUsername",
59
+ description: "Sign in with username and password",
60
+ requestBody: OpenAPI.json_request_body(
61
+ OpenAPI.object_schema(
62
+ {
63
+ username: {type: "string"},
64
+ password: {type: "string"},
65
+ callbackURL: {type: ["string", "null"]},
66
+ rememberMe: {type: ["boolean", "null"]}
67
+ },
68
+ required: ["username", "password"]
69
+ )
70
+ ),
71
+ responses: {
72
+ "200" => OpenAPI.json_response(
73
+ "Signed in",
74
+ OpenAPI.object_schema(
75
+ {
76
+ token: {type: "string"},
77
+ user: {type: "object", "$ref": "#/components/schemas/User"}
78
+ },
79
+ required: ["token", "user"]
80
+ )
81
+ )
82
+ }
83
+ }
57
84
  }
58
85
  ) do |ctx|
59
86
  body = normalize_hash(ctx.body)
@@ -115,7 +142,35 @@ module BetterAuth
115
142
  end
116
143
 
117
144
  def is_username_available_endpoint(config)
118
- Endpoint.new(path: "/is-username-available", method: "POST") do |ctx|
145
+ Endpoint.new(
146
+ path: "/is-username-available",
147
+ method: "POST",
148
+ metadata: {
149
+ openapi: {
150
+ operationId: "isUsernameAvailable",
151
+ description: "Check whether a username is available",
152
+ requestBody: OpenAPI.json_request_body(
153
+ OpenAPI.object_schema(
154
+ {
155
+ username: {type: "string"}
156
+ },
157
+ required: ["username"]
158
+ )
159
+ ),
160
+ responses: {
161
+ "200" => OpenAPI.json_response(
162
+ "Username availability",
163
+ OpenAPI.object_schema(
164
+ {
165
+ available: {type: "boolean"}
166
+ },
167
+ required: ["available"]
168
+ )
169
+ )
170
+ }
171
+ }
172
+ }
173
+ ) do |ctx|
119
174
  body = normalize_hash(ctx.body)
120
175
  username = body[:username].to_s
121
176
  raise APIError.new("UNPROCESSABLE_ENTITY", message: USERNAME_ERROR_CODES["INVALID_USERNAME"]) if username.empty?
@@ -142,6 +142,7 @@ module BetterAuth
142
142
 
143
143
  def storage_for(context, config)
144
144
  return [:custom, config[:custom_storage]] if config[:custom_storage]
145
+ return [:database, context.internal_adapter.adapter] if config[:storage] == "database"
145
146
 
146
147
  if config[:storage] == "secondary-storage" && context.options.secondary_storage
147
148
  return [:secondary, context.options.secondary_storage]
@@ -151,6 +152,8 @@ module BetterAuth
151
152
  end
152
153
 
153
154
  def read_storage((type, storage), key)
155
+ return read_database_storage(storage, key) if type == :database
156
+
154
157
  data = storage.get(key)
155
158
  data = JSON.parse(data) if type == :secondary && data.is_a?(String)
156
159
  normalize_rate_limit_data(symbolize_keys(data))
@@ -159,12 +162,29 @@ module BetterAuth
159
162
  end
160
163
 
161
164
  def write_storage((type, storage), key, data, ttl:, update:)
165
+ return write_database_storage(storage, key, data) if type == :database
166
+
162
167
  value = (type == :secondary) ? JSON.generate(secondary_storage_data(data)) : data
163
168
  return call_secondary_storage_set(storage, key, value, ttl: ttl, update: update) if type == :secondary
164
169
 
165
170
  call_storage_set(storage, key, value, ttl: ttl, update: update)
166
171
  end
167
172
 
173
+ def read_database_storage(adapter, key)
174
+ data = adapter.find_one(model: "rateLimit", where: [{field: "key", value: key}])
175
+ normalize_rate_limit_data(symbolize_keys(data))
176
+ end
177
+
178
+ def write_database_storage(adapter, key, data)
179
+ value = secondary_storage_data(data)
180
+ existing = adapter.find_one(model: "rateLimit", where: [{field: "key", value: key}])
181
+ if existing
182
+ adapter.update(model: "rateLimit", where: [{field: "key", value: key}], update: value)
183
+ else
184
+ adapter.create(model: "rateLimit", data: value)
185
+ end
186
+ end
187
+
168
188
  def secondary_storage_data(data)
169
189
  {
170
190
  key: data[:key],
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module BetterAuth
6
+ class Response
7
+ attr_reader :status, :headers, :body
8
+
9
+ def initialize(status:, headers:, body:)
10
+ @status = status
11
+ @headers = headers
12
+ @body = body
13
+ end
14
+
15
+ def self.from_rack(tuple)
16
+ status, headers, body = tuple
17
+ new(status: status, headers: headers, body: body)
18
+ end
19
+
20
+ def to_a
21
+ [status, headers, body]
22
+ end
23
+
24
+ alias_method :to_ary, :to_a
25
+
26
+ def each(&block)
27
+ to_a.each(&block)
28
+ end
29
+
30
+ def [](index)
31
+ to_a[index]
32
+ end
33
+
34
+ def first
35
+ status
36
+ end
37
+
38
+ def json(**options)
39
+ JSON.parse(body.join, **options)
40
+ end
41
+ end
42
+ end
@@ -88,6 +88,8 @@ module BetterAuth
88
88
  error_response(error)
89
89
  rescue JSON::ParserError
90
90
  error_response(APIError.new("BAD_REQUEST", message: "Invalid JSON body"))
91
+ ensure
92
+ context.clear_runtime! if context.respond_to?(:clear_runtime!)
91
93
  end
92
94
 
93
95
  def self.conflicting_methods(entries)
@@ -360,7 +362,11 @@ module BetterAuth
360
362
  end
361
363
 
362
364
  def server_only?(endpoint)
363
- endpoint.metadata[:server_only] || endpoint.metadata[:SERVER_ONLY] || endpoint.metadata["SERVER_ONLY"]
365
+ endpoint.metadata[:server_only] ||
366
+ endpoint.metadata[:SERVER_ONLY] ||
367
+ endpoint.metadata["SERVER_ONLY"] ||
368
+ endpoint.metadata[:scope].to_s == "server" ||
369
+ endpoint.metadata["scope"].to_s == "server"
364
370
  end
365
371
 
366
372
  def error_response(error, headers: {})