better_auth 0.1.1 → 0.3.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 (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +110 -18
  4. data/lib/better_auth/adapters/base.rb +49 -0
  5. data/lib/better_auth/adapters/internal_adapter.rb +589 -0
  6. data/lib/better_auth/adapters/memory.rb +235 -0
  7. data/lib/better_auth/adapters/mongodb.rb +9 -0
  8. data/lib/better_auth/adapters/mssql.rb +42 -0
  9. data/lib/better_auth/adapters/mysql.rb +33 -0
  10. data/lib/better_auth/adapters/postgres.rb +17 -0
  11. data/lib/better_auth/adapters/sql.rb +441 -0
  12. data/lib/better_auth/adapters/sqlite.rb +20 -0
  13. data/lib/better_auth/api.rb +226 -0
  14. data/lib/better_auth/api_error.rb +53 -0
  15. data/lib/better_auth/auth.rb +42 -0
  16. data/lib/better_auth/configuration.rb +399 -0
  17. data/lib/better_auth/context.rb +211 -0
  18. data/lib/better_auth/cookies.rb +278 -0
  19. data/lib/better_auth/core.rb +37 -1
  20. data/lib/better_auth/crypto/jwe.rb +76 -0
  21. data/lib/better_auth/crypto.rb +191 -0
  22. data/lib/better_auth/database_hooks.rb +114 -0
  23. data/lib/better_auth/endpoint.rb +326 -0
  24. data/lib/better_auth/error.rb +52 -0
  25. data/lib/better_auth/middleware/origin_check.rb +128 -0
  26. data/lib/better_auth/password.rb +120 -0
  27. data/lib/better_auth/plugin.rb +142 -0
  28. data/lib/better_auth/plugin_context.rb +16 -0
  29. data/lib/better_auth/plugin_registry.rb +67 -0
  30. data/lib/better_auth/plugins/access.rb +87 -0
  31. data/lib/better_auth/plugins/additional_fields.rb +29 -0
  32. data/lib/better_auth/plugins/admin/schema.rb +28 -0
  33. data/lib/better_auth/plugins/admin.rb +518 -0
  34. data/lib/better_auth/plugins/anonymous.rb +198 -0
  35. data/lib/better_auth/plugins/api_key.rb +16 -0
  36. data/lib/better_auth/plugins/bearer.rb +128 -0
  37. data/lib/better_auth/plugins/captcha.rb +159 -0
  38. data/lib/better_auth/plugins/custom_session.rb +84 -0
  39. data/lib/better_auth/plugins/device_authorization.rb +302 -0
  40. data/lib/better_auth/plugins/email_otp.rb +536 -0
  41. data/lib/better_auth/plugins/expo.rb +88 -0
  42. data/lib/better_auth/plugins/generic_oauth.rb +780 -0
  43. data/lib/better_auth/plugins/have_i_been_pwned.rb +94 -0
  44. data/lib/better_auth/plugins/jwt.rb +482 -0
  45. data/lib/better_auth/plugins/last_login_method.rb +92 -0
  46. data/lib/better_auth/plugins/magic_link.rb +181 -0
  47. data/lib/better_auth/plugins/mcp.rb +342 -0
  48. data/lib/better_auth/plugins/multi_session.rb +173 -0
  49. data/lib/better_auth/plugins/oauth_protocol.rb +694 -0
  50. data/lib/better_auth/plugins/oauth_provider.rb +16 -0
  51. data/lib/better_auth/plugins/oauth_proxy.rb +257 -0
  52. data/lib/better_auth/plugins/oidc_provider.rb +597 -0
  53. data/lib/better_auth/plugins/one_tap.rb +154 -0
  54. data/lib/better_auth/plugins/one_time_token.rb +106 -0
  55. data/lib/better_auth/plugins/open_api.rb +489 -0
  56. data/lib/better_auth/plugins/organization/schema.rb +106 -0
  57. data/lib/better_auth/plugins/organization.rb +995 -0
  58. data/lib/better_auth/plugins/passkey.rb +16 -0
  59. data/lib/better_auth/plugins/phone_number.rb +321 -0
  60. data/lib/better_auth/plugins/scim.rb +16 -0
  61. data/lib/better_auth/plugins/siwe.rb +242 -0
  62. data/lib/better_auth/plugins/sso.rb +16 -0
  63. data/lib/better_auth/plugins/stripe.rb +16 -0
  64. data/lib/better_auth/plugins/two_factor.rb +514 -0
  65. data/lib/better_auth/plugins/username.rb +278 -0
  66. data/lib/better_auth/plugins.rb +46 -0
  67. data/lib/better_auth/rate_limiter.rb +232 -0
  68. data/lib/better_auth/request_ip.rb +70 -0
  69. data/lib/better_auth/router.rb +378 -0
  70. data/lib/better_auth/routes/account.rb +211 -0
  71. data/lib/better_auth/routes/email_verification.rb +111 -0
  72. data/lib/better_auth/routes/error.rb +102 -0
  73. data/lib/better_auth/routes/ok.rb +15 -0
  74. data/lib/better_auth/routes/password.rb +183 -0
  75. data/lib/better_auth/routes/session.rb +160 -0
  76. data/lib/better_auth/routes/sign_in.rb +90 -0
  77. data/lib/better_auth/routes/sign_out.rb +15 -0
  78. data/lib/better_auth/routes/sign_up.rb +196 -0
  79. data/lib/better_auth/routes/social.rb +367 -0
  80. data/lib/better_auth/routes/user.rb +205 -0
  81. data/lib/better_auth/schema/sql.rb +202 -0
  82. data/lib/better_auth/schema.rb +291 -0
  83. data/lib/better_auth/session.rb +122 -0
  84. data/lib/better_auth/session_store.rb +91 -0
  85. data/lib/better_auth/social_providers/apple.rb +91 -0
  86. data/lib/better_auth/social_providers/atlassian.rb +32 -0
  87. data/lib/better_auth/social_providers/base.rb +325 -0
  88. data/lib/better_auth/social_providers/cognito.rb +32 -0
  89. data/lib/better_auth/social_providers/discord.rb +81 -0
  90. data/lib/better_auth/social_providers/dropbox.rb +33 -0
  91. data/lib/better_auth/social_providers/facebook.rb +35 -0
  92. data/lib/better_auth/social_providers/figma.rb +31 -0
  93. data/lib/better_auth/social_providers/github.rb +74 -0
  94. data/lib/better_auth/social_providers/gitlab.rb +67 -0
  95. data/lib/better_auth/social_providers/google.rb +90 -0
  96. data/lib/better_auth/social_providers/huggingface.rb +31 -0
  97. data/lib/better_auth/social_providers/kakao.rb +32 -0
  98. data/lib/better_auth/social_providers/kick.rb +32 -0
  99. data/lib/better_auth/social_providers/line.rb +33 -0
  100. data/lib/better_auth/social_providers/linear.rb +44 -0
  101. data/lib/better_auth/social_providers/linkedin.rb +30 -0
  102. data/lib/better_auth/social_providers/microsoft_entra_id.rb +137 -0
  103. data/lib/better_auth/social_providers/naver.rb +31 -0
  104. data/lib/better_auth/social_providers/notion.rb +33 -0
  105. data/lib/better_auth/social_providers/paybin.rb +31 -0
  106. data/lib/better_auth/social_providers/paypal.rb +36 -0
  107. data/lib/better_auth/social_providers/polar.rb +31 -0
  108. data/lib/better_auth/social_providers/railway.rb +49 -0
  109. data/lib/better_auth/social_providers/reddit.rb +32 -0
  110. data/lib/better_auth/social_providers/roblox.rb +31 -0
  111. data/lib/better_auth/social_providers/salesforce.rb +38 -0
  112. data/lib/better_auth/social_providers/slack.rb +30 -0
  113. data/lib/better_auth/social_providers/spotify.rb +31 -0
  114. data/lib/better_auth/social_providers/tiktok.rb +35 -0
  115. data/lib/better_auth/social_providers/twitch.rb +39 -0
  116. data/lib/better_auth/social_providers/twitter.rb +32 -0
  117. data/lib/better_auth/social_providers/vercel.rb +47 -0
  118. data/lib/better_auth/social_providers/vk.rb +34 -0
  119. data/lib/better_auth/social_providers/wechat.rb +104 -0
  120. data/lib/better_auth/social_providers/zoom.rb +31 -0
  121. data/lib/better_auth/social_providers.rb +38 -0
  122. data/lib/better_auth/version.rb +1 -1
  123. data/lib/better_auth.rb +86 -2
  124. metadata +233 -21
  125. data/.ruby-version +0 -1
  126. data/.standard.yml +0 -12
  127. data/.vscode/settings.json +0 -22
  128. data/AGENTS.md +0 -50
  129. data/CLAUDE.md +0 -1
  130. data/CODE_OF_CONDUCT.md +0 -173
  131. data/CONTRIBUTING.md +0 -187
  132. data/Gemfile +0 -12
  133. data/Makefile +0 -207
  134. data/Rakefile +0 -25
  135. data/SECURITY.md +0 -28
  136. data/docker-compose.yml +0 -63
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "time"
5
+
6
+ module BetterAuth
7
+ module Adapters
8
+ class Memory < Base
9
+ attr_reader :db
10
+
11
+ def initialize(options, db = nil)
12
+ super(options)
13
+ @db = db || build_db
14
+ end
15
+
16
+ def create(model:, data:, force_allow_id: false)
17
+ model = model.to_s
18
+ table_for(model) << transform_input(model, data, "create", force_allow_id)
19
+ table_for(model).last
20
+ end
21
+
22
+ def find_one(model:, where: [], select: nil, join: nil)
23
+ find_many(model: model, where: where, select: select, join: join, limit: 1).first
24
+ end
25
+
26
+ def find_many(model:, where: [], sort_by: nil, limit: nil, offset: nil, select: nil, join: nil)
27
+ model = model.to_s
28
+ records = table_for(model).select { |record| matches_where?(record, where || []) }.map(&:dup)
29
+ records = records.map { |record| apply_join(model, record, join) } if join
30
+ records = sort_records(model, records, sort_by) if sort_by
31
+ records = records.drop(offset.to_i) if offset
32
+ records = records.first(limit.to_i) if limit
33
+ records = records.map { |record| select_fields(model, record, select) } if select && !select.empty?
34
+ records
35
+ end
36
+
37
+ def update(model:, where:, update:)
38
+ records = table_for(model).select { |record| matches_where?(record, where || []) }
39
+ data = transform_input(model.to_s, update, "update", true)
40
+ records.each { |record| record.merge!(data) }
41
+ records.first
42
+ end
43
+
44
+ def update_many(model:, where:, update:)
45
+ records = table_for(model).select { |record| matches_where?(record, where || []) }
46
+ data = transform_input(model.to_s, update, "update", true)
47
+ records.each { |record| record.merge!(data) }
48
+ records.first
49
+ end
50
+
51
+ def delete(model:, where:)
52
+ delete_many(model: model, where: where)
53
+ nil
54
+ end
55
+
56
+ def delete_many(model:, where:)
57
+ table = table_for(model)
58
+ matches = table.select { |record| matches_where?(record, where || []) }
59
+ @db[model.to_s] = table.reject { |record| matches.include?(record) }
60
+ matches.length
61
+ end
62
+
63
+ def count(model:, where: nil)
64
+ find_many(model: model, where: where || []).length
65
+ end
66
+
67
+ def transaction
68
+ snapshot = Marshal.load(Marshal.dump(db))
69
+ yield self
70
+ rescue
71
+ @db = snapshot
72
+ raise
73
+ end
74
+
75
+ private
76
+
77
+ def build_db
78
+ Schema.auth_tables(options).keys.to_h { |model| [model, []] }
79
+ end
80
+
81
+ def table_for(model)
82
+ db[model.to_s] ||= []
83
+ end
84
+
85
+ def transform_input(model, data, action, force_allow_id)
86
+ fields = Schema.auth_tables(options).fetch(model).fetch(:fields)
87
+ input = stringify_keys(data)
88
+ output = {}
89
+
90
+ fields.each do |field, attributes|
91
+ next if field == "id" && input.key?(field) && !force_allow_id
92
+
93
+ value_provided = input.key?(field)
94
+ value = input[field]
95
+
96
+ if value_provided && attributes[:input] == false && value && !force_allow_id
97
+ raise APIError.new("BAD_REQUEST", message: "#{field} is not allowed to be set")
98
+ end
99
+
100
+ if !value_provided && action == "create" && attributes.key?(:default_value)
101
+ value = resolve_default(attributes[:default_value])
102
+ value_provided = true
103
+ elsif !value_provided && action == "update" && attributes[:on_update]
104
+ value = resolve_default(attributes[:on_update])
105
+ value_provided = true
106
+ end
107
+
108
+ if !value_provided && action == "create" && attributes[:required]
109
+ raise APIError.new("BAD_REQUEST", message: "#{field} is required") unless field == "id"
110
+ end
111
+
112
+ output[field] = coerce_value(value, attributes) if value_provided
113
+ end
114
+
115
+ output["id"] = generated_id if action == "create" && !output.key?("id")
116
+ output
117
+ end
118
+
119
+ def generated_id
120
+ generator = options.advanced.dig(:database, :generate_id)
121
+ return generator.call.to_s if generator.respond_to?(:call)
122
+ return SecureRandom.uuid if generator == "uuid"
123
+
124
+ SecureRandom.hex(16)
125
+ end
126
+
127
+ def resolve_default(default)
128
+ default.respond_to?(:call) ? default.call : default
129
+ end
130
+
131
+ def coerce_value(value, attributes)
132
+ return value if value.nil?
133
+ return Time.parse(value) if attributes[:type] == "date" && value.is_a?(String)
134
+
135
+ value
136
+ end
137
+
138
+ def matches_where?(record, where)
139
+ clauses = Array(where)
140
+ return true if clauses.empty?
141
+
142
+ result = evaluate_clause(record, clauses.first)
143
+ clauses.each do |clause|
144
+ clause_result = evaluate_clause(record, clause)
145
+ if fetch_key(clause, :connector).to_s.upcase == "OR"
146
+ result ||= clause_result
147
+ else
148
+ result &&= clause_result
149
+ end
150
+ end
151
+ result
152
+ end
153
+
154
+ def evaluate_clause(record, clause)
155
+ field = Schema.storage_key(fetch_key(clause, :field))
156
+ value = fetch_key(clause, :value)
157
+ operator = (fetch_key(clause, :operator) || "eq").to_s
158
+ current = record[field]
159
+
160
+ case operator
161
+ when "in"
162
+ Array(value).include?(current)
163
+ when "not_in"
164
+ !Array(value).include?(current)
165
+ when "contains"
166
+ current.to_s.include?(value.to_s)
167
+ when "starts_with"
168
+ current.to_s.start_with?(value.to_s)
169
+ when "ends_with"
170
+ current.to_s.end_with?(value.to_s)
171
+ when "ne"
172
+ current != value
173
+ when "gt"
174
+ !value.nil? && current > value
175
+ when "gte"
176
+ !value.nil? && current >= value
177
+ when "lt"
178
+ !value.nil? && current < value
179
+ when "lte"
180
+ !value.nil? && current <= value
181
+ else
182
+ current == value
183
+ end
184
+ end
185
+
186
+ def sort_records(model, records, sort_by)
187
+ field = Schema.storage_key(fetch_key(sort_by, :field))
188
+ direction = fetch_key(sort_by, :direction).to_s
189
+ records.sort_by { |record| sortable_value(record[field]) }.then do |sorted|
190
+ if direction == "desc"
191
+ sorted.reverse
192
+ else
193
+ sorted
194
+ end
195
+ end
196
+ end
197
+
198
+ def sortable_value(value)
199
+ value.nil? ? "" : value
200
+ end
201
+
202
+ def select_fields(_model, record, select)
203
+ fields = Array(select).map { |field| Schema.storage_key(field) }
204
+ record.slice(*fields)
205
+ end
206
+
207
+ def apply_join(model, record, join)
208
+ joined = record.dup
209
+ join.each_key do |join_model|
210
+ join_model = join_model.to_s
211
+ joined[join_model] = case [model, join_model]
212
+ when ["session", "user"], ["account", "user"]
213
+ table_for("user").find { |user| user["id"] == record["userId"] }
214
+ when ["user", "account"]
215
+ table_for("account").select { |account| account["userId"] == record["id"] }
216
+ end
217
+ end
218
+ joined
219
+ end
220
+
221
+ def stringify_keys(data)
222
+ data.each_with_object({}) do |(key, value), result|
223
+ result[Schema.storage_key(key)] = value
224
+ end
225
+ end
226
+
227
+ def fetch_key(hash, key)
228
+ [key, key.to_s, Schema.storage_key(key), Schema.storage_key(key).to_sym].each do |candidate|
229
+ return hash[candidate] if hash.key?(candidate)
230
+ end
231
+ nil
232
+ end
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "better_auth/mongo_adapter"
5
+ rescue LoadError => error
6
+ raise if error.path && error.path != "better_auth/mongo_adapter"
7
+
8
+ raise LoadError, "BetterAuth::Adapters::MongoDB requires the better_auth-mongo-adapter gem. Add `gem \"better_auth-mongo-adapter\"` and `require \"better_auth/mongo_adapter\"`."
9
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Adapters
5
+ class MSSQL < SQL
6
+ attr_reader :url
7
+
8
+ def initialize(options = nil, url: nil, connection: nil)
9
+ unless connection
10
+ require "sequel"
11
+ require "tiny_tds"
12
+ end
13
+
14
+ config = options || Configuration.new(secret: Configuration::DEFAULT_SECRET, database: :memory)
15
+ @url = url
16
+ super(config, connection: connection || Sequel.connect(url), dialect: :mssql)
17
+ end
18
+
19
+ def transaction
20
+ return super unless connection.respond_to?(:transaction)
21
+
22
+ connection.transaction { yield self }
23
+ end
24
+
25
+ private
26
+
27
+ def execute(sql, params)
28
+ if connection.respond_to?(:fetch)
29
+ connection.fetch(sql, *params).all.map { |row| stringify_row(row) }
30
+ else
31
+ super
32
+ end
33
+ end
34
+
35
+ def stringify_row(row)
36
+ row.each_with_object({}) do |(key, value), result|
37
+ result[key.to_s] = value
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module BetterAuth
6
+ module Adapters
7
+ class MySQL < SQL
8
+ attr_reader :url
9
+
10
+ def initialize(options = nil, url: nil, connection: nil)
11
+ require "mysql2" unless connection
12
+
13
+ config = options || Configuration.new(secret: Configuration::DEFAULT_SECRET, database: :memory)
14
+ @url = url
15
+ super(config, connection: connection || Mysql2::Client.new(mysql_options(url)), dialect: :mysql)
16
+ end
17
+
18
+ private
19
+
20
+ def mysql_options(url)
21
+ uri = URI.parse(url.to_s)
22
+ {
23
+ host: uri.host,
24
+ port: uri.port || 3306,
25
+ username: URI.decode_www_form_component(uri.user.to_s),
26
+ password: URI.decode_www_form_component(uri.password.to_s),
27
+ database: uri.path.to_s.delete_prefix("/"),
28
+ symbolize_keys: false
29
+ }.compact
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BetterAuth
4
+ module Adapters
5
+ class Postgres < SQL
6
+ attr_reader :url
7
+
8
+ def initialize(options = nil, url: nil, connection: nil)
9
+ require "pg" unless connection
10
+
11
+ config = options || Configuration.new(secret: Configuration::DEFAULT_SECRET, database: :memory)
12
+ @url = url
13
+ super(config, connection: connection || PG.connect(url), dialect: :postgres)
14
+ end
15
+ end
16
+ end
17
+ end