supabase-rb 2.0.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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/lib/supabase/README.md +90 -0
  3. data/lib/supabase/auth/README.md +172 -0
  4. data/lib/supabase/auth/admin_api.rb +218 -0
  5. data/lib/supabase/auth/admin_oauth_api.rb +51 -0
  6. data/lib/supabase/auth/api.rb +125 -0
  7. data/lib/supabase/auth/async/admin_api.rb +36 -0
  8. data/lib/supabase/auth/async/admin_oauth_api.rb +15 -0
  9. data/lib/supabase/auth/async/api.rb +32 -0
  10. data/lib/supabase/auth/async/client.rb +33 -0
  11. data/lib/supabase/auth/async.rb +14 -0
  12. data/lib/supabase/auth/client.rb +1217 -0
  13. data/lib/supabase/auth/constants.rb +32 -0
  14. data/lib/supabase/auth/errors.rb +207 -0
  15. data/lib/supabase/auth/helpers.rb +222 -0
  16. data/lib/supabase/auth/memory_storage.rb +25 -0
  17. data/lib/supabase/auth/storage.rb +19 -0
  18. data/lib/supabase/auth/timer.rb +40 -0
  19. data/lib/supabase/auth/types.rb +517 -0
  20. data/lib/supabase/auth/version.rb +7 -0
  21. data/lib/supabase/auth.rb +19 -0
  22. data/lib/supabase/client.rb +200 -0
  23. data/lib/supabase/client_options.rb +82 -0
  24. data/lib/supabase/functions/README.md +71 -0
  25. data/lib/supabase/functions/async/client.rb +45 -0
  26. data/lib/supabase/functions/async.rb +8 -0
  27. data/lib/supabase/functions/client.rb +174 -0
  28. data/lib/supabase/functions/errors.rb +38 -0
  29. data/lib/supabase/functions/types.rb +37 -0
  30. data/lib/supabase/functions/version.rb +7 -0
  31. data/lib/supabase/functions.rb +11 -0
  32. data/lib/supabase/postgrest/README.md +84 -0
  33. data/lib/supabase/postgrest/async/client.rb +50 -0
  34. data/lib/supabase/postgrest/async.rb +8 -0
  35. data/lib/supabase/postgrest/client.rb +136 -0
  36. data/lib/supabase/postgrest/errors.rb +49 -0
  37. data/lib/supabase/postgrest/request_builder.rb +657 -0
  38. data/lib/supabase/postgrest/types.rb +60 -0
  39. data/lib/supabase/postgrest/utils.rb +24 -0
  40. data/lib/supabase/postgrest/version.rb +7 -0
  41. data/lib/supabase/postgrest.rb +13 -0
  42. data/lib/supabase/realtime/README.md +90 -0
  43. data/lib/supabase/realtime/channel.rb +274 -0
  44. data/lib/supabase/realtime/client.rb +182 -0
  45. data/lib/supabase/realtime/errors.rb +19 -0
  46. data/lib/supabase/realtime/message.rb +38 -0
  47. data/lib/supabase/realtime/presence.rb +136 -0
  48. data/lib/supabase/realtime/push.rb +48 -0
  49. data/lib/supabase/realtime/socket.rb +40 -0
  50. data/lib/supabase/realtime/sockets/async_websocket.rb +175 -0
  51. data/lib/supabase/realtime/sockets/websocket_client_simple.rb +94 -0
  52. data/lib/supabase/realtime/test_socket.rb +65 -0
  53. data/lib/supabase/realtime/transformers.rb +26 -0
  54. data/lib/supabase/realtime/types.rb +70 -0
  55. data/lib/supabase/realtime/version.rb +7 -0
  56. data/lib/supabase/realtime.rb +18 -0
  57. data/lib/supabase/storage/README.md +108 -0
  58. data/lib/supabase/storage/analytics.rb +69 -0
  59. data/lib/supabase/storage/async/client.rb +52 -0
  60. data/lib/supabase/storage/async.rb +8 -0
  61. data/lib/supabase/storage/bucket_api.rb +65 -0
  62. data/lib/supabase/storage/client.rb +80 -0
  63. data/lib/supabase/storage/errors.rb +32 -0
  64. data/lib/supabase/storage/file_api.rb +281 -0
  65. data/lib/supabase/storage/request.rb +63 -0
  66. data/lib/supabase/storage/types.rb +236 -0
  67. data/lib/supabase/storage/utils.rb +35 -0
  68. data/lib/supabase/storage/vectors.rb +189 -0
  69. data/lib/supabase/storage/version.rb +7 -0
  70. data/lib/supabase/storage.rb +17 -0
  71. data/lib/supabase/version.rb +5 -0
  72. data/lib/supabase-auth.rb +3 -0
  73. data/lib/supabase.rb +63 -0
  74. metadata +272 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 1640366f383a39fbba5df993fe27d204981f99672377fc6982403b4259f6cd81
4
+ data.tar.gz: d332d7b5a8c3bd0f698263988a8c949c132dbbe51908007b7bf5321d8e53a22b
5
+ SHA512:
6
+ metadata.gz: 11f501920c972f3dde5fee01049df4f95fd2aaff1ff6bc2e29c093e8f4c61671900e9caab52e9b870fa86a430a05d0cdbfaf5c4e957e418a389397cfd4ec941f
7
+ data.tar.gz: 582991118f05d8171764efa5788d97234c013f8f7eacc84eefe903959068f20a7d083eaf09ead84a4dc76d225a318c6d69e70b96654f97e0254d184a13bbec39
@@ -0,0 +1,90 @@
1
+ # `supabase-rb`
2
+
3
+ Ruby client for [Supabase](https://supabase.com). Umbrella gem that exposes
4
+ Auth, PostgREST, Storage, Edge Functions, and Realtime through a single
5
+ `Supabase.create_client` factory.
6
+
7
+ - Documentation: [supabase.com/docs](https://supabase.com/docs/reference)
8
+ - Source: [github.com/supabase-ruby/supabase-rb](https://github.com/supabase-ruby/supabase-rb)
9
+
10
+ ## Installation
11
+
12
+ ```ruby
13
+ gem "supabase-rb"
14
+ ```
15
+
16
+ Then `bundle install`. (Requires Ruby >= 3.0.) The Ruby require path is
17
+ `require "supabase"` — only the gem name differs.
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ require "supabase"
23
+
24
+ client = Supabase.create_client(
25
+ supabase_url: ENV["SUPABASE_URL"],
26
+ supabase_key: ENV["SUPABASE_ANON_KEY"]
27
+ )
28
+
29
+ # Auth
30
+ client.auth.sign_in_with_password(email: "user@example.com", password: "pw")
31
+
32
+ # PostgREST
33
+ users = client.from("users").select("id, name").eq("status", "active").execute
34
+
35
+ # Storage
36
+ client.storage.from("avatars").upload("user1.png", File.binread("user1.png"),
37
+ content_type: "image/png")
38
+
39
+ # Edge Functions
40
+ result = client.functions.invoke("hello-world", body: { name: "Ada" })
41
+
42
+ # Realtime (you bring the WebSocket transport — see supabase-realtime)
43
+ channel = client.realtime.channel("realtime:public:users")
44
+ channel.on_postgres_changes("INSERT", schema: "public", table: "users") { |p| puts p }
45
+ ```
46
+
47
+ Pass `async: true` to swap Auth / PostgREST / Storage / Functions into their
48
+ async variants (Realtime stays sync):
49
+
50
+ ```ruby
51
+ require "async"
52
+
53
+ async_client = Supabase.create_client(
54
+ supabase_url: ENV["SUPABASE_URL"],
55
+ supabase_key: ENV["SUPABASE_ANON_KEY"],
56
+ async: true
57
+ )
58
+
59
+ Async do |task|
60
+ jobs = user_ids.map do |id|
61
+ task.async { async_client.from("users").select("*").eq("id", id).execute }
62
+ end
63
+ jobs.map(&:wait)
64
+ end
65
+ ```
66
+
67
+ `client.set_auth(jwt)` rotates the `Authorization` header across every
68
+ sub-client at once — useful after `auth.sign_in` returns a fresh user JWT.
69
+
70
+ ## URL routing
71
+
72
+ Sub-client URLs are derived from the project URL:
73
+
74
+ | Sub-client | URL |
75
+ |---|---|
76
+ | Auth | `<project>/auth/v1` |
77
+ | PostgREST | `<project>/rest/v1` |
78
+ | Storage | `<project>/storage/v1` |
79
+ | Functions | `<project>/functions/v1` |
80
+ | Realtime | `wss://<host>/realtime/v1/websocket` |
81
+
82
+ ## Modules
83
+
84
+ `supabase-rb` packages every module in one gem. Per-module references:
85
+
86
+ - [Auth](auth/README.md)
87
+ - [PostgREST](postgrest/README.md)
88
+ - [Storage](storage/README.md)
89
+ - [Edge Functions](functions/README.md)
90
+ - [Realtime](realtime/README.md)
@@ -0,0 +1,172 @@
1
+ # `supabase-auth`
2
+
3
+ Ruby client for [Supabase Auth](https://supabase.com/docs/guides/auth)
4
+ (GoTrue). Sign-in flows, session management, MFA, JWT verification, OAuth,
5
+ and admin user management. Mirrors the public surface of
6
+ [`supabase_auth`](https://github.com/supabase/supabase-py/tree/main/src/auth)
7
+ in Python.
8
+
9
+ - Source: [github.com/supabase-rb/client](https://github.com/supabase-rb/client)
10
+ - RubyGems: [rubygems.org/gems/supabase-auth](https://rubygems.org/gems/supabase-auth)
11
+
12
+ ## Installation
13
+
14
+ ```ruby
15
+ gem "supabase-auth"
16
+ ```
17
+
18
+ Then `bundle install`. (Requires Ruby >= 3.0.)
19
+
20
+ ## Usage
21
+
22
+ ```ruby
23
+ require "supabase/auth"
24
+
25
+ client = Supabase::Auth::Client.new(
26
+ url: "https://your-project.supabase.co/auth/v1",
27
+ headers: { "apiKey" => "your-anon-key" }
28
+ )
29
+
30
+ response = client.sign_in_with_password(email: "user@example.com", password: "pw")
31
+ session = client.get_session
32
+ user = client.get_user
33
+ ```
34
+
35
+ ### Sign-in methods
36
+
37
+ ```ruby
38
+ client.sign_in_with_password(email:, password:)
39
+ client.sign_in_with_otp(email:) # magic link
40
+ client.sign_in_with_otp(phone:) # SMS OTP
41
+ client.sign_in_with_oauth(provider: "google")
42
+ client.sign_in_with_sso(domain: "company.com")
43
+ client.sign_in_with_id_token(provider:, token:)
44
+ client.sign_in_anonymously
45
+ client.sign_out
46
+ ```
47
+
48
+ ### Session lifecycle
49
+
50
+ ```ruby
51
+ client.set_session("access_token", "refresh_token")
52
+ client.refresh_session
53
+ client.exchange_code_for_session(auth_code: "code") # PKCE
54
+
55
+ subscription = client.on_auth_state_change { |event, session| ... }
56
+ subscription.unsubscribe.call
57
+ ```
58
+
59
+ Events: `SIGNED_IN`, `SIGNED_OUT`, `TOKEN_REFRESHED`, `USER_UPDATED`,
60
+ `MFA_CHALLENGE_VERIFIED`, `PASSWORD_RECOVERY`.
61
+
62
+ ### MFA
63
+
64
+ ```ruby
65
+ enrolled = client.mfa.enroll(factor_type: "totp")
66
+ challenge = client.mfa.challenge(factor_id: enrolled["id"])
67
+ client.mfa.verify(factor_id: enrolled["id"], challenge_id: challenge.id, code: "123456")
68
+
69
+ client.mfa.challenge_and_verify(factor_id: enrolled["id"], code: "123456")
70
+ client.mfa.get_authenticator_assurance_level
71
+ client.mfa.list_factors
72
+ client.mfa.unenroll(factor_id: enrolled["id"])
73
+ ```
74
+
75
+ ### JWT verification
76
+
77
+ ```ruby
78
+ claims = client.get_claims(jwt: "eyJhbG...")
79
+ claims.claims # decoded payload
80
+ claims.headers # JWT headers
81
+ ```
82
+
83
+ Supports HS256, RS256, ES256, PS256+ (and their 384/512 variants).
84
+
85
+ ### Admin API
86
+
87
+ ```ruby
88
+ admin = Supabase::Auth::AdminApi.new(
89
+ url: "https://your-project.supabase.co/auth/v1",
90
+ headers: { "Authorization" => "Bearer #{service_role}", "apiKey" => service_role }
91
+ )
92
+
93
+ admin.create_user(email:, password:)
94
+ admin.list_users(page: 1, per_page: 50)
95
+ admin.invite_user_by_email("user@example.com")
96
+ admin.generate_link(type: "signup", email:, password:)
97
+
98
+ # OAuth 2.1 client administration (when the OAuth server feature is enabled)
99
+ admin.oauth.create_client(client_name:, redirect_uris:)
100
+ admin.oauth.list_clients(page: 1, per_page: 20)
101
+ admin.oauth.regenerate_client_secret("client-uuid")
102
+ ```
103
+
104
+ ### Async variant
105
+
106
+ ```ruby
107
+ require "supabase/auth/async"
108
+
109
+ async_client = Supabase::Auth::Async::Client.new(
110
+ url: "https://your-project.supabase.co/auth/v1",
111
+ headers: { "apiKey" => "your-anon-key" }
112
+ )
113
+
114
+ Async do
115
+ user = async_client.get_user
116
+ end
117
+ ```
118
+
119
+ Built on [`async-http-faraday`](https://github.com/socketry/async-http-faraday).
120
+ Loaded only when you `require "supabase/auth/async"` so sync-only users pay
121
+ zero cost.
122
+
123
+ ### Constructor options
124
+
125
+ ```ruby
126
+ Supabase::Auth::Client.new(
127
+ url: "...",
128
+ headers: { "apiKey" => "..." },
129
+ auto_refresh_token: true,
130
+ persist_session: true,
131
+ detect_session_in_url: true,
132
+ flow_type: "implicit", # or "pkce"
133
+ storage: custom_storage
134
+ )
135
+ ```
136
+
137
+ ## Ruby-specific additions
138
+
139
+ The Ruby port carries two intentional enhancements over `supabase_auth` (py),
140
+ documented here so they don't get "fixed" to match Python.
141
+
142
+ ### `AuthPKCEError`
143
+
144
+ `Supabase::Auth::Errors::AuthPKCEError` is a dedicated exception for
145
+ PKCE-flow failures (missing or invalid `code_verifier` during
146
+ `exchange_code_for_session`). Python raises a generic `AuthError`; the
147
+ dedicated class gives callers a precise `rescue` target.
148
+
149
+ ### Explicit JWT algorithm → digest mapping
150
+
151
+ `Supabase::Auth::Client::ALG_TO_DIGEST` is a frozen lookup table:
152
+
153
+ ```ruby
154
+ ALG_TO_DIGEST = {
155
+ "RS256" => "SHA256", "RS384" => "SHA384", "RS512" => "SHA512",
156
+ "ES256" => "SHA256", "ES384" => "SHA384", "ES512" => "SHA512",
157
+ "PS256" => "SHA256", "PS384" => "SHA384", "PS512" => "SHA512"
158
+ }.freeze
159
+ ```
160
+
161
+ Python resolves algorithms dynamically via `PyJWT.get_algorithm_by_name`.
162
+ The Ruby table makes the supported set readable in one place and fails fast
163
+ (`AuthInvalidJwtError`) on unsupported `alg` values.
164
+
165
+ ## Development
166
+
167
+ Integration tests need the GoTrue stack on ports 9996–9999:
168
+
169
+ ```bash
170
+ docker compose -f infra/docker-compose.yml up -d
171
+ bundle exec rspec spec/supabase/auth/ spec/client_spec.rb spec/admin_api_spec.rb
172
+ ```
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Supabase
6
+ module Auth
7
+ # Admin API for managing users with a service role key.
8
+ # Provides CRUD operations on users, link generation, and MFA management.
9
+ class AdminApi < Api
10
+ # @return [AdminOAuthApi] OAuth 2.1 client administration accessor
11
+ attr_reader :oauth
12
+
13
+ # @param url [String] The GoTrue API base URL
14
+ # @param headers [Hash] Headers including Authorization bearer token
15
+ # @param http_client [Faraday::Connection, nil] Optional custom Faraday client
16
+ # @param verify [Boolean] Verify TLS certificates (default true)
17
+ # @param proxy [String, nil] HTTP proxy URL
18
+ # @param timeout [Numeric, nil] Per-request timeout in seconds
19
+ def initialize(url:, headers: {}, http_client: nil, verify: true, proxy: nil, timeout: nil)
20
+ super(url: url, headers: headers, http_client: http_client, verify: verify, proxy: proxy, timeout: timeout)
21
+ @oauth = AdminOAuthApi.new(self)
22
+ end
23
+
24
+ # Creates a new user via the admin API.
25
+ # @param attributes [Hash] user attributes (email, password, user_metadata, app_metadata, etc.)
26
+ # @return [Types::UserResponse]
27
+ def create_user(attributes)
28
+ data = post("admin/users", body: attributes)
29
+ Helpers.parse_user_response(data)
30
+ end
31
+
32
+ # Lists all users.
33
+ # @param page [Integer, nil] page number
34
+ # @param per_page [Integer, nil] users per page
35
+ # @return [Array<Types::User>]
36
+ def list_users(page: nil, per_page: nil)
37
+ params = {}
38
+ params[:page] = page if page
39
+ params[:per_page] = per_page if per_page
40
+ data = get("admin/users", params: params)
41
+ users = data["users"] || []
42
+ users.map { |u| Types::User.from_hash(u) }
43
+ end
44
+
45
+ # Gets a user by their ID.
46
+ # @param uid [String] user UUID
47
+ # @return [Types::UserResponse]
48
+ # @raise [ArgumentError] if uid is not a valid UUID
49
+ def get_user_by_id(uid)
50
+ _validate_uuid(uid)
51
+ data = get("admin/users/#{uid}")
52
+ Helpers.parse_user_response(data)
53
+ end
54
+
55
+ # Updates a user by their ID.
56
+ # @param uid [String] user UUID
57
+ # @param attributes [Hash] attributes to update
58
+ # @return [Types::UserResponse]
59
+ # @raise [ArgumentError] if uid is not a valid UUID
60
+ def update_user_by_id(uid, attributes)
61
+ _validate_uuid(uid)
62
+ data = put("admin/users/#{uid}", body: attributes)
63
+ Helpers.parse_user_response(data)
64
+ end
65
+
66
+ # Deletes a user by their ID.
67
+ # @param uid [String] user UUID
68
+ # @param should_soft_delete [Boolean] soft delete instead of hard delete
69
+ # @raise [ArgumentError] if uid is not a valid UUID
70
+ def delete_user(uid, should_soft_delete: false)
71
+ _validate_uuid(uid)
72
+ _request("DELETE", "admin/users/#{uid}", body: { should_soft_delete: should_soft_delete })
73
+ end
74
+
75
+ # Generates email links and OTPs.
76
+ def generate_link(params)
77
+ options = params[:options] || params["options"] || {}
78
+ body = {
79
+ type: params[:type] || params["type"],
80
+ email: params[:email] || params["email"],
81
+ password: params[:password] || params["password"],
82
+ new_email: params[:new_email] || params["new_email"],
83
+ data: options[:data] || options["data"]
84
+ }
85
+ redirect_to = options[:redirect_to] || options["redirect_to"]
86
+ query = {}
87
+ query["redirect_to"] = redirect_to if redirect_to
88
+ data = post("admin/generate_link", body: body, params: query)
89
+ Helpers.parse_link_response(data)
90
+ end
91
+
92
+ # Invites a user by email.
93
+ def invite_user_by_email(email, options = {})
94
+ body = { email: email, data: options[:data] || options["data"] }
95
+ redirect_to = options[:redirect_to] || options["redirect_to"]
96
+ query = {}
97
+ query["redirect_to"] = redirect_to if redirect_to
98
+ data = post("invite", body: body, params: query)
99
+ Helpers.parse_user_response(data)
100
+ end
101
+
102
+ # Signs out a user by revoking their session via the admin API.
103
+ def sign_out(access_token, scope = "global")
104
+ _request("POST", "logout", jwt: access_token, params: { "scope" => scope }, no_resolve_json: true)
105
+ end
106
+
107
+ # Lists MFA factors for a user (admin).
108
+ # @param params [Hash] :user_id (required)
109
+ # @return [Types::AuthMFAAdminListFactorsResponse]
110
+ def _list_factors(params)
111
+ user_id = params[:user_id] || params["user_id"]
112
+ _validate_uuid(user_id)
113
+ data = get("admin/users/#{user_id}/factors")
114
+ Types::AuthMFAAdminListFactorsResponse.from_hash(data)
115
+ end
116
+
117
+ # Deletes an MFA factor for a user (admin).
118
+ # @param params [Hash] :user_id and :id (both required)
119
+ # @return [Types::AuthMFAAdminDeleteFactorResponse]
120
+ def _delete_factor(params)
121
+ user_id = params[:user_id] || params["user_id"]
122
+ factor_id = params[:id] || params["id"]
123
+ _validate_uuid(user_id)
124
+ _validate_uuid(factor_id)
125
+ data = delete("admin/users/#{user_id}/factors/#{factor_id}")
126
+ Types::AuthMFAAdminDeleteFactorResponse.from_hash(data)
127
+ end
128
+
129
+ # Lists OAuth clients with optional pagination. Only relevant when the OAuth 2.1
130
+ # server is enabled in Supabase Auth.
131
+ # @param params [Hash, Types::PageParams, nil] optional :page and :per_page
132
+ # @return [Types::OAuthClientListResponse]
133
+ def _list_oauth_clients(params = nil)
134
+ query = {}
135
+ if params
136
+ page = params[:page] || params["page"]
137
+ per_page = params[:per_page] || params["per_page"]
138
+ query[:page] = page if page
139
+ query[:per_page] = per_page if per_page
140
+ end
141
+
142
+ response = _request("GET", "admin/oauth/clients", params: query, no_resolve_json: true)
143
+ body = response.body.is_a?(String) ? JSON.parse(response.body) : (response.body || {})
144
+ result = Types::OAuthClientListResponse.from_hash(body)
145
+
146
+ total = response.headers["x-total-count"] || response.headers["X-Total-Count"]
147
+ result.total = total.to_i if total
148
+
149
+ links = response.headers["link"] || response.headers["Link"]
150
+ if links
151
+ links.split(",").each do |link|
152
+ parts = link.split(";")
153
+ next unless parts.length >= 2
154
+
155
+ page_match = parts[0].split("page=")
156
+ next unless page_match.length >= 2
157
+
158
+ page_num = page_match[1].split("&")[0].sub(/>$/, "").to_i
159
+ rel = parts[1].split("=")[1].to_s.delete('"').strip
160
+ case rel
161
+ when "next" then result.next_page = page_num
162
+ when "last" then result.last_page = page_num
163
+ end
164
+ end
165
+ end
166
+
167
+ result
168
+ end
169
+
170
+ # Creates a new OAuth client. Only relevant when the OAuth 2.1 server is enabled.
171
+ # @param params [Hash] OAuth client attributes (client_name, redirect_uris, etc.)
172
+ # @return [Types::OAuthClientResponse]
173
+ def _create_oauth_client(params)
174
+ data = post("admin/oauth/clients", body: params)
175
+ Types::OAuthClientResponse.new(client: Types::OAuthClient.from_hash(data))
176
+ end
177
+
178
+ # Gets details of a specific OAuth client.
179
+ # @param client_id [String] OAuth client UUID
180
+ # @return [Types::OAuthClientResponse]
181
+ # @raise [ArgumentError] if client_id is not a valid UUID
182
+ def _get_oauth_client(client_id)
183
+ _validate_uuid(client_id)
184
+ data = get("admin/oauth/clients/#{client_id}")
185
+ Types::OAuthClientResponse.new(client: Types::OAuthClient.from_hash(data))
186
+ end
187
+
188
+ # Updates an OAuth client.
189
+ # @param client_id [String] OAuth client UUID
190
+ # @param params [Hash] attributes to update
191
+ # @return [Types::OAuthClientResponse]
192
+ # @raise [ArgumentError] if client_id is not a valid UUID
193
+ def _update_oauth_client(client_id, params)
194
+ _validate_uuid(client_id)
195
+ data = put("admin/oauth/clients/#{client_id}", body: params)
196
+ Types::OAuthClientResponse.new(client: Types::OAuthClient.from_hash(data))
197
+ end
198
+
199
+ # Deletes an OAuth client.
200
+ # @param client_id [String] OAuth client UUID
201
+ # @raise [ArgumentError] if client_id is not a valid UUID
202
+ def _delete_oauth_client(client_id)
203
+ _validate_uuid(client_id)
204
+ _request("DELETE", "admin/oauth/clients/#{client_id}")
205
+ end
206
+
207
+ # Regenerates the secret for an OAuth client.
208
+ # @param client_id [String] OAuth client UUID
209
+ # @return [Types::OAuthClientResponse]
210
+ # @raise [ArgumentError] if client_id is not a valid UUID
211
+ def _regenerate_oauth_client_secret(client_id)
212
+ _validate_uuid(client_id)
213
+ data = post("admin/oauth/clients/#{client_id}/regenerate_secret")
214
+ Types::OAuthClientResponse.new(client: Types::OAuthClient.from_hash(data))
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supabase
4
+ module Auth
5
+ # OAuth 2.1 client administration. Mirrors supabase-py's SyncGoTrueAdminOAuthAPI.
6
+ # Only relevant when the OAuth 2.1 server is enabled in Supabase Auth.
7
+ # Accessed via {AdminApi#oauth}; delegates to the underscored implementations on AdminApi.
8
+ class AdminOAuthApi
9
+ # @param admin [AdminApi]
10
+ def initialize(admin)
11
+ @admin = admin
12
+ end
13
+
14
+ # @param params [Hash, Types::PageParams, nil] optional :page / :per_page
15
+ # @return [Types::OAuthClientListResponse]
16
+ def list_clients(params = nil)
17
+ @admin._list_oauth_clients(params)
18
+ end
19
+
20
+ # @param params [Hash] new client attributes (client_name, redirect_uris, etc.)
21
+ # @return [Types::OAuthClientResponse]
22
+ def create_client(params)
23
+ @admin._create_oauth_client(params)
24
+ end
25
+
26
+ # @param client_id [String] OAuth client UUID
27
+ # @return [Types::OAuthClientResponse]
28
+ def get_client(client_id)
29
+ @admin._get_oauth_client(client_id)
30
+ end
31
+
32
+ # @param client_id [String] OAuth client UUID
33
+ # @param params [Hash] attributes to update
34
+ # @return [Types::OAuthClientResponse]
35
+ def update_client(client_id, params)
36
+ @admin._update_oauth_client(client_id, params)
37
+ end
38
+
39
+ # @param client_id [String] OAuth client UUID
40
+ def delete_client(client_id)
41
+ @admin._delete_oauth_client(client_id)
42
+ end
43
+
44
+ # @param client_id [String] OAuth client UUID
45
+ # @return [Types::OAuthClientResponse] response with rotated client_secret
46
+ def regenerate_client_secret(client_id)
47
+ @admin._regenerate_oauth_client_secret(client_id)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "faraday"
4
+ require "json"
5
+
6
+ module Supabase
7
+ module Auth
8
+ class Api
9
+ CONTENT_TYPE = "application/json;charset=UTF-8"
10
+ UUID_REGEX = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
11
+
12
+ attr_reader :url, :headers
13
+
14
+ # @param url [String] The GoTrue API base URL
15
+ # @param headers [Hash] Default headers to include on every request (e.g., apikey)
16
+ # @param http_client [Faraday::Connection, nil] Optional custom Faraday client
17
+ # @param verify [Boolean] Verify TLS certificates (default true)
18
+ # @param proxy [String, nil] HTTP proxy URL
19
+ # @param timeout [Numeric, nil] Per-request timeout in seconds
20
+ def initialize(url:, headers: {}, http_client: nil, verify: true, proxy: nil, timeout: nil)
21
+ @url = url
22
+ @headers = headers
23
+ @http_client = http_client
24
+ @verify = verify
25
+ @proxy = proxy
26
+ @timeout = timeout
27
+ end
28
+
29
+ # Central HTTP dispatch method. Builds URL, merges headers (including API version
30
+ # and Authorization), handles redirect_to as query param, parses JSON, applies
31
+ # optional transform, and maps errors via Helpers.handle_exception.
32
+ #
33
+ # @param method [String, Symbol] HTTP method (GET, POST, PUT, DELETE)
34
+ # @param path [String] Request path (relative to base URL)
35
+ # @param jwt [String, nil] Bearer token for Authorization header
36
+ # @param body [Hash, nil] Request body (serialized to JSON)
37
+ # @param params [Hash] Query parameters
38
+ # @param headers [Hash] Additional headers for this request
39
+ # @param redirect_to [String, nil] If present, added as redirect_to query param
40
+ # @param xform [Proc, nil] Optional transform function applied to parsed response
41
+ # @param no_resolve_json [Boolean] If true, return raw Faraday::Response
42
+ # @return [Hash, Object] Parsed JSON response, transformed result, or raw response
43
+ def _request(method, path, jwt: nil, body: nil, params: {}, headers: {}, redirect_to: nil, xform: nil, no_resolve_json: false)
44
+ merged_headers = @headers.merge(headers)
45
+ merged_headers["Content-Type"] ||= CONTENT_TYPE
46
+ merged_headers[Constants::API_VERSION_HEADER_NAME] ||= Constants::API_VERSIONS.keys.last
47
+ merged_headers["Authorization"] = "Bearer #{jwt}" if jwt
48
+
49
+ query = params.dup
50
+ query["redirect_to"] = redirect_to if redirect_to
51
+
52
+ full_path = build_path(path)
53
+ json_body = body ? JSON.generate(body) : nil
54
+
55
+ response = connection.run_request(method.to_s.downcase.to_sym, full_path, json_body, merged_headers) do |req|
56
+ req.params.update(query) unless query.empty?
57
+ end
58
+
59
+ result = no_resolve_json ? response : parse_response(response)
60
+
61
+ xform ? xform.call(result) : result
62
+ rescue Faraday::Error => e
63
+ raise Helpers.handle_exception(e)
64
+ rescue StandardError => e
65
+ raise Helpers.handle_exception(e)
66
+ end
67
+
68
+ # @param id [String] UUID to validate
69
+ # @raise [ArgumentError] if not a valid UUID format
70
+ def _validate_uuid(id)
71
+ unless id.is_a?(String) && id.match?(UUID_REGEX)
72
+ raise ArgumentError, "Invalid id, '#{id}' is not a valid uuid"
73
+ end
74
+ end
75
+
76
+ # Convenience methods that delegate to _request
77
+
78
+ def get(path, headers: {}, params: {})
79
+ _request(:get, path, headers: headers, params: params)
80
+ end
81
+
82
+ def post(path, body: {}, headers: {}, params: {})
83
+ _request(:post, path, body: body, headers: headers, params: params)
84
+ end
85
+
86
+ def put(path, body: {}, headers: {}, params: {})
87
+ _request(:put, path, body: body, headers: headers, params: params)
88
+ end
89
+
90
+ def delete(path, headers: {}, params: {})
91
+ _request(:delete, path, headers: headers, params: params)
92
+ end
93
+
94
+ private
95
+
96
+ def connection
97
+ @connection ||= @http_client || build_connection
98
+ end
99
+
100
+ def build_connection
101
+ Faraday.new(url: @url, ssl: { verify: @verify }, proxy: @proxy) do |f|
102
+ f.response :raise_error
103
+ if @timeout
104
+ f.options.timeout = @timeout
105
+ f.options.open_timeout = @timeout
106
+ end
107
+ f.adapter Faraday.default_adapter
108
+ end
109
+ end
110
+
111
+ def build_path(path)
112
+ base_path = URI.parse(@url).path.chomp("/")
113
+ "#{base_path}/#{path.sub(%r{^/}, '')}"
114
+ end
115
+
116
+ def parse_response(response)
117
+ return {} if response.body.nil? || response.body.empty?
118
+
119
+ JSON.parse(response.body)
120
+ rescue JSON::ParserError
121
+ {}
122
+ end
123
+ end
124
+ end
125
+ end