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
@@ -156,7 +156,7 @@ module BetterAuth
156
156
  end
157
157
 
158
158
  def organization_create_endpoint(config)
159
- Endpoint.new(path: "/organization/create", method: "POST") do |ctx|
159
+ Endpoint.new(path: "/organization/create", method: "POST", metadata: organization_openapi("createOrganization", "Create an organization", response: organization_ref_schema("Organization"))) do |ctx|
160
160
  body = normalize_hash(ctx.body)
161
161
  session = Routes.current_session(ctx, allow_nil: true)
162
162
  user = session ? session[:user] : ctx.context.internal_adapter.find_user_by_id(body[:user_id])
@@ -204,7 +204,8 @@ module BetterAuth
204
204
  end
205
205
 
206
206
  def organization_check_slug_endpoint
207
- Endpoint.new(path: "/organization/check-slug", method: "POST") do |ctx|
207
+ Endpoint.new(path: "/organization/check-slug", method: "POST", metadata: organization_openapi("checkOrganizationSlug", "Check if an organization slug is available", response: OpenAPI.status_response_schema)) do |ctx|
208
+ Routes.request_only_session(ctx)
208
209
  slug = normalize_hash(ctx.body)[:slug].to_s
209
210
  if slug.empty? || organization_by_slug(ctx, slug)
210
211
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("ORGANIZATION_SLUG_ALREADY_TAKEN"))
@@ -214,7 +215,7 @@ module BetterAuth
214
215
  end
215
216
 
216
217
  def organization_list_endpoint
217
- Endpoint.new(path: "/organization/list", method: "GET") do |ctx|
218
+ Endpoint.new(path: "/organization/list", method: "GET", metadata: organization_openapi("listOrganizations", "List organizations", response: organization_array_schema("Organization"))) do |ctx|
218
219
  session = Routes.current_session(ctx)
219
220
  members = ctx.context.adapter.find_many(model: "member", where: [{field: "userId", value: session[:user]["id"]}])
220
221
  organizations = members.filter_map { |member| organization_by_id(ctx, member["organizationId"]) }
@@ -223,7 +224,7 @@ module BetterAuth
223
224
  end
224
225
 
225
226
  def organization_update_endpoint(config)
226
- Endpoint.new(path: "/organization/update", method: "POST") do |ctx|
227
+ Endpoint.new(path: "/organization/update", method: "POST", metadata: organization_openapi("updateOrganization", "Update an organization", response: organization_ref_schema("Organization"))) do |ctx|
227
228
  session = Routes.current_session(ctx)
228
229
  body = normalize_hash(ctx.body)
229
230
  id = body[:organization_id] || body[:organizationId]
@@ -248,7 +249,7 @@ module BetterAuth
248
249
  end
249
250
 
250
251
  def organization_delete_endpoint(config)
251
- Endpoint.new(path: "/organization/delete", method: "POST") do |ctx|
252
+ Endpoint.new(path: "/organization/delete", method: "POST", metadata: organization_openapi("deleteOrganization", "Delete an organization", response: OpenAPI.status_response_schema)) do |ctx|
252
253
  session = Routes.current_session(ctx)
253
254
  body = normalize_hash(ctx.body)
254
255
  organization = organization_by_id(ctx, body[:organization_id]) || organization_by_slug(ctx, body[:organization_slug])
@@ -269,7 +270,7 @@ module BetterAuth
269
270
  end
270
271
 
271
272
  def organization_set_active_endpoint
272
- Endpoint.new(path: "/organization/set-active", method: "POST") do |ctx|
273
+ Endpoint.new(path: "/organization/set-active", method: "POST", metadata: organization_openapi("setActiveOrganization", "Set the active organization", response: organization_nullable_schema("Organization"))) do |ctx|
273
274
  session = Routes.current_session(ctx, sensitive: true)
274
275
  body = normalize_hash(ctx.body)
275
276
  if body.key?(:organization_id) && body[:organization_id].nil?
@@ -288,7 +289,7 @@ module BetterAuth
288
289
  end
289
290
 
290
291
  def organization_get_full_endpoint(config)
291
- Endpoint.new(path: "/organization/get-full-organization", method: "GET") do |ctx|
292
+ Endpoint.new(path: "/organization/get-full-organization", method: "GET", metadata: organization_openapi("getOrganization", "Get the full organization", response: organization_nullable_schema("Organization"))) do |ctx|
292
293
  session = Routes.current_session(ctx)
293
294
  query = normalize_hash(ctx.query)
294
295
  explicit_lookup = query.key?(:organization_slug) || query.key?(:organization_id)
@@ -315,7 +316,7 @@ module BetterAuth
315
316
  end
316
317
 
317
318
  def organization_invite_endpoint(config)
318
- Endpoint.new(path: "/organization/invite-member", method: "POST") do |ctx|
319
+ Endpoint.new(path: "/organization/invite-member", method: "POST", metadata: organization_openapi("createOrganizationInvitation", "Create an organization invitation", response: organization_ref_schema("Invitation"))) do |ctx|
319
320
  session = Routes.current_session(ctx)
320
321
  body = normalize_hash(ctx.body)
321
322
  organization = organization_by_id(ctx, body[:organization_id] || session[:session]["activeOrganizationId"]) || organization_by_slug(ctx, body[:organization_slug])
@@ -370,7 +371,7 @@ module BetterAuth
370
371
  end
371
372
 
372
373
  def organization_accept_invitation_endpoint(config)
373
- Endpoint.new(path: "/organization/accept-invitation", method: "POST") do |ctx|
374
+ Endpoint.new(path: "/organization/accept-invitation", method: "POST", metadata: organization_openapi("acceptOrganizationInvitation", "Accept an organization invitation", response: organization_accept_invitation_schema)) do |ctx|
374
375
  session = Routes.current_session(ctx)
375
376
  body = normalize_hash(ctx.body)
376
377
  invitation = invitation_by_id(ctx, body[:invitation_id] || body[:id])
@@ -394,7 +395,7 @@ module BetterAuth
394
395
  end
395
396
 
396
397
  def organization_reject_invitation_endpoint(_config)
397
- Endpoint.new(path: "/organization/reject-invitation", method: "POST") do |ctx|
398
+ Endpoint.new(path: "/organization/reject-invitation", method: "POST", metadata: organization_openapi("rejectOrganizationInvitation", "Reject an organization invitation", response: organization_ref_schema("Invitation"))) do |ctx|
398
399
  session = Routes.current_session(ctx)
399
400
  invitation = invitation_by_id(ctx, normalize_hash(ctx.body)[:invitation_id])
400
401
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
@@ -405,7 +406,7 @@ module BetterAuth
405
406
  end
406
407
 
407
408
  def organization_cancel_invitation_endpoint(config)
408
- Endpoint.new(path: "/organization/cancel-invitation", method: "POST") do |ctx|
409
+ Endpoint.new(path: "/organization/cancel-invitation", method: "POST", metadata: organization_openapi("cancelOrganizationInvitation", "Cancel an organization invitation", response: organization_ref_schema("Invitation"))) do |ctx|
409
410
  session = Routes.current_session(ctx)
410
411
  invitation = invitation_by_id(ctx, normalize_hash(ctx.body)[:invitation_id])
411
412
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
@@ -416,7 +417,7 @@ module BetterAuth
416
417
  end
417
418
 
418
419
  def organization_get_invitation_endpoint
419
- Endpoint.new(path: "/organization/get-invitation", method: "GET") do |ctx|
420
+ Endpoint.new(path: "/organization/get-invitation", method: "GET", metadata: organization_openapi("getOrganizationInvitation", "Get an organization invitation", response: organization_ref_schema("Invitation"))) do |ctx|
420
421
  invitation = invitation_by_id(ctx, normalize_hash(ctx.query)[:id] || normalize_hash(ctx.query)[:invitation_id])
421
422
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("INVITATION_NOT_FOUND")) unless invitation
422
423
  ctx.json(invitation_wire(ctx, invitation))
@@ -424,7 +425,7 @@ module BetterAuth
424
425
  end
425
426
 
426
427
  def organization_list_invitations_endpoint(config)
427
- Endpoint.new(path: "/organization/list-invitations", method: "GET") do |ctx|
428
+ Endpoint.new(path: "/organization/list-invitations", method: "GET", metadata: organization_openapi("listOrganizationInvitations", "List organization invitations", response: organization_array_schema("Invitation"))) do |ctx|
428
429
  session = Routes.current_session(ctx)
429
430
  organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
430
431
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
@@ -435,7 +436,7 @@ module BetterAuth
435
436
  end
436
437
 
437
438
  def organization_list_user_invitations_endpoint
438
- Endpoint.new(path: "/organization/list-user-invitations", method: "GET") do |ctx|
439
+ Endpoint.new(path: "/organization/list-user-invitations", method: "GET", metadata: organization_openapi("listUserInvitations", "List user invitations", response: organization_array_schema("Invitation"))) do |ctx|
439
440
  session = Routes.current_session(ctx)
440
441
  invitations = ctx.context.adapter.find_many(model: "invitation", where: [{field: "email", value: session[:user]["email"].to_s.downcase}, {field: "status", value: "pending"}])
441
442
  ctx.json(invitations.map { |entry| invitation_wire(ctx, entry) })
@@ -443,7 +444,7 @@ module BetterAuth
443
444
  end
444
445
 
445
446
  def organization_add_member_endpoint(config)
446
- Endpoint.new(path: "/organization/add-member", method: "POST") do |ctx|
447
+ Endpoint.new(path: "/organization/add-member", method: "POST", metadata: organization_openapi("addOrganizationMember", "Add an organization member", response: organization_ref_schema("Member"))) do |ctx|
447
448
  session = Routes.current_session(ctx)
448
449
  body = normalize_hash(ctx.body)
449
450
  organization_id = body[:organization_id]
@@ -464,7 +465,7 @@ module BetterAuth
464
465
  end
465
466
 
466
467
  def organization_remove_member_endpoint(config)
467
- Endpoint.new(path: "/organization/remove-member", method: "POST") do |ctx|
468
+ Endpoint.new(path: "/organization/remove-member", method: "POST", metadata: organization_openapi("removeOrganizationMember", "Remove an organization member", response: OpenAPI.status_response_schema)) do |ctx|
468
469
  session = Routes.current_session(ctx)
469
470
  body = normalize_hash(ctx.body)
470
471
  member = member_by_id(ctx, body[:member_id]) || require_member(ctx, body[:user_id], body[:organization_id])
@@ -481,7 +482,7 @@ module BetterAuth
481
482
  end
482
483
 
483
484
  def organization_update_member_role_endpoint(config)
484
- Endpoint.new(path: "/organization/update-member-role", method: "POST") do |ctx|
485
+ Endpoint.new(path: "/organization/update-member-role", method: "POST", metadata: organization_openapi("updateOrganizationMemberRole", "Update an organization member role", response: organization_ref_schema("Member"))) do |ctx|
485
486
  session = Routes.current_session(ctx)
486
487
  body = normalize_hash(ctx.body)
487
488
  member = member_by_id(ctx, body[:member_id]) || require_member(ctx, body[:user_id], body[:organization_id])
@@ -493,7 +494,7 @@ module BetterAuth
493
494
  end
494
495
 
495
496
  def organization_get_active_member_endpoint(_config)
496
- Endpoint.new(path: "/organization/get-active-member", method: "GET") do |ctx|
497
+ Endpoint.new(path: "/organization/get-active-member", method: "GET", metadata: organization_openapi("getActiveOrganizationMember", "Get the active organization member", response: organization_ref_schema("Member"))) do |ctx|
497
498
  session = Routes.current_session(ctx)
498
499
  organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
499
500
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("NO_ACTIVE_ORGANIZATION")) unless organization_id
@@ -503,7 +504,7 @@ module BetterAuth
503
504
  end
504
505
 
505
506
  def organization_get_active_member_role_endpoint(_config)
506
- Endpoint.new(path: "/organization/get-active-member-role", method: "GET") do |ctx|
507
+ Endpoint.new(path: "/organization/get-active-member-role", method: "GET", metadata: organization_openapi("getActiveOrganizationMemberRole", "Get the active organization member role", response: organization_active_member_role_schema)) do |ctx|
507
508
  session = Routes.current_session(ctx)
508
509
  query = normalize_hash(ctx.query)
509
510
  organization_id = query[:organization_id] || session[:session]["activeOrganizationId"]
@@ -515,7 +516,7 @@ module BetterAuth
515
516
  end
516
517
 
517
518
  def organization_leave_endpoint(config)
518
- Endpoint.new(path: "/organization/leave", method: "POST") do |ctx|
519
+ Endpoint.new(path: "/organization/leave", method: "POST", metadata: organization_openapi("leaveOrganization", "Leave an organization", response: OpenAPI.status_response_schema)) do |ctx|
519
520
  session = Routes.current_session(ctx)
520
521
  organization_id = normalize_hash(ctx.body)[:organization_id]
521
522
  member = require_member!(ctx, session[:user]["id"], organization_id)
@@ -527,7 +528,7 @@ module BetterAuth
527
528
  end
528
529
 
529
530
  def organization_list_members_endpoint(_config)
530
- Endpoint.new(path: "/organization/list-members", method: "GET") do |ctx|
531
+ Endpoint.new(path: "/organization/list-members", method: "GET", metadata: organization_openapi("listOrganizationMembers", "List organization members", response: organization_members_response_schema)) do |ctx|
531
532
  session = Routes.current_session(ctx)
532
533
  query = normalize_hash(ctx.query)
533
534
  organization_id = query[:organization_id] || organization_by_slug(ctx, query[:organization_slug])&.fetch("id") || session[:session]["activeOrganizationId"]
@@ -538,7 +539,7 @@ module BetterAuth
538
539
  end
539
540
 
540
541
  def organization_has_permission_endpoint(config)
541
- Endpoint.new(path: "/organization/has-permission", method: "POST") do |ctx|
542
+ Endpoint.new(path: "/organization/has-permission", method: "POST", metadata: organization_openapi("hasOrganizationPermission", "Check if the member has organization permission", response: organization_permission_response_schema)) do |ctx|
542
543
  session = Routes.current_session(ctx)
543
544
  body = normalize_hash(ctx.body)
544
545
  organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
@@ -550,7 +551,7 @@ module BetterAuth
550
551
  end
551
552
 
552
553
  def organization_create_team_endpoint(config)
553
- Endpoint.new(path: "/organization/create-team", method: "POST") do |ctx|
554
+ Endpoint.new(path: "/organization/create-team", method: "POST", metadata: organization_openapi("createOrganizationTeam", "Create an organization team", response: organization_ref_schema("Team"))) do |ctx|
554
555
  session = Routes.current_session(ctx)
555
556
  body = normalize_hash(ctx.body)
556
557
  organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
@@ -570,7 +571,7 @@ module BetterAuth
570
571
  end
571
572
 
572
573
  def organization_list_teams_endpoint(_config)
573
- Endpoint.new(path: "/organization/list-teams", method: "GET") do |ctx|
574
+ Endpoint.new(path: "/organization/list-teams", method: "GET", metadata: organization_openapi("listOrganizationTeams", "List organization teams", response: organization_array_schema("Team"))) do |ctx|
574
575
  session = Routes.current_session(ctx)
575
576
  organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
576
577
  require_member!(ctx, session[:user]["id"], organization_id)
@@ -580,7 +581,7 @@ module BetterAuth
580
581
  end
581
582
 
582
583
  def organization_update_team_endpoint(config)
583
- Endpoint.new(path: "/organization/update-team", method: "POST") do |ctx|
584
+ Endpoint.new(path: "/organization/update-team", method: "POST", metadata: organization_openapi("updateOrganizationTeam", "Update an organization team", response: organization_ref_schema("Team"))) do |ctx|
584
585
  session = Routes.current_session(ctx)
585
586
  body = normalize_hash(ctx.body)
586
587
  team = team_by_id(ctx, body[:team_id])
@@ -592,7 +593,7 @@ module BetterAuth
592
593
  end
593
594
 
594
595
  def organization_remove_team_endpoint(config)
595
- Endpoint.new(path: "/organization/remove-team", method: "POST") do |ctx|
596
+ Endpoint.new(path: "/organization/remove-team", method: "POST", metadata: organization_openapi("removeOrganizationTeam", "Remove an organization team", response: OpenAPI.status_response_schema)) do |ctx|
596
597
  session = Routes.current_session(ctx)
597
598
  team = team_by_id(ctx, normalize_hash(ctx.body)[:team_id])
598
599
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("TEAM_NOT_FOUND")) unless team
@@ -608,7 +609,7 @@ module BetterAuth
608
609
  end
609
610
 
610
611
  def organization_set_active_team_endpoint(_config)
611
- Endpoint.new(path: "/organization/set-active-team", method: "POST") do |ctx|
612
+ Endpoint.new(path: "/organization/set-active-team", method: "POST", metadata: organization_openapi("setActiveOrganizationTeam", "Set the active organization team", response: organization_nullable_schema("Team"))) do |ctx|
612
613
  session = Routes.current_session(ctx)
613
614
  body = normalize_hash(ctx.body)
614
615
  if body.key?(:team_id) && body[:team_id].nil?
@@ -626,7 +627,7 @@ module BetterAuth
626
627
  end
627
628
 
628
629
  def organization_list_user_teams_endpoint
629
- Endpoint.new(path: "/organization/list-user-teams", method: "GET") do |ctx|
630
+ Endpoint.new(path: "/organization/list-user-teams", method: "GET", metadata: organization_openapi("listUserTeams", "List user teams", response: organization_array_schema("Team"))) do |ctx|
630
631
  session = Routes.current_session(ctx)
631
632
  memberships = ctx.context.adapter.find_many(model: "teamMember", where: [{field: "userId", value: session[:user]["id"]}])
632
633
  ctx.json(memberships.filter_map { |entry| team_by_id(ctx, entry["teamId"]) }.map { |team| team_wire(ctx, team) })
@@ -634,7 +635,7 @@ module BetterAuth
634
635
  end
635
636
 
636
637
  def organization_list_team_members_endpoint(_config)
637
- Endpoint.new(path: "/organization/list-team-members", method: "GET") do |ctx|
638
+ Endpoint.new(path: "/organization/list-team-members", method: "GET", metadata: organization_openapi("listTeamMembers", "List team members", response: organization_array_schema("TeamMember"))) do |ctx|
638
639
  session = Routes.current_session(ctx)
639
640
  team_id = normalize_hash(ctx.query)[:team_id] || session[:session]["activeTeamId"]
640
641
  raise APIError.new("BAD_REQUEST", message: ORGANIZATION_ERROR_CODES.fetch("YOU_DO_NOT_HAVE_AN_ACTIVE_TEAM")) unless team_id
@@ -646,7 +647,7 @@ module BetterAuth
646
647
  end
647
648
 
648
649
  def organization_add_team_member_endpoint(config)
649
- Endpoint.new(path: "/organization/add-team-member", method: "POST") do |ctx|
650
+ Endpoint.new(path: "/organization/add-team-member", method: "POST", metadata: organization_openapi("addTeamMember", "Add a team member", response: organization_ref_schema("TeamMember"))) do |ctx|
650
651
  session = Routes.current_session(ctx)
651
652
  body = normalize_hash(ctx.body)
652
653
  team = team_by_id(ctx, body[:team_id])
@@ -667,7 +668,7 @@ module BetterAuth
667
668
  end
668
669
 
669
670
  def organization_remove_team_member_endpoint(config)
670
- Endpoint.new(path: "/organization/remove-team-member", method: "POST") do |ctx|
671
+ Endpoint.new(path: "/organization/remove-team-member", method: "POST", metadata: organization_openapi("removeTeamMember", "Remove a team member", response: OpenAPI.status_response_schema)) do |ctx|
671
672
  session = Routes.current_session(ctx)
672
673
  body = normalize_hash(ctx.body)
673
674
  team = team_by_id(ctx, body[:team_id])
@@ -679,7 +680,7 @@ module BetterAuth
679
680
  end
680
681
 
681
682
  def organization_create_role_endpoint(config)
682
- Endpoint.new(path: "/organization/create-role", method: "POST") do |ctx|
683
+ Endpoint.new(path: "/organization/create-role", method: "POST", metadata: organization_openapi("createOrganizationRole", "Create an organization role", response: organization_role_action_schema)) do |ctx|
683
684
  session = Routes.current_session(ctx)
684
685
  body = normalize_hash(ctx.body)
685
686
  organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
@@ -698,7 +699,7 @@ module BetterAuth
698
699
  end
699
700
 
700
701
  def organization_list_roles_endpoint(config)
701
- Endpoint.new(path: "/organization/list-roles", method: "GET") do |ctx|
702
+ Endpoint.new(path: "/organization/list-roles", method: "GET", metadata: organization_openapi("listOrganizationRoles", "List organization roles", response: {type: "array", items: organization_role_schema})) do |ctx|
702
703
  session = Routes.current_session(ctx)
703
704
  organization_id = normalize_hash(ctx.query)[:organization_id] || session[:session]["activeOrganizationId"]
704
705
  require_org_permission!(ctx, config, session, organization_id, {ac: ["read"]}, ORGANIZATION_ERROR_CODES.fetch("YOU_ARE_NOT_ALLOWED_TO_LIST_A_ROLE"))
@@ -709,7 +710,7 @@ module BetterAuth
709
710
  end
710
711
 
711
712
  def organization_get_role_endpoint(config)
712
- Endpoint.new(path: "/organization/get-role", method: "GET") do |ctx|
713
+ Endpoint.new(path: "/organization/get-role", method: "GET", metadata: organization_openapi("getOrganizationRole", "Get an organization role", response: organization_role_schema)) do |ctx|
713
714
  session = Routes.current_session(ctx)
714
715
  query = normalize_hash(ctx.query)
715
716
  organization_id = query[:organization_id] || session[:session]["activeOrganizationId"]
@@ -721,7 +722,7 @@ module BetterAuth
721
722
  end
722
723
 
723
724
  def organization_update_role_endpoint(config)
724
- Endpoint.new(path: "/organization/update-role", method: "POST") do |ctx|
725
+ Endpoint.new(path: "/organization/update-role", method: "POST", metadata: organization_openapi("updateOrganizationRole", "Update an organization role", response: organization_role_action_schema)) do |ctx|
725
726
  session = Routes.current_session(ctx)
726
727
  body = normalize_hash(ctx.body)
727
728
  organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
@@ -746,7 +747,7 @@ module BetterAuth
746
747
  end
747
748
 
748
749
  def organization_delete_role_endpoint(config)
749
- Endpoint.new(path: "/organization/delete-role", method: "POST") do |ctx|
750
+ Endpoint.new(path: "/organization/delete-role", method: "POST", metadata: organization_openapi("deleteOrganizationRole", "Delete an organization role", response: OpenAPI.success_response_schema)) do |ctx|
750
751
  session = Routes.current_session(ctx)
751
752
  body = normalize_hash(ctx.body)
752
753
  organization_id = body[:organization_id] || session[:session]["activeOrganizationId"]
@@ -766,6 +767,104 @@ module BetterAuth
766
767
  end
767
768
  end
768
769
 
770
+ def organization_openapi(operation_id, description, response:, response_description: "Success", request: nil, required: [], parameters: nil)
771
+ openapi = {
772
+ operationId: operation_id,
773
+ description: description,
774
+ responses: {
775
+ "200" => OpenAPI.json_response(response_description, response)
776
+ }
777
+ }
778
+ openapi[:requestBody] = OpenAPI.json_request_body(OpenAPI.object_schema(request, required: required)) if request
779
+ openapi[:parameters] = parameters if parameters
780
+
781
+ {openapi: openapi}
782
+ end
783
+
784
+ def organization_ref_schema(name)
785
+ {
786
+ type: "object",
787
+ "$ref": "#/components/schemas/#{name}"
788
+ }
789
+ end
790
+
791
+ def organization_nullable_schema(name)
792
+ {
793
+ type: ["object", "null"],
794
+ "$ref": "#/components/schemas/#{name}"
795
+ }
796
+ end
797
+
798
+ def organization_array_schema(name)
799
+ {
800
+ type: "array",
801
+ items: organization_ref_schema(name)
802
+ }
803
+ end
804
+
805
+ def organization_accept_invitation_schema
806
+ OpenAPI.object_schema(
807
+ {
808
+ invitation: organization_ref_schema("Invitation"),
809
+ member: organization_ref_schema("Member")
810
+ },
811
+ required: ["invitation", "member"]
812
+ )
813
+ end
814
+
815
+ def organization_active_member_role_schema
816
+ OpenAPI.object_schema(
817
+ {
818
+ role: {type: "string"},
819
+ member: organization_ref_schema("Member")
820
+ },
821
+ required: ["role", "member"]
822
+ )
823
+ end
824
+
825
+ def organization_members_response_schema
826
+ OpenAPI.object_schema(
827
+ {
828
+ members: organization_array_schema("Member"),
829
+ total: {type: "number"}
830
+ },
831
+ required: ["members", "total"]
832
+ )
833
+ end
834
+
835
+ def organization_permission_response_schema
836
+ OpenAPI.object_schema(
837
+ {
838
+ error: {type: ["string", "null"]},
839
+ success: {type: "boolean"}
840
+ },
841
+ required: ["success"]
842
+ )
843
+ end
844
+
845
+ def organization_role_schema
846
+ OpenAPI.object_schema(
847
+ {
848
+ id: {type: "string"},
849
+ organizationId: {type: "string"},
850
+ role: {type: "string"},
851
+ permission: {type: "object"}
852
+ },
853
+ required: ["role", "permission"]
854
+ )
855
+ end
856
+
857
+ def organization_role_action_schema
858
+ OpenAPI.object_schema(
859
+ {
860
+ success: {type: "boolean"},
861
+ roleData: organization_role_schema,
862
+ statements: {type: "object"}
863
+ },
864
+ required: ["success", "roleData"]
865
+ )
866
+ end
867
+
769
868
  def parse_roles(roles)
770
869
  Array(roles).join(",")
771
870
  end
@@ -65,7 +65,38 @@ module BetterAuth
65
65
  end
66
66
 
67
67
  def sign_in_phone_number_endpoint(config)
68
- Endpoint.new(path: "/sign-in/phone-number", method: "POST") do |ctx|
68
+ Endpoint.new(
69
+ path: "/sign-in/phone-number",
70
+ method: "POST",
71
+ metadata: {
72
+ openapi: {
73
+ operationId: "signInPhoneNumber",
74
+ description: "Sign in with phone number and password",
75
+ requestBody: OpenAPI.json_request_body(
76
+ OpenAPI.object_schema(
77
+ {
78
+ phoneNumber: {type: "string"},
79
+ password: {type: "string"},
80
+ rememberMe: {type: ["boolean", "null"]}
81
+ },
82
+ required: ["phoneNumber", "password"]
83
+ )
84
+ ),
85
+ responses: {
86
+ "200" => OpenAPI.json_response(
87
+ "Signed in",
88
+ OpenAPI.object_schema(
89
+ {
90
+ token: {type: "string"},
91
+ user: {type: "object", "$ref": "#/components/schemas/User"}
92
+ },
93
+ required: ["token", "user"]
94
+ )
95
+ )
96
+ }
97
+ }
98
+ }
99
+ ) do |ctx|
69
100
  body = normalize_hash(ctx.body)
70
101
  phone_number = body[:phone_number].to_s
71
102
  password = body[:password].to_s
@@ -101,7 +132,35 @@ module BetterAuth
101
132
  end
102
133
 
103
134
  def send_phone_number_otp_endpoint(config)
104
- Endpoint.new(path: "/phone-number/send-otp", method: "POST") do |ctx|
135
+ Endpoint.new(
136
+ path: "/phone-number/send-otp",
137
+ method: "POST",
138
+ metadata: {
139
+ openapi: {
140
+ operationId: "sendPhoneNumberOTP",
141
+ description: "Send a phone number OTP",
142
+ requestBody: OpenAPI.json_request_body(
143
+ OpenAPI.object_schema(
144
+ {
145
+ phoneNumber: {type: "string"}
146
+ },
147
+ required: ["phoneNumber"]
148
+ )
149
+ ),
150
+ responses: {
151
+ "200" => OpenAPI.json_response(
152
+ "OTP sent",
153
+ OpenAPI.object_schema(
154
+ {
155
+ message: {type: "string"}
156
+ },
157
+ required: ["message"]
158
+ )
159
+ )
160
+ }
161
+ }
162
+ }
163
+ ) do |ctx|
105
164
  sender = config[:send_otp]
106
165
  unless sender.respond_to?(:call)
107
166
  raise APIError.new("NOT_IMPLEMENTED", message: PHONE_NUMBER_ERROR_CODES["SEND_OTP_NOT_IMPLEMENTED"])
@@ -118,7 +177,40 @@ module BetterAuth
118
177
  end
119
178
 
120
179
  def verify_phone_number_endpoint(config)
121
- Endpoint.new(path: "/phone-number/verify", method: "POST") do |ctx|
180
+ Endpoint.new(
181
+ path: "/phone-number/verify",
182
+ method: "POST",
183
+ metadata: {
184
+ openapi: {
185
+ operationId: "verifyPhoneNumber",
186
+ description: "Verify a phone number OTP",
187
+ requestBody: OpenAPI.json_request_body(
188
+ OpenAPI.object_schema(
189
+ {
190
+ phoneNumber: {type: "string"},
191
+ code: {type: "string"},
192
+ updatePhoneNumber: {type: ["boolean", "null"]},
193
+ disableSession: {type: ["boolean", "null"]}
194
+ },
195
+ required: ["phoneNumber", "code"]
196
+ )
197
+ ),
198
+ responses: {
199
+ "200" => OpenAPI.json_response(
200
+ "Phone number verified",
201
+ OpenAPI.object_schema(
202
+ {
203
+ status: {type: "boolean"},
204
+ token: {type: ["string", "null"]},
205
+ user: {type: "object", "$ref": "#/components/schemas/User"}
206
+ },
207
+ required: ["status", "user"]
208
+ )
209
+ )
210
+ }
211
+ }
212
+ }
213
+ ) do |ctx|
122
214
  body = normalize_hash(ctx.body)
123
215
  phone_number = body[:phone_number].to_s
124
216
  code = body[:code].to_s
@@ -163,7 +255,27 @@ module BetterAuth
163
255
  end
164
256
 
165
257
  def request_password_reset_phone_number_endpoint(config)
166
- Endpoint.new(path: "/phone-number/request-password-reset", method: "POST") do |ctx|
258
+ Endpoint.new(
259
+ path: "/phone-number/request-password-reset",
260
+ method: "POST",
261
+ metadata: {
262
+ openapi: {
263
+ operationId: "requestPasswordResetPhoneNumber",
264
+ description: "Request a phone number password reset OTP",
265
+ requestBody: OpenAPI.json_request_body(
266
+ OpenAPI.object_schema(
267
+ {
268
+ phoneNumber: {type: "string"}
269
+ },
270
+ required: ["phoneNumber"]
271
+ )
272
+ ),
273
+ responses: {
274
+ "200" => OpenAPI.json_response("Password reset OTP requested", OpenAPI.status_response_schema)
275
+ }
276
+ }
277
+ }
278
+ ) do |ctx|
167
279
  body = normalize_hash(ctx.body)
168
280
  phone_number = body[:phone_number].to_s
169
281
  user = ctx.context.adapter.find_one(model: "user", where: [{field: "phoneNumber", value: phone_number}])
@@ -179,7 +291,29 @@ module BetterAuth
179
291
  end
180
292
 
181
293
  def reset_password_phone_number_endpoint(config)
182
- Endpoint.new(path: "/phone-number/reset-password", method: "POST") do |ctx|
294
+ Endpoint.new(
295
+ path: "/phone-number/reset-password",
296
+ method: "POST",
297
+ metadata: {
298
+ openapi: {
299
+ operationId: "resetPasswordPhoneNumber",
300
+ description: "Reset a password with a phone number OTP",
301
+ requestBody: OpenAPI.json_request_body(
302
+ OpenAPI.object_schema(
303
+ {
304
+ phoneNumber: {type: "string"},
305
+ otp: {type: "string"},
306
+ newPassword: {type: "string"}
307
+ },
308
+ required: ["phoneNumber", "otp", "newPassword"]
309
+ )
310
+ ),
311
+ responses: {
312
+ "200" => OpenAPI.json_response("Password reset", OpenAPI.status_response_schema)
313
+ }
314
+ }
315
+ }
316
+ ) do |ctx|
183
317
  body = normalize_hash(ctx.body)
184
318
  phone_number = body[:phone_number].to_s
185
319
  otp = body[:otp].to_s
@@ -225,7 +359,8 @@ module BetterAuth
225
359
  "phoneNumber" => phone_number,
226
360
  "phoneNumberVerified" => true,
227
361
  "emailVerified" => false
228
- )
362
+ ),
363
+ context: ctx
229
364
  )
230
365
  end
231
366