supabase-auth 0.1.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.
@@ -0,0 +1,435 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Supabase
6
+ module Auth
7
+ module Types
8
+ # Parse an ISO8601 string into a Time object, or return nil
9
+ # @param value [String, Time, nil]
10
+ # @return [Time, nil]
11
+ def self.parse_timestamp(value)
12
+ return nil if value.nil?
13
+ return value if value.is_a?(Time)
14
+
15
+ Time.parse(value.to_s)
16
+ end
17
+
18
+ # Authentication method reference entry (matches Python AMREntry)
19
+ AMREntry = Struct.new(
20
+ :method,
21
+ :timestamp,
22
+ keyword_init: true
23
+ ) do
24
+ def self.from_hash(hash)
25
+ return nil if hash.nil?
26
+
27
+ new(
28
+ method: hash["method"] || hash[:method],
29
+ timestamp: hash["timestamp"] || hash[:timestamp]
30
+ )
31
+ end
32
+ end
33
+
34
+ Factor = Struct.new(
35
+ :id,
36
+ :friendly_name,
37
+ :factor_type,
38
+ :status,
39
+ :created_at,
40
+ :updated_at,
41
+ keyword_init: true
42
+ ) do
43
+ def self.from_hash(hash)
44
+ return nil if hash.nil?
45
+
46
+ new(
47
+ id: hash["id"] || hash[:id],
48
+ friendly_name: hash["friendly_name"] || hash[:friendly_name],
49
+ factor_type: hash["factor_type"] || hash[:factor_type],
50
+ status: hash["status"] || hash[:status],
51
+ created_at: Types.parse_timestamp(hash["created_at"] || hash[:created_at]),
52
+ updated_at: Types.parse_timestamp(hash["updated_at"] || hash[:updated_at])
53
+ )
54
+ end
55
+ end
56
+
57
+ Identity = Struct.new(
58
+ :id,
59
+ :identity_id,
60
+ :user_id,
61
+ :identity_data,
62
+ :provider,
63
+ :last_sign_in_at,
64
+ :created_at,
65
+ :updated_at,
66
+ keyword_init: true
67
+ ) do
68
+ def self.from_hash(hash)
69
+ return nil if hash.nil?
70
+
71
+ new(
72
+ id: hash["id"] || hash[:id],
73
+ identity_id: hash["identity_id"] || hash[:identity_id],
74
+ user_id: hash["user_id"] || hash[:user_id],
75
+ identity_data: hash["identity_data"] || hash[:identity_data] || {},
76
+ provider: hash["provider"] || hash[:provider],
77
+ last_sign_in_at: Types.parse_timestamp(hash["last_sign_in_at"] || hash[:last_sign_in_at]),
78
+ created_at: Types.parse_timestamp(hash["created_at"] || hash[:created_at]),
79
+ updated_at: Types.parse_timestamp(hash["updated_at"] || hash[:updated_at])
80
+ )
81
+ end
82
+ end
83
+
84
+ User = Struct.new(
85
+ :id,
86
+ :aud,
87
+ :role,
88
+ :email,
89
+ :email_confirmed_at,
90
+ :phone,
91
+ :phone_confirmed_at,
92
+ :confirmed_at,
93
+ :last_sign_in_at,
94
+ :app_metadata,
95
+ :user_metadata,
96
+ :identities,
97
+ :factors,
98
+ :created_at,
99
+ :updated_at,
100
+ :new_email,
101
+ :new_phone,
102
+ :invited_at,
103
+ :is_anonymous,
104
+ :confirmation_sent_at,
105
+ :recovery_sent_at,
106
+ :email_change_sent_at,
107
+ :action_link,
108
+ keyword_init: true
109
+ ) do
110
+ def self.from_hash(hash)
111
+ return nil if hash.nil?
112
+
113
+ identities = (hash["identities"] || hash[:identities])&.map { |i| Identity.from_hash(i) }
114
+ factors = (hash["factors"] || hash[:factors])&.map { |f| Factor.from_hash(f) }
115
+
116
+ new(
117
+ id: hash["id"] || hash[:id],
118
+ aud: hash["aud"] || hash[:aud],
119
+ role: hash["role"] || hash[:role],
120
+ email: hash["email"] || hash[:email],
121
+ email_confirmed_at: Types.parse_timestamp(hash["email_confirmed_at"] || hash[:email_confirmed_at]),
122
+ phone: hash["phone"] || hash[:phone],
123
+ phone_confirmed_at: Types.parse_timestamp(hash["phone_confirmed_at"] || hash[:phone_confirmed_at]),
124
+ confirmed_at: Types.parse_timestamp(hash["confirmed_at"] || hash[:confirmed_at]),
125
+ last_sign_in_at: Types.parse_timestamp(hash["last_sign_in_at"] || hash[:last_sign_in_at]),
126
+ app_metadata: hash["app_metadata"] || hash[:app_metadata] || {},
127
+ user_metadata: hash["user_metadata"] || hash[:user_metadata] || {},
128
+ identities: identities,
129
+ factors: factors,
130
+ created_at: Types.parse_timestamp(hash["created_at"] || hash[:created_at]),
131
+ updated_at: Types.parse_timestamp(hash["updated_at"] || hash[:updated_at]),
132
+ new_email: hash["new_email"] || hash[:new_email],
133
+ new_phone: hash["new_phone"] || hash[:new_phone],
134
+ invited_at: Types.parse_timestamp(hash["invited_at"] || hash[:invited_at]),
135
+ is_anonymous: hash.key?("is_anonymous") ? hash["is_anonymous"] : (hash.key?(:is_anonymous) ? hash[:is_anonymous] : false),
136
+ confirmation_sent_at: Types.parse_timestamp(hash["confirmation_sent_at"] || hash[:confirmation_sent_at]),
137
+ recovery_sent_at: Types.parse_timestamp(hash["recovery_sent_at"] || hash[:recovery_sent_at]),
138
+ email_change_sent_at: Types.parse_timestamp(hash["email_change_sent_at"] || hash[:email_change_sent_at]),
139
+ action_link: hash["action_link"] || hash[:action_link]
140
+ )
141
+ end
142
+ end
143
+
144
+ Session = Struct.new(
145
+ :provider_token,
146
+ :provider_refresh_token,
147
+ :access_token,
148
+ :refresh_token,
149
+ :token_type,
150
+ :expires_in,
151
+ :expires_at,
152
+ :user,
153
+ keyword_init: true
154
+ ) do
155
+ def self.from_hash(hash)
156
+ return nil if hash.nil?
157
+
158
+ expires_at = hash["expires_at"] || hash[:expires_at]
159
+ expires_in = hash["expires_in"] || hash[:expires_in]
160
+ if expires_in && !expires_at
161
+ expires_at = Time.now.to_i + expires_in.to_i
162
+ end
163
+ expires_at = expires_at.to_i if expires_at
164
+
165
+ new(
166
+ provider_token: hash["provider_token"] || hash[:provider_token],
167
+ provider_refresh_token: hash["provider_refresh_token"] || hash[:provider_refresh_token],
168
+ access_token: hash["access_token"] || hash[:access_token],
169
+ refresh_token: hash["refresh_token"] || hash[:refresh_token],
170
+ token_type: hash["token_type"] || hash[:token_type],
171
+ expires_in: hash["expires_in"] || hash[:expires_in],
172
+ expires_at: expires_at,
173
+ user: User.from_hash(hash["user"] || hash[:user])
174
+ )
175
+ end
176
+ end
177
+
178
+ AuthResponse = Struct.new(
179
+ :user,
180
+ :session,
181
+ keyword_init: true
182
+ ) do
183
+ def self.from_hash(hash)
184
+ return nil if hash.nil?
185
+
186
+ new(
187
+ user: User.from_hash(hash["user"] || hash[:user]),
188
+ session: Session.from_hash(hash["session"] || hash[:session])
189
+ )
190
+ end
191
+ end
192
+
193
+ OAuthResponse = Struct.new(
194
+ :provider,
195
+ :url,
196
+ keyword_init: true
197
+ )
198
+
199
+ GenerateLinkProperties = Struct.new(
200
+ :action_link,
201
+ :email_otp,
202
+ :hashed_token,
203
+ :redirect_to,
204
+ :verification_type,
205
+ keyword_init: true
206
+ )
207
+
208
+ GenerateLinkResponse = Struct.new(
209
+ :properties,
210
+ :user,
211
+ keyword_init: true
212
+ )
213
+
214
+ UserResponse = Struct.new(
215
+ :user,
216
+ keyword_init: true
217
+ ) do
218
+ def self.from_hash(hash)
219
+ return nil if hash.nil?
220
+
221
+ new(user: User.from_hash(hash["user"] || hash[:user]))
222
+ end
223
+ end
224
+
225
+ SSOResponse = Struct.new(
226
+ :url,
227
+ keyword_init: true
228
+ ) do
229
+ def self.from_hash(hash)
230
+ return nil if hash.nil?
231
+
232
+ new(url: hash["url"] || hash[:url])
233
+ end
234
+ end
235
+
236
+ LinkIdentityResponse = Struct.new(
237
+ :url,
238
+ keyword_init: true
239
+ ) do
240
+ def self.from_hash(hash)
241
+ return nil if hash.nil?
242
+
243
+ new(url: hash["url"] || hash[:url])
244
+ end
245
+ end
246
+
247
+ AuthOtpResponse = Struct.new(
248
+ :message_id,
249
+ :user,
250
+ :session,
251
+ keyword_init: true
252
+ ) do
253
+ def self.from_hash(hash)
254
+ return nil if hash.nil?
255
+
256
+ new(
257
+ message_id: hash["message_id"] || hash[:message_id],
258
+ user: hash.key?("user") || hash.key?(:user) ? User.from_hash(hash["user"] || hash[:user]) : nil,
259
+ session: hash.key?("session") || hash.key?(:session) ? Session.from_hash(hash["session"] || hash[:session]) : nil
260
+ )
261
+ end
262
+ end
263
+ # MFA Enroll response - returned when enrolling a new TOTP factor
264
+ AuthMFAEnrollResponse = Struct.new(
265
+ :id,
266
+ :type,
267
+ :friendly_name,
268
+ :totp,
269
+ :phone,
270
+ keyword_init: true
271
+ ) do
272
+ def self.from_hash(hash)
273
+ return nil if hash.nil?
274
+
275
+ totp = hash["totp"] || hash[:totp]
276
+ totp_struct = totp ? MFATotpInfo.new(qr_code: totp["qr_code"] || totp[:qr_code],
277
+ secret: totp["secret"] || totp[:secret],
278
+ uri: totp["uri"] || totp[:uri]) : nil
279
+ new(
280
+ id: hash["id"] || hash[:id],
281
+ type: hash["type"] || hash[:type],
282
+ friendly_name: hash["friendly_name"] || hash[:friendly_name],
283
+ totp: totp_struct,
284
+ phone: hash["phone"] || hash[:phone]
285
+ )
286
+ end
287
+ end
288
+
289
+ AuthMFAEnrollResponseTotp = Struct.new(:qr_code, :secret, :uri, keyword_init: true)
290
+ MFATotpInfo = AuthMFAEnrollResponseTotp
291
+
292
+ # MFA Challenge response
293
+ AuthMFAChallengeResponse = Struct.new(
294
+ :id,
295
+ :factor_type,
296
+ :expires_at,
297
+ keyword_init: true
298
+ ) do
299
+ def self.from_hash(hash)
300
+ return nil if hash.nil?
301
+
302
+ new(
303
+ id: hash["id"] || hash[:id],
304
+ factor_type: hash["factor_type"] || hash[:factor_type] || hash["type"] || hash[:type],
305
+ expires_at: hash["expires_at"] || hash[:expires_at]
306
+ )
307
+ end
308
+ end
309
+
310
+ # MFA Unenroll response
311
+ AuthMFAUnenrollResponse = Struct.new(
312
+ :id,
313
+ keyword_init: true
314
+ ) do
315
+ def self.from_hash(hash)
316
+ return nil if hash.nil?
317
+
318
+ new(id: hash["id"] || hash[:id])
319
+ end
320
+ end
321
+
322
+ # MFA List Factors response
323
+ AuthMFAListFactorsResponse = Struct.new(
324
+ :all,
325
+ :totp,
326
+ :phone,
327
+ keyword_init: true
328
+ )
329
+
330
+ # MFA Verify response (matches Python: access_token, token_type, expires_in, refresh_token, user)
331
+ AuthMFAVerifyResponse = Struct.new(
332
+ :access_token,
333
+ :token_type,
334
+ :expires_in,
335
+ :refresh_token,
336
+ :user,
337
+ keyword_init: true
338
+ ) do
339
+ def self.from_hash(hash)
340
+ return nil if hash.nil?
341
+
342
+ new(
343
+ access_token: hash["access_token"] || hash[:access_token],
344
+ token_type: hash["token_type"] || hash[:token_type],
345
+ expires_in: hash["expires_in"] || hash[:expires_in],
346
+ refresh_token: hash["refresh_token"] || hash[:refresh_token],
347
+ user: User.from_hash(hash["user"] || hash[:user])
348
+ )
349
+ end
350
+ end
351
+
352
+ # MFA Get Authenticator Assurance Level response
353
+ AuthMFAGetAuthenticatorAssuranceLevelResponse = Struct.new(
354
+ :current_level,
355
+ :next_level,
356
+ :current_authentication_methods,
357
+ keyword_init: true
358
+ )
359
+
360
+ # Admin MFA List Factors response
361
+ AuthMFAAdminListFactorsResponse = Struct.new(
362
+ :factors,
363
+ keyword_init: true
364
+ ) do
365
+ def self.from_hash(hash)
366
+ return nil if hash.nil?
367
+
368
+ factors = (hash["factors"] || hash[:factors] || []).map { |f| Factor.from_hash(f) }
369
+ new(factors: factors)
370
+ end
371
+ end
372
+
373
+ # Admin MFA Delete Factor response
374
+ AuthMFAAdminDeleteFactorResponse = Struct.new(
375
+ :id,
376
+ keyword_init: true
377
+ ) do
378
+ def self.from_hash(hash)
379
+ return nil if hash.nil?
380
+
381
+ new(id: hash["id"] || hash[:id])
382
+ end
383
+ end
384
+
385
+ # Identities response (wraps user identities)
386
+ IdentitiesResponse = Struct.new(
387
+ :identities,
388
+ keyword_init: true
389
+ )
390
+
391
+ # User Identity (includes identity_id)
392
+ UserIdentity = Struct.new(
393
+ :id,
394
+ :identity_id,
395
+ :user_id,
396
+ :identity_data,
397
+ :provider,
398
+ :created_at,
399
+ :last_sign_in_at,
400
+ :updated_at,
401
+ keyword_init: true
402
+ ) do
403
+ def self.from_hash(hash)
404
+ return nil if hash.nil?
405
+
406
+ new(
407
+ id: hash["id"] || hash[:id],
408
+ identity_id: hash["identity_id"] || hash[:identity_id],
409
+ user_id: hash["user_id"] || hash[:user_id],
410
+ identity_data: hash["identity_data"] || hash[:identity_data] || {},
411
+ provider: hash["provider"] || hash[:provider],
412
+ created_at: Types.parse_timestamp(hash["created_at"] || hash[:created_at]),
413
+ last_sign_in_at: Types.parse_timestamp(hash["last_sign_in_at"] || hash[:last_sign_in_at]),
414
+ updated_at: Types.parse_timestamp(hash["updated_at"] || hash[:updated_at])
415
+ )
416
+ end
417
+ end
418
+
419
+ Subscription = Struct.new(
420
+ :id,
421
+ :callback,
422
+ :unsubscribe,
423
+ keyword_init: true
424
+ )
425
+
426
+ # JWT Claims response - returned by get_claims (matches Python ClaimsResponse)
427
+ ClaimsResponse = Struct.new(
428
+ :claims,
429
+ :headers,
430
+ :signature,
431
+ keyword_init: true
432
+ )
433
+ end
434
+ end
435
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Supabase
4
+ module Auth
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "auth/version"
4
+ require_relative "auth/constants"
5
+ require_relative "auth/types"
6
+ require_relative "auth/errors"
7
+ require_relative "auth/api"
8
+ require_relative "auth/admin_api"
9
+ require_relative "auth/helpers"
10
+ require_relative "auth/storage"
11
+ require_relative "auth/memory_storage"
12
+ require_relative "auth/timer"
13
+ require_relative "auth/client"
14
+
15
+ module Supabase
16
+ module Auth
17
+ end
18
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "supabase/auth"
metadata ADDED
@@ -0,0 +1,159 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: supabase-auth
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Bogdan Tarasenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-03-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-multipart
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: jwt
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.12'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.12'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.22'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.22'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.19'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.19'
97
+ - !ruby/object:Gem::Dependency
98
+ name: faker
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.2'
111
+ description: A Ruby gem implementing a client for Supabase Auth (GoTrue API), with
112
+ adaptations for Ruby idioms.
113
+ email:
114
+ - bogdantarasenkozp@gmail.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - README.md
120
+ - lib/supabase-auth.rb
121
+ - lib/supabase/auth.rb
122
+ - lib/supabase/auth/admin_api.rb
123
+ - lib/supabase/auth/api.rb
124
+ - lib/supabase/auth/client.rb
125
+ - lib/supabase/auth/constants.rb
126
+ - lib/supabase/auth/errors.rb
127
+ - lib/supabase/auth/helpers.rb
128
+ - lib/supabase/auth/memory_storage.rb
129
+ - lib/supabase/auth/storage.rb
130
+ - lib/supabase/auth/timer.rb
131
+ - lib/supabase/auth/types.rb
132
+ - lib/supabase/auth/version.rb
133
+ homepage: https://github.com/bogdantarasenko/supabase-rb
134
+ licenses:
135
+ - MIT
136
+ metadata:
137
+ homepage_uri: https://github.com/bogdantarasenko/supabase-rb
138
+ source_code_uri: https://github.com/bogdantarasenko/supabase-rb
139
+ changelog_uri: https://github.com/bogdantarasenko/supabase-rb/blob/main/CHANGELOG.md
140
+ post_install_message:
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: 3.0.0
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubygems_version: 3.0.3.1
156
+ signing_key:
157
+ specification_version: 4
158
+ summary: Ruby client for Supabase Auth (GoTrue API)
159
+ test_files: []