better_auth-scim 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: 26a7c3c25bebca9f5019560372ecfb0a0c735527fcd04e7da74b75875ce85114
4
- data.tar.gz: e1ec7062829cc935822883aab00490d8dfd33b44b6e8f3ad0a43933a684d2c69
3
+ metadata.gz: 8ffe12400df4278b53ac2c8673635a6db362e875fb85c3d268e079a97b2a0568
4
+ data.tar.gz: d52b222ff8315e95df2df47922acc44e4eaa625a9df903d472a55d63c9130e90
5
5
  SHA512:
6
- metadata.gz: 3bd60c706aeab0fde80fcd123b5e3f9aa20d6d90c3cb5cabcc688503e7b810524a12f5a140f780da9d593bc61e27292abc25d00b2b5c758d829cb623012da2aa
7
- data.tar.gz: 0b741fd545a206c5a38b906ed7c8640bee7859cfce090708e8256062d791deb0a7017ae52f36554ae56827dbd7f513727f632b7e00b9f4b633586f4a7ef7a821
6
+ metadata.gz: bf07cda2658e52e8f3bc61164298df2b785f81ed1227e66ae2e03c9217e9d426fddc8f4d487c68f8212d6995d5f33a82efbf4f96a973116ddc4cd518bf5e7996
7
+ data.tar.gz: 4736ac7244374c48111234fb9b210b6b54e5330bc3f1f1063e34e2397cd08c544f03f8f33f5084b48a77fe6800030ba27d6b976568adca8be1f63678991de44f
data/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.10.0 - 2026-05-21
6
+
7
+ - Improved SCIM route, adapter, user, and rate-limit coverage.
8
+ - Clarified SCIM setup docs and runtime dependencies.
9
+
5
10
  ## 0.7.0 - 2026-05-05
6
11
 
7
12
  - Changed generated SCIM provider tokens to use hashed storage by default. Set `store_scim_token: "plain"` only when plaintext database storage is intentionally required.
data/README.md CHANGED
@@ -14,9 +14,7 @@ require "better_auth/scim"
14
14
 
15
15
  BetterAuth.auth(
16
16
  plugins: [
17
- BetterAuth::Plugins.scim(
18
- provider_ownership: { enabled: true }
19
- )
17
+ BetterAuth::Plugins.scim
20
18
  ]
21
19
  )
22
20
  ```
@@ -42,6 +40,17 @@ Implemented API methods include token generation, provider connection management
42
40
  Options use Ruby snake_case names: `store_scim_token`, `default_scim`, `provider_ownership`, `required_role`, `before_scim_token_generated`, and `after_scim_token_generated`.
43
41
  `store_scim_token` defaults to `"hashed"` so generated SCIM provider tokens are
44
42
  not stored in plaintext.
43
+ `provider_ownership` defaults to `{ enabled: true }`, a Ruby-specific security
44
+ default that scopes personal SCIM provider management to the user who generated
45
+ the provider token. To preserve legacy shared management of non-organization
46
+ providers, configure `provider_ownership: { enabled: false }`; only use that mode
47
+ when all authenticated users who can access SCIM management routes are trusted to
48
+ view, rotate, or delete unowned personal providers.
49
+
50
+ Organization-scoped SCIM bearer tokens must include the organization component
51
+ generated by `generate_scim_token`. Removing that component invalidates the token.
52
+ Deleting a SCIM user unlinks the current provider account first; the underlying
53
+ Better Auth user is deleted only when no other accounts remain.
45
54
 
46
55
  The plugin exposes upstream-style surface metadata:
47
56
 
@@ -52,3 +61,23 @@ The plugin exposes upstream-style surface metadata:
52
61
  ## Production recommendations
53
62
 
54
63
  - In the accounts table (`accounts` or the configured table name), use a unique composite index on `(providerId, accountId)` to prevent duplicate SCIM accounts under concurrent provisioning. The gem does not create this constraint automatically because index syntax and migrations depend on your database adapter and application.
64
+
65
+ ## Testing
66
+
67
+ Run the default SCIM package suite from this package directory:
68
+
69
+ ```sh
70
+ rbenv exec bundle exec rake test
71
+ ```
72
+
73
+ The suite includes SCIM route coverage for memory, custom, secondary-storage,
74
+ and database-backed rate limiting. It also runs a live SQLite adapter smoke test
75
+ against a temporary database.
76
+
77
+ External adapter smoke tests are skip-gated. They run only when the relevant gem
78
+ and service are available:
79
+
80
+ - PostgreSQL: `BETTER_AUTH_POSTGRES_URL`
81
+ - MySQL: `BETTER_AUTH_MYSQL_HOST`, `BETTER_AUTH_MYSQL_PORT`, `BETTER_AUTH_MYSQL_USER`, `BETTER_AUTH_MYSQL_PASSWORD`, `BETTER_AUTH_MYSQL_DATABASE`
82
+ - MSSQL: `BETTER_AUTH_MSSQL_URL`, `BETTER_AUTH_MSSQL_MASTER_URL`
83
+ - MongoDB: `BETTER_AUTH_MONGODB_URL`
@@ -24,7 +24,7 @@ module BetterAuth
24
24
  singleton_class.remove_method(:scim) if singleton_class.method_defined?(:scim) || singleton_class.private_method_defined?(:scim)
25
25
 
26
26
  def scim(options = {})
27
- config = {store_scim_token: "hashed"}.merge(normalize_hash(options))
27
+ config = {store_scim_token: "hashed", provider_ownership: {enabled: true}}.merge(normalize_hash(options))
28
28
  Plugin.new(
29
29
  id: "scim",
30
30
  version: BetterAuth::SCIM::VERSION,
@@ -61,6 +61,7 @@ module BetterAuth
61
61
 
62
62
  {
63
63
  scimProvider: {
64
+ model_name: "scim_providers",
64
65
  fields: scim_provider_fields
65
66
  }
66
67
  }
@@ -5,9 +5,10 @@ module BetterAuth
5
5
  module_function
6
6
 
7
7
  def scim_user_update(body)
8
+ email = scim_primary_email(body)&.downcase
8
9
  {
9
- email: scim_primary_email(body)&.downcase,
10
- name: scim_display_name(body, body[:user_name].to_s),
10
+ email: email,
11
+ name: scim_display_name(body, email),
11
12
  updatedAt: Time.now
12
13
  }.compact
13
14
  end
@@ -23,6 +23,7 @@ module BetterAuth
23
23
  where: [{field: "providerId", value: provider_id}].tap { |where| where << {field: "organizationId", value: organization_id} if organization_id }
24
24
  )
25
25
  raise scim_error("UNAUTHORIZED", "Invalid SCIM token") unless provider
26
+ raise scim_error("UNAUTHORIZED", "Invalid SCIM token") unless provider["organizationId"].to_s == organization_id.to_s
26
27
  raise scim_error("UNAUTHORIZED", "Invalid SCIM token") unless scim_token_matches?(ctx, config, token, provider.fetch("scimToken"))
27
28
  end
28
29
 
@@ -49,7 +49,7 @@ module BetterAuth
49
49
  end
50
50
  end
51
51
 
52
- def scim_assert_provider_access!(ctx, user_id, provider, required_roles)
52
+ def scim_assert_provider_access!(ctx, user_id, provider, required_roles, config = {})
53
53
  return unless provider
54
54
 
55
55
  organization_id = provider["organizationId"]
@@ -59,6 +59,8 @@ module BetterAuth
59
59
  member = scim_find_organization_member(ctx, user_id, organization_id)
60
60
  raise APIError.new("FORBIDDEN", message: "You must be a member of the organization to access this provider") unless member
61
61
  raise APIError.new("FORBIDDEN", message: "Insufficient role for this operation") unless scim_has_required_role?(member.fetch("role", ""), required_roles)
62
+ elsif scim_provider_ownership_enabled?(config)
63
+ raise APIError.new("FORBIDDEN", message: "You must be the owner to access this provider") unless provider["userId"] == user_id
62
64
  elsif provider.key?("userId") && provider["userId"] && provider["userId"] != user_id
63
65
  raise APIError.new("FORBIDDEN", message: "You must be the owner to access this provider")
64
66
  end
@@ -7,7 +7,7 @@ module BetterAuth
7
7
  module_function
8
8
 
9
9
  def scim_generate_token_endpoint(config)
10
- Endpoint.new(path: "/scim/generate-token", method: "POST", metadata: scim_openapi_metadata("Generates a new SCIM token for the given provider")) do |ctx|
10
+ Endpoint.new(path: "/scim/generate-token", method: "POST", metadata: scim_generate_token_openapi_metadata) do |ctx|
11
11
  session = Routes.current_session(ctx)
12
12
  body = normalize_hash(ctx.body)
13
13
  raw_provider_id = body[:provider_id]
@@ -34,7 +34,7 @@ module BetterAuth
34
34
 
35
35
  existing = ctx.context.adapter.find_one(model: "scimProvider", where: [{field: "providerId", value: provider_id}])
36
36
  if existing
37
- scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), existing, required_roles)
37
+ scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), existing, required_roles, config)
38
38
  ctx.context.adapter.delete(model: "scimProvider", where: [{field: "id", value: existing.fetch("id")}])
39
39
  end
40
40
 
@@ -61,6 +61,8 @@ module BetterAuth
61
61
  if organization_id
62
62
  member = org_memberships[organization_id]
63
63
  member && scim_has_required_role?(member.fetch("role", ""), required_roles)
64
+ elsif scim_provider_ownership_enabled?(config)
65
+ provider["userId"] == user_id
64
66
  else
65
67
  !provider.key?("userId") || provider["userId"].nil? || provider["userId"] == user_id
66
68
  end
@@ -73,17 +75,17 @@ module BetterAuth
73
75
  Endpoint.new(path: "/scim/get-provider-connection", method: "GET", metadata: scim_openapi_metadata("Get SCIM provider connection.")) do |ctx|
74
76
  session = Routes.current_session(ctx)
75
77
  provider = scim_provider_by_provider_id!(ctx, scim_provider_id_query(ctx))
76
- scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), provider, scim_required_roles(ctx, config))
78
+ scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), provider, scim_required_roles(ctx, config), config)
77
79
  ctx.json(scim_normalized_provider(provider))
78
80
  end
79
81
  end
80
82
 
81
83
  def scim_delete_provider_connection_endpoint(config)
82
- Endpoint.new(path: "/scim/delete-provider-connection", method: "POST", metadata: scim_openapi_metadata("Delete SCIM provider connection.")) do |ctx|
84
+ Endpoint.new(path: "/scim/delete-provider-connection", method: "POST", metadata: scim_delete_provider_openapi_metadata) do |ctx|
83
85
  session = Routes.current_session(ctx)
84
86
  body = normalize_hash(ctx.body)
85
87
  provider = scim_provider_by_provider_id!(ctx, body[:provider_id])
86
- scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), provider, scim_required_roles(ctx, config))
88
+ scim_assert_provider_access!(ctx, session.fetch(:user).fetch("id"), provider, scim_required_roles(ctx, config), config)
87
89
  ctx.context.adapter.delete(model: "scimProvider", where: [{field: "providerId", value: provider.fetch("providerId")}])
88
90
  ctx.json({success: true})
89
91
  end
@@ -168,8 +170,12 @@ module BetterAuth
168
170
 
169
171
  def scim_delete_user_endpoint(config)
170
172
  Endpoint.new(path: "/scim/v2/Users/:userId", method: "DELETE", metadata: scim_hidden_metadata("Delete SCIM user.", [*SCIM_SUPPORTED_MEDIA_TYPES, ""]), use: [scim_auth_middleware(config)]) do |ctx|
171
- user, = scim_find_user_with_account!(ctx)
172
- ctx.context.internal_adapter.delete_user(user.fetch("id"))
173
+ user, account = scim_find_user_with_account!(ctx)
174
+ ctx.context.adapter.transaction do
175
+ ctx.context.internal_adapter.delete_account(account.fetch("id"))
176
+ remaining_accounts = ctx.context.internal_adapter.find_accounts(user.fetch("id"))
177
+ ctx.context.internal_adapter.delete_user(user.fetch("id")) if remaining_accounts.empty?
178
+ end
173
179
  ctx.json(nil, status: 204)
174
180
  end
175
181
  end
@@ -177,9 +183,14 @@ module BetterAuth
177
183
  def scim_list_users_endpoint(config)
178
184
  Endpoint.new(path: "/scim/v2/Users", method: "GET", metadata: scim_hidden_metadata("List SCIM users.", SCIM_SUPPORTED_MEDIA_TYPES), use: [scim_auth_middleware(config)]) do |ctx|
179
185
  provider = ctx.context.scim_provider
186
+ filter_value = nil
187
+ if ctx.query[:filter] || ctx.query["filter"]
188
+ _filter_field, filter_value = scim_parse_filter(ctx.query[:filter] || ctx.query["filter"])
189
+ end
190
+ start_index, count = scim_list_pagination(ctx.query)
191
+ empty_list = {schemas: [SCIM_LIST_RESPONSE_SCHEMA], totalResults: 0, itemsPerPage: 0, startIndex: start_index, Resources: []}
180
192
  accounts = ctx.context.adapter.find_many(model: "account", where: [{field: "providerId", value: provider.fetch("providerId")}])
181
193
  account_user_ids = accounts.map { |account| account.fetch("userId") }.uniq
182
- empty_list = {schemas: [SCIM_LIST_RESPONSE_SCHEMA], totalResults: 0, itemsPerPage: 0, startIndex: 1, Resources: []}
183
194
  if account_user_ids.empty?
184
195
  ctx.json(empty_list)
185
196
  else
@@ -198,15 +209,22 @@ module BetterAuth
198
209
  ctx.json(empty_list)
199
210
  else
200
211
  where = [{field: "id", value: user_ids, operator: "in"}]
201
- if ctx.query[:filter] || ctx.query["filter"]
202
- _filter_field, filter_value = scim_parse_filter(ctx.query[:filter] || ctx.query["filter"])
212
+ if filter_value
203
213
  where << {field: "email", value: filter_value.to_s.downcase, operator: "eq"}
204
214
  end
205
215
 
206
- users = ctx.context.internal_adapter.list_users(where: where, sort_by: {field: "email", direction: "asc"})
216
+ total_results = ctx.context.internal_adapter.count_total_users(where: where)
217
+ offset = start_index - 1
218
+ limit = count || ((offset > 0) ? [total_results - offset, 0].max : nil)
219
+ users = ctx.context.internal_adapter.list_users(
220
+ where: where,
221
+ sort_by: {field: "email", direction: "asc"},
222
+ limit: limit,
223
+ offset: offset.positive? ? offset : nil
224
+ )
207
225
  accounts_by_user = accounts.each_with_object({}) { |account, result| result[account.fetch("userId")] ||= account }
208
226
  resources = users.map { |user| scim_user_resource(user, accounts_by_user[user.fetch("id")], ctx.context.base_url) }
209
- ctx.json({schemas: [SCIM_LIST_RESPONSE_SCHEMA], totalResults: resources.length, itemsPerPage: resources.length, startIndex: 1, Resources: resources})
227
+ ctx.json({schemas: [SCIM_LIST_RESPONSE_SCHEMA], totalResults: total_results, itemsPerPage: resources.length, startIndex: start_index, Resources: resources})
210
228
  end
211
229
  end
212
230
  end
@@ -293,5 +311,27 @@ module BetterAuth
293
311
 
294
312
  [user, account]
295
313
  end
314
+
315
+ def scim_list_pagination(query)
316
+ start_index = scim_positive_integer(query[:startIndex] || query[:start_index] || query["startIndex"] || query["start_index"], 1)
317
+ count = if query.key?(:count) || query.key?("count")
318
+ scim_non_negative_integer(query[:count] || query["count"], 0)
319
+ end
320
+ [start_index, count]
321
+ end
322
+
323
+ def scim_positive_integer(value, fallback)
324
+ parsed = Integer(value)
325
+ parsed.positive? ? parsed : fallback
326
+ rescue ArgumentError, TypeError
327
+ fallback
328
+ end
329
+
330
+ def scim_non_negative_integer(value, fallback)
331
+ parsed = Integer(value)
332
+ parsed.negative? ? fallback : parsed
333
+ rescue ArgumentError, TypeError
334
+ fallback
335
+ end
296
336
  end
297
337
  end
@@ -6,7 +6,7 @@ module BetterAuth
6
6
 
7
7
  def scim_parse_filter(filter)
8
8
  match = filter.to_s.match(/\A\s*([^\s]+)\s+(eq|ne|co|sw|ew|pr)\s*(?:"([^"]*)"|([^\s]+))?\s*\z/i)
9
- raise scim_error("BAD_REQUEST", "Invalid SCIM filter", scim_type: "invalidFilter") unless match
9
+ raise scim_error("BAD_REQUEST", "Invalid filter expression", scim_type: "invalidFilter") unless match
10
10
 
11
11
  field = match[1]
12
12
  operator = match[2].downcase
@@ -29,6 +29,48 @@ module BetterAuth
29
29
  }
30
30
  end
31
31
 
32
+ def scim_generate_token_openapi_metadata
33
+ {
34
+ openapi: {
35
+ summary: "Generates a new SCIM token for the given provider",
36
+ requestBody: OpenAPI.json_request_body(
37
+ OpenAPI.object_schema(
38
+ {
39
+ provider_id: {type: "string", description: "SCIM provider identifier"},
40
+ organization_id: {type: "string", description: "Organization ID to restrict the SCIM token to"}
41
+ },
42
+ required: ["provider_id"]
43
+ )
44
+ ),
45
+ responses: scim_openapi_responses.merge(
46
+ "201" => OpenAPI.json_response(
47
+ "SCIM token generated",
48
+ OpenAPI.object_schema({scimToken: {type: "string"}}, required: ["scimToken"])
49
+ )
50
+ )
51
+ }
52
+ }
53
+ end
54
+
55
+ def scim_delete_provider_openapi_metadata
56
+ {
57
+ openapi: {
58
+ summary: "Delete SCIM provider connection.",
59
+ requestBody: OpenAPI.json_request_body(
60
+ OpenAPI.object_schema(
61
+ {
62
+ provider_id: {type: "string", description: "SCIM provider identifier"}
63
+ },
64
+ required: ["provider_id"]
65
+ )
66
+ ),
67
+ responses: scim_openapi_responses.merge(
68
+ "200" => OpenAPI.json_response("SCIM provider connection deleted", OpenAPI.success_response_schema)
69
+ )
70
+ }
71
+ }
72
+ end
73
+
32
74
  def scim_openapi_responses
33
75
  {
34
76
  "200" => {description: "Success"},
@@ -48,7 +48,7 @@ module BetterAuth
48
48
  def scim_default_provider(config, provider_id, organization_id)
49
49
  Array(config[:default_scim]).find do |provider|
50
50
  candidate = normalize_hash(provider)
51
- next true if candidate[:provider_id].to_s == provider_id.to_s && organization_id.to_s.empty?
51
+ next true if candidate[:provider_id].to_s == provider_id.to_s && organization_id.to_s.empty? && candidate[:organization_id].to_s.empty?
52
52
 
53
53
  candidate[:provider_id].to_s == provider_id.to_s &&
54
54
  !organization_id.to_s.empty? &&
@@ -4,6 +4,9 @@ module BetterAuth
4
4
  module Plugins
5
5
  module_function
6
6
 
7
+ SCIM_MAX_PATCH_OPERATIONS = 100
8
+ SCIM_MAX_PATCH_VALUE_DEPTH = 5
9
+
7
10
  def scim_validate_user_body!(body)
8
11
  raise scim_error("BAD_REQUEST", BASE_ERROR_CODES["VALIDATION_ERROR"]) unless body[:user_name].is_a?(String)
9
12
  raise scim_error("BAD_REQUEST", BASE_ERROR_CODES["VALIDATION_ERROR"]) if body[:user_name].empty?
@@ -26,8 +29,13 @@ module BetterAuth
26
29
  schemas = Array(body[:schemas])
27
30
  raise scim_error("BAD_REQUEST", "Invalid schemas for PatchOp") unless schemas.include?("urn:ietf:params:scim:api:messages:2.0:PatchOp")
28
31
 
29
- Array(body[:operations]).each_with_index do |operation, index|
30
- op = normalize_hash(operation)[:op]
32
+ operations = body[:operations]
33
+ raise scim_error("BAD_REQUEST", BASE_ERROR_CODES["VALIDATION_ERROR"]) unless operations.is_a?(Array)
34
+ raise scim_error("BAD_REQUEST", "Too many SCIM patch operations") if operations.length > SCIM_MAX_PATCH_OPERATIONS
35
+
36
+ operations.each_with_index do |operation, index|
37
+ normalized = normalize_hash(operation)
38
+ op = normalized[:op]
31
39
  next if op.nil? || op.to_s.empty?
32
40
 
33
41
  unless op.is_a?(String)
@@ -38,14 +46,19 @@ module BetterAuth
38
46
 
39
47
  raise scim_patch_validation_error("[body.Operations.#{index}.op] Invalid option: expected one of \"replace\"|\"add\"|\"remove\"")
40
48
  end
49
+
50
+ operations.each { |operation| scim_validate_patch_value_depth!(normalize_hash(operation)[:value]) }
51
+ end
52
+
53
+ def scim_validate_patch_value_depth!(value, depth = 0)
54
+ return unless value.is_a?(Hash)
55
+ raise scim_error("BAD_REQUEST", "SCIM patch value is too deeply nested") if depth > SCIM_MAX_PATCH_VALUE_DEPTH
56
+
57
+ value.each_value { |nested| scim_validate_patch_value_depth!(nested, depth + 1) }
41
58
  end
42
59
 
43
60
  def scim_patch_validation_error(message)
44
- APIError.new(
45
- "BAD_REQUEST",
46
- message: BASE_ERROR_CODES["VALIDATION_ERROR"],
47
- body: {code: "VALIDATION_ERROR", message: message}
48
- )
61
+ scim_error("BAD_REQUEST", message)
49
62
  end
50
63
  end
51
64
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BetterAuth
4
4
  module SCIM
5
- VERSION = "0.8.0"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: better_auth-scim
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
@@ -85,6 +85,20 @@ dependencies:
85
85
  - - "~>"
86
86
  - !ruby/object:Gem::Version
87
87
  version: '13.2'
88
+ - !ruby/object:Gem::Dependency
89
+ name: sqlite3
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '2.0'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '2.0'
88
102
  - !ruby/object:Gem::Dependency
89
103
  name: standardrb
90
104
  requirement: !ruby/object:Gem::Requirement
@@ -127,14 +141,14 @@ files:
127
141
  - lib/better_auth/scim/utils.rb
128
142
  - lib/better_auth/scim/validation.rb
129
143
  - lib/better_auth/scim/version.rb
130
- homepage: https://github.com/sebasxsala/better-auth
144
+ homepage: https://github.com/sebasxsala/better-auth-rb
131
145
  licenses:
132
146
  - MIT
133
147
  metadata:
134
- homepage_uri: https://github.com/sebasxsala/better-auth
135
- source_code_uri: https://github.com/sebasxsala/better-auth
136
- changelog_uri: https://github.com/sebasxsala/better-auth/blob/main/packages/better_auth-scim/CHANGELOG.md
137
- bug_tracker_uri: https://github.com/sebasxsala/better-auth/issues
148
+ homepage_uri: https://github.com/sebasxsala/better-auth-rb
149
+ source_code_uri: https://github.com/sebasxsala/better-auth-rb
150
+ changelog_uri: https://github.com/sebasxsala/better-auth-rb/blob/main/packages/better_auth-scim/CHANGELOG.md
151
+ bug_tracker_uri: https://github.com/sebasxsala/better-auth-rb/issues
138
152
  rdoc_options: []
139
153
  require_paths:
140
154
  - lib