clowk 0.1.0 → 0.3.3

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: 56e3503c335ec716e89e49428b66a0c4157be9cb6ff19c46095117062f4c7848
4
- data.tar.gz: 1db10f568e025bc52ea97e255f9e4cdc8064507ace6adf83e74d78d20d5b5d9e
3
+ metadata.gz: 7b02c176f355917aa070a2b183a5844911e769b49ff7906cc40c13bbcd68cbf6
4
+ data.tar.gz: 7ba591ce54755e2ff3d6a586ef7942ae96442efa6ae93caff5c64e0ac313cbd1
5
5
  SHA512:
6
- metadata.gz: 3ed1c6bb69c0644f5476374f8004ce865c1661aeec3729b42a59442ee453e6e431c23583d603ccf49b9e045ae2c0d7e9733f2482a040ef3418c069e6f88ef531
7
- data.tar.gz: 393ed6db1f3777cc402731fe16e0874d64c842ab3b5118aa49c9a79e5b208f38a660b4bbcca45ee349eccf1f1be030ac35ff6e235310edbc9e14d57cd730a24b
6
+ metadata.gz: f33e323f94c88fc064d12d0caa0cbc87bf9fad301ba0e29fc89d51ec0f40e81658e7dc67f485e437281705b0cdd10c6157b927500d0afd255f151d48485188ad
7
+ data.tar.gz: 9c6e02049ac043a85e293ad4ac8ec060d4f90e2b1231931cb593a6287a73b1400bdb6c4c01e18cf20fe090cf5be43336eeade52bb495c8d24074878b0a4c688f
data/README.md CHANGED
@@ -86,7 +86,7 @@ Clowk.configure do |config|
86
86
  config.after_sign_in_path = '/'
87
87
  config.after_sign_out_path = '/'
88
88
 
89
- config.api_base_url = 'https://api.clowk.dev/client/v1'
89
+ config.api_base_url = 'https://api.clowk.dev/api/v1'
90
90
  config.callback_path = '/clowk/oauth/callback'
91
91
  config.mount_path = '/clowk'
92
92
 
@@ -362,4 +362,4 @@ Its job is to make the Rails side of Clowk integration predictable:
362
362
 
363
363
  ## License
364
364
 
365
- MIT. See `LICENSE`.
365
+ AGPL-3.0. See `LICENSE`.
data/clowk.gemspec CHANGED
@@ -1,31 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lib/clowk/version'
3
+ require_relative "lib/clowk/version"
4
4
 
5
5
  Gem::Specification.new do |spec|
6
- spec.name = 'clowk'
6
+ spec.name = "clowk"
7
7
  spec.version = Clowk::VERSION
8
- spec.authors = ['Clowk']
9
- spec.email = ['support@clowk.in']
8
+ spec.authors = ["Clowk"]
9
+ spec.email = ["support@clowk.in"]
10
10
 
11
- spec.summary = 'Rails SDK for Clowk authentication'
12
- spec.description = 'Clowk Authentication, JWT verification, and future API access'
13
- spec.homepage = 'https://clowk.in'
14
- spec.license = 'AGPL-3.0'
15
- spec.required_ruby_version = '>= 3.3'
11
+ spec.summary = "Rails SDK for Clowk authentication"
12
+ spec.description = "Clowk Authentication, JWT verification, and future API access"
13
+ spec.homepage = "https://clowk.in"
14
+ spec.license = "AGPL-3.0-only"
15
+ spec.required_ruby_version = ">= 3.3"
16
16
  spec.metadata = {
17
- 'rubygems_mfa_required' => 'true'
17
+ "rubygems_mfa_required" => "true"
18
18
  }
19
19
 
20
20
  spec.files = Dir.chdir(__dir__) do
21
- Dir['README.md', 'clowk.gemspec', 'config/routes.rb', 'lib/**/*.rb']
21
+ Dir["README.md", "clowk.gemspec", "config/routes.rb", "lib/**/*.rb"]
22
22
  end
23
23
 
24
- spec.require_paths = ['lib']
24
+ spec.require_paths = ["lib"]
25
25
 
26
- spec.add_dependency 'activesupport', '>= 7.0'
27
- spec.add_dependency 'jwt', '>= 2.7', '< 3.0'
28
- spec.add_dependency 'railties', '>= 7.0'
26
+ spec.add_dependency "activesupport", ">= 7.0"
27
+ spec.add_dependency "jwt", ">= 2.7", "< 3.0"
28
+ spec.add_dependency "railties", ">= 7.0"
29
29
 
30
- spec.add_development_dependency 'rspec', '>= 3.13', '< 4.0'
30
+ spec.add_development_dependency "rspec", ">= 3.13", "< 4.0"
31
+ spec.add_development_dependency "standard", ">= 1.0"
31
32
  end
data/config/routes.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Clowk::Engine.routes.draw do
4
- get '/sign_in', to: 'sessions#new', as: :sign_in
5
- get '/sign_up', to: 'sessions#sign_up', as: :sign_up
6
- match '/sign_out', to: 'sessions#destroy', via: %i(get delete), as: :sign_out
7
- get '/oauth/callback', to: 'callbacks#show', as: :auth_callback
4
+ get "/sign_in", to: "sessions#new", as: :sign_in
5
+ get "/sign_up", to: "sessions#sign_up", as: :sign_up
6
+ match "/sign_out", to: "sessions#destroy", via: %i[get delete], as: :sign_out
7
+ get "/oauth/callback", to: "callbacks#show", as: :auth_callback
8
8
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/concern'
3
+ require "active_support/concern"
4
4
 
5
5
  module Clowk
6
6
  module Authenticable
@@ -12,17 +12,31 @@ module Clowk
12
12
  authenticate_method = :"authenticate_#{scope}!"
13
13
  signed_in_method = :"#{scope}_signed_in?"
14
14
 
15
+ enforce_session_method = :"#{scope}_enforce_session!"
16
+
15
17
  base.class_eval do
16
- define_method(current_method) do
17
- clowk_current_resource
18
+ unless current_method == :clowk_current_resource
19
+ define_method(current_method) do
20
+ clowk_current_resource
21
+ end
22
+ end
23
+
24
+ unless authenticate_method == :clowk_authenticate!
25
+ define_method(authenticate_method) do
26
+ clowk_authenticate!
27
+ end
18
28
  end
19
29
 
20
- define_method(authenticate_method) do
21
- clowk_authenticate!
30
+ unless signed_in_method == :clowk_signed_in?
31
+ define_method(signed_in_method) do
32
+ clowk_signed_in?
33
+ end
22
34
  end
23
35
 
24
- define_method(signed_in_method) do
25
- clowk_current_resource.present?
36
+ unless enforce_session_method == :clowk_enforce_session!
37
+ define_method(enforce_session_method) do
38
+ clowk_enforce_session!
39
+ end
26
40
  end
27
41
 
28
42
  helper_method current_method, authenticate_method, signed_in_method, :current_token if respond_to?(:helper_method)
@@ -41,21 +55,40 @@ module Clowk
41
55
  end
42
56
 
43
57
  def current_token
44
- stored_session&.dig('token') || extracted_token
58
+ stored_session&.dig("token") || extracted_token
45
59
  end
46
60
 
47
61
  def clowk_signed_in?
48
62
  clowk_current_resource.present?
49
63
  end
50
64
 
65
+ def clowk_session_status
66
+ @clowk_session_status ||= resolve_session_status
67
+ end
68
+
69
+ def clowk_session_active?
70
+ clowk_session_status&.dig(:status) == "active"
71
+ end
72
+
73
+ def clowk_enforce_session!
74
+ return if clowk_session_active?
75
+
76
+ session_info = clowk_session_status
77
+ callback = Clowk.config.on_session_expired
78
+
79
+ if callback.respond_to?(:call)
80
+ callback.call(self, session_info)
81
+
82
+ return
83
+ end
84
+
85
+ clowk_handle_expired_session(session_info)
86
+ end
87
+
51
88
  def clowk_authenticate!
52
89
  return clowk_current_resource if clowk_signed_in?
53
90
 
54
- if request.format.json?
55
- render json: { error: 'Unauthorized' }, status: :unauthorized
56
- else
57
- redirect_to clowk_sign_in_path(return_to: request.fullpath)
58
- end
91
+ clowk_handle_unauthenticated
59
92
  end
60
93
 
61
94
  def clowk_sign_out!
@@ -67,6 +100,22 @@ module Clowk
67
100
 
68
101
  private
69
102
 
103
+ def clowk_handle_unauthenticated
104
+ if request.format.json?
105
+ render json: {error: "Unauthorized"}, status: :unauthorized
106
+ else
107
+ redirect_to clowk_sign_in_path(return_to: request.fullpath)
108
+ end
109
+ end
110
+
111
+ def clowk_handle_expired_session(_session_info)
112
+ if request.format.json?
113
+ render json: {error: "Session expired or inactive"}, status: :unauthorized
114
+ else
115
+ redirect_to clowk_sign_in_path(return_to: request.fullpath)
116
+ end
117
+ end
118
+
70
119
  def verified_request_payload
71
120
  return unless extracted_token
72
121
 
@@ -90,13 +139,13 @@ module Clowk
90
139
  end
91
140
 
92
141
  def stored_user_payload
93
- payload = stored_session&.dig('user') || stored_session&.dig(:user)
142
+ payload = stored_session&.dig("user") || stored_session&.dig(:user)
94
143
  payload&.deep_symbolize_keys
95
144
  end
96
145
 
97
146
  def persist_clowk_session(token, payload)
98
147
  session[Clowk.config.session_key] = {
99
- token: token,
148
+ token:,
100
149
  user: payload,
101
150
  signed_in_at: Time.now.to_i
102
151
  }
@@ -108,5 +157,26 @@ module Clowk
108
157
  secure: request.ssl?
109
158
  }
110
159
  end
160
+
161
+ def resolve_session_status
162
+ cached = stored_session&.dig("session_status") || stored_session&.dig(:session_status)
163
+
164
+ return cached&.deep_symbolize_keys if cached
165
+
166
+ resource = clowk_current_resource
167
+
168
+ return unless resource&.session_id
169
+ return unless Clowk.config.secret_key.present?
170
+
171
+ client = Clowk::SDK::Client.new(secret_key: Clowk.config.secret_key)
172
+ result = client.tokens.verify_with_session(token: current_token)
173
+ status = result&.dig(:session)
174
+
175
+ session[Clowk.config.session_key] = stored_session.merge("session_status" => status) if status && stored_session
176
+
177
+ status
178
+ rescue Clowk::InvalidTokenError
179
+ nil
180
+ end
111
181
  end
112
182
  end
@@ -4,8 +4,6 @@ module Clowk
4
4
  class Configuration
5
5
  attr_accessor :api_base_url
6
6
  attr_accessor :app_base_url
7
- attr_accessor :after_sign_in_path
8
- attr_accessor :after_sign_out_path
9
7
  attr_accessor :callback_path
10
8
  attr_accessor :cookie_key
11
9
  attr_accessor :http_logger
@@ -22,25 +20,60 @@ module Clowk
22
20
  attr_accessor :session_key
23
21
  attr_accessor :subdomain_url
24
22
  attr_accessor :token_param
23
+ attr_accessor :enforce_active_session
24
+ attr_accessor :on_session_expired
25
25
 
26
26
  def initialize
27
- @api_base_url = 'https://api.clowk.dev/client/v1'
28
- @app_base_url = 'https://app.clowk.in'
29
- @after_sign_in_path = '/'
30
- @after_sign_out_path = '/'
31
- @mount_path = '/clowk'
32
- @callback_path = '/clowk/oauth/callback'
33
- @cookie_key = 'clowk_token'
27
+ @api_base_url = "https://api.clowk.dev/api/v1"
28
+ @app_base_url = "https://app.clowk.in"
29
+ @after_sign_in_path = "/"
30
+ @after_sign_out_path = "/"
31
+ @mount_path = "/clowk"
32
+ @callback_path = "/clowk/oauth/callback"
33
+ @cookie_key = "clowk_token"
34
34
  @http_logger = nil
35
35
  @http_open_timeout = 5
36
36
  @http_read_timeout = 10
37
37
  @http_retry_attempts = 2
38
38
  @http_retry_interval = 0.05
39
39
  @http_write_timeout = 10
40
- @issuer = 'clowk'
40
+ @issuer = "clowk"
41
41
  @session_key = :clowk
42
42
  @prefix_by = :clowk
43
43
  @token_param = :token
44
+ @enforce_active_session = false
45
+ @on_session_expired = nil
46
+ end
47
+
48
+ def after_sign_in_path
49
+ resolve_path(@after_sign_in_path)
50
+ end
51
+
52
+ def after_sign_out_path
53
+ resolve_path(@after_sign_out_path)
54
+ end
55
+
56
+ attr_writer :after_sign_in_path
57
+
58
+ attr_writer :after_sign_out_path
59
+
60
+ def validate!
61
+ errors = []
62
+ errors << "secret_key must be a String" unless @secret_key.is_a?(String) || @secret_key.nil?
63
+ errors << "http_open_timeout must be Numeric" unless @http_open_timeout.is_a?(Numeric)
64
+ errors << "http_read_timeout must be Numeric" unless @http_read_timeout.is_a?(Numeric)
65
+ errors << "http_write_timeout must be Numeric" unless @http_write_timeout.is_a?(Numeric)
66
+ errors << "http_retry_attempts must be a non-negative Integer" unless @http_retry_attempts.is_a?(Integer) && @http_retry_attempts >= 0
67
+
68
+ raise ConfigurationError, errors.join(", ") if errors.any?
69
+
70
+ true
71
+ end
72
+
73
+ private
74
+
75
+ def resolve_path(path_or_callable)
76
+ path_or_callable.respond_to?(:call) ? path_or_callable.call : path_or_callable
44
77
  end
45
78
  end
46
79
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'securerandom'
4
- require 'uri'
3
+ require "securerandom"
4
+ require "uri"
5
5
 
6
6
  module Clowk
7
7
  class BaseController < ActionController::Base
@@ -13,18 +13,18 @@ module Clowk
13
13
  private
14
14
 
15
15
  def redirect_back_or(default, return_to: params[:return_to])
16
- redirect_target = safe_redirect_path(return_to) || safe_redirect_path(default) || '/'
16
+ redirect_target = safe_redirect_path(return_to) || safe_redirect_path(default) || "/"
17
17
 
18
18
  redirect_to redirect_target
19
19
  end
20
20
 
21
21
  def start_clowk_auth_flow!(return_to: nil)
22
- sanitized_return_to = safe_redirect_path(return_to) || safe_redirect_path(Clowk.config.after_sign_in_path) || '/'
22
+ sanitized_return_to = safe_redirect_path(return_to) || safe_redirect_path(Clowk.config.after_sign_in_path) || "/"
23
23
  state = SecureRandom.hex(32)
24
24
 
25
25
  session[:clowk_auth_flow] = {
26
- 'state' => state,
27
- 'return_to' => sanitized_return_to
26
+ "state" => state,
27
+ "return_to" => sanitized_return_to
28
28
  }
29
29
 
30
30
  state
@@ -38,9 +38,9 @@ module Clowk
38
38
  end
39
39
 
40
40
  def validate_clowk_state!(expected_state, actual_state)
41
- raise Clowk::InvalidStateError, 'missing state' if actual_state.blank?
42
- raise Clowk::InvalidStateError, 'missing state' if expected_state.blank?
43
- raise Clowk::InvalidStateError, 'invalid state' unless state_matches?(expected_state, actual_state)
41
+ raise Clowk::InvalidStateError, "missing state" if actual_state.blank?
42
+ raise Clowk::InvalidStateError, "missing state" if expected_state.blank?
43
+ raise Clowk::InvalidStateError, "invalid state" unless state_matches?(expected_state, actual_state)
44
44
  end
45
45
 
46
46
  def state_matches?(expected_state, actual_state)
@@ -53,7 +53,7 @@ module Clowk
53
53
  value = candidate.to_s
54
54
  return if value.empty?
55
55
 
56
- return value if value.start_with?('/') && !value.start_with?('//')
56
+ return value if value.start_with?("/") && !value.start_with?("//")
57
57
 
58
58
  uri = URI.parse(value)
59
59
  return unless uri.host == request.host && uri.scheme == request.scheme
@@ -4,20 +4,21 @@ module Clowk
4
4
  class CallbacksController < BaseController
5
5
  def show
6
6
  flow = consume_clowk_auth_flow!
7
- validate_clowk_state!(flow['state'], params[:state])
7
+ validate_clowk_state!(flow["state"], params[:state])
8
8
 
9
9
  token = params[Clowk.config.token_param]
10
- raise Clowk::InvalidTokenError, 'missing token' if token.blank?
10
+ raise Clowk::InvalidTokenError, "missing token" if token.blank?
11
11
 
12
12
  payload = Clowk::JwtVerifier.new.verify(token)
13
- return_to = flow['return_to']
13
+ return_to = flow["return_to"]
14
14
 
15
15
  reset_clowk_session!
16
16
  persist_clowk_session(token, payload)
17
17
 
18
18
  redirect_back_or(Clowk.config.after_sign_in_path, return_to:)
19
19
  rescue Clowk::InvalidTokenError, Clowk::InvalidStateError => e
20
- flash[:alert] = "Clowk authentication failed: #{e.message}"
20
+ Rails.logger.error("[Clowk] Authentication failed: #{e.class} - #{e.message}")
21
+ flash[:alert] = "Authentication failed. Please try again."
21
22
 
22
23
  redirect_back_or(Clowk.config.after_sign_out_path, return_to: nil)
23
24
  end
data/lib/clowk/current.rb CHANGED
@@ -36,6 +36,10 @@ module Clowk
36
36
  attributes[:app_id]
37
37
  end
38
38
 
39
+ def session_id
40
+ attributes[:session_id]
41
+ end
42
+
39
43
  def [](key)
40
44
  attributes[key.to_sym]
41
45
  end
@@ -43,5 +47,19 @@ module Clowk
43
47
  def to_h
44
48
  attributes.merge(id: id)
45
49
  end
50
+
51
+ def ==(other)
52
+ return false unless other.is_a?(Current)
53
+
54
+ to_h == other.to_h
55
+ end
56
+
57
+ def eql?(other)
58
+ self == other
59
+ end
60
+
61
+ def hash
62
+ to_h.hash
63
+ end
46
64
  end
47
- end
65
+ end
data/lib/clowk/engine.rb CHANGED
@@ -4,7 +4,7 @@ module Clowk
4
4
  class Engine < ::Rails::Engine
5
5
  isolate_namespace Clowk
6
6
 
7
- initializer 'clowk.helpers' do
7
+ initializer "clowk.helpers" do
8
8
  ActiveSupport.on_load(:action_controller_base) do
9
9
  include Clowk::Helpers::UrlHelpers
10
10
  end
@@ -1,20 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cgi'
4
-
5
3
  module Clowk
6
4
  module Helpers
7
5
  module UrlHelpers
8
6
  def clowk_sign_in_path(return_to: nil)
9
- append_query(clowk_local_path('/sign_in'), return_to:)
7
+ append_query(clowk_local_path("/sign_in"), return_to:)
10
8
  end
11
9
 
12
10
  def clowk_sign_up_path(return_to: nil)
13
- append_query(clowk_local_path('/sign_up'), return_to:)
11
+ append_query(clowk_local_path("/sign_up"), return_to:)
14
12
  end
15
13
 
16
14
  def clowk_sign_out_path(return_to: nil)
17
- append_query(clowk_local_path('/sign_out'), return_to:)
15
+ append_query(clowk_local_path("/sign_out"), return_to:)
18
16
  end
19
17
 
20
18
  def clowk_callback_url(return_to: nil, state: nil)
@@ -22,18 +20,18 @@ module Clowk
22
20
  end
23
21
 
24
22
  def clowk_sign_in_url(redirect_to: nil, return_to: nil, state: nil)
25
- clowk_remote_auth_url('sign-in', redirect_to:, return_to:, state:)
23
+ clowk_remote_auth_url("sign-in", redirect_to:, return_to:, state:)
26
24
  end
27
25
 
28
26
  def clowk_sign_up_url(redirect_to: nil, return_to: nil, state: nil)
29
- clowk_remote_auth_url('sign-up', redirect_to:, return_to:, state:)
27
+ clowk_remote_auth_url("sign-up", redirect_to:, return_to:, state:)
30
28
  end
31
29
 
32
30
  private
33
31
 
34
32
  def clowk_remote_auth_url(action, redirect_to:, return_to:, state:)
35
33
  callback_url = clowk_callback_url(return_to: redirect_to || return_to, state:)
36
- query = { redirect_uri: callback_url }
34
+ query = {redirect_uri: callback_url}
37
35
 
38
36
  append_query("#{clowk_instance_base_url}/#{action}", query)
39
37
  end
@@ -50,7 +48,7 @@ module Clowk
50
48
  filtered = params.compact.reject { |_key, value| value.respond_to?(:empty?) && value.empty? }
51
49
  return url if filtered.empty?
52
50
 
53
- separator = url.include?('?') ? '&' : '?'
51
+ separator = url.include?("?") ? "&" : "?"
54
52
  "#{url}#{separator}#{Rack::Utils.build_query(filtered)}"
55
53
  end
56
54
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'net/http'
5
- require 'uri'
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
6
 
7
7
  module Clowk
8
8
  class Http
@@ -121,7 +121,7 @@ module Clowk
121
121
 
122
122
  request.body = JSON.generate(env[:body]) unless env[:body].nil?
123
123
 
124
- raw_response = Net::HTTP.start(env[:uri].host, env[:uri].port, use_ssl: env[:uri].scheme == 'https') do |http|
124
+ raw_response = Net::HTTP.start(env[:uri].host, env[:uri].port, use_ssl: env[:uri].scheme == "https") do |http|
125
125
  apply_timeouts(http, env[:timeouts])
126
126
  http.request(request)
127
127
  end
@@ -144,12 +144,12 @@ module Clowk
144
144
  end
145
145
 
146
146
  def normalize_path(path)
147
- path.to_s.start_with?('/') ? path : "/#{path}"
147
+ path.to_s.start_with?("/") ? path : "/#{path}"
148
148
  end
149
149
 
150
150
  def join_paths(base_path, extra_path)
151
- segments = [base_path.to_s, extra_path.to_s].map { |segment| segment.gsub(%r{^/+|/+$}, '') }.reject(&:empty?)
152
- "/#{segments.join('/')}"
151
+ segments = [base_path.to_s, extra_path.to_s].map { |segment| segment.gsub(%r{^/+|/+$}, "") }.reject(&:empty?)
152
+ "/#{segments.join("/")}"
153
153
  end
154
154
 
155
155
  def apply_headers(request, request_headers)
@@ -167,8 +167,8 @@ module Clowk
167
167
 
168
168
  def default_headers
169
169
  {
170
- 'Accept' => 'application/json',
171
- 'Content-Type' => 'application/json'
170
+ "Accept" => "application/json",
171
+ "Content-Type" => "application/json"
172
172
  }
173
173
  end
174
174
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'logger'
3
+ require "logger"
4
4
 
5
5
  module Clowk
6
6
  class Http
@@ -22,7 +22,8 @@ module Clowk
22
22
  attr_reader :app, :logger
23
23
 
24
24
  class NullLogger
25
- def info(*); end
25
+ def info(*)
26
+ end
26
27
  end
27
28
  end
28
29
  end
@@ -27,17 +27,17 @@ module Clowk
27
27
  body: body,
28
28
  body_parsed: body_parsed,
29
29
  headers: headers,
30
- success?: success?
30
+ success: success?
31
31
  }
32
32
  end
33
33
 
34
34
  def ==(other)
35
- if other.respond_to?(:to_h)
36
- to_h == other.to_h
35
+ to_h == if other.respond_to?(:to_h)
36
+ other.to_h
37
37
  else
38
- to_h == other
38
+ other
39
39
  end
40
40
  end
41
41
  end
42
42
  end
43
- end
43
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'net/http'
3
+ require "net/http"
4
4
 
5
5
  module Clowk
6
6
  class Http
@@ -44,4 +44,4 @@ module Clowk
44
44
  attr_reader :app, :logger
45
45
  end
46
46
  end
47
- end
47
+ end
@@ -27,4 +27,4 @@ module Clowk
27
27
  attr_reader :app, :logger, :open_timeout, :read_timeout, :write_timeout
28
28
  end
29
29
  end
30
- end
30
+ end
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'jwt'
3
+ require "jwt"
4
4
 
5
5
  module Clowk
6
6
  class JwtVerifier
7
- ALGORITHM = 'HS256'
7
+ ALGORITHM = "HS256"
8
8
 
9
9
  def initialize(secret_key: Clowk.config.secret_key, issuer: Clowk.config.issuer)
10
10
  @secret_key = secret_key
@@ -12,9 +12,9 @@ module Clowk
12
12
  end
13
13
 
14
14
  def verify(token)
15
- raise ConfigurationError, 'missing Clowk secret_key' if @secret_key.to_s.empty?
15
+ raise ConfigurationError, "missing Clowk secret_key" if @secret_key.to_s.empty?
16
16
 
17
- options = { algorithm: ALGORITHM }
17
+ options = {algorithm: ALGORITHM}
18
18
  options[:iss] = @issuer if @issuer
19
19
  options[:verify_iss] = @issuer.present?
20
20
 
@@ -18,7 +18,7 @@ module Clowk
18
18
  attr_reader :request, :token_param, :cookie_key
19
19
 
20
20
  def token_from_params
21
- params = request.respond_to?(:params) && request.params ? request.params : {}
21
+ params = (request.respond_to?(:params) && request.params) ? request.params : {}
22
22
  params[token_param.to_s].presence
23
23
  end
24
24
 
@@ -26,8 +26,8 @@ module Clowk
26
26
  header = request.authorization.to_s
27
27
  return if header.empty?
28
28
 
29
- scheme, token = header.split(' ', 2)
30
- return unless scheme.to_s.casecmp('Bearer').zero?
29
+ scheme, token = header.split(" ", 2)
30
+ return unless scheme.to_s.casecmp("Bearer").zero?
31
31
 
32
32
  token.presence
33
33
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/inflector'
3
+ require "active_support/inflector"
4
4
 
5
5
  module Clowk
6
6
  module SDK
7
7
  class Client
8
8
  def initialize(options = {})
9
- @api_base_url = options.fetch(:api_base_url, Clowk.config.api_base_url)
9
+ @api_base_url = options.fetch(:api_base_url, nil).presence || Clowk.config.api_base_url
10
10
  @secret_key = options.fetch(:secret_key, Clowk.config.secret_key)
11
11
  @publishable_key = options.fetch(:publishable_key, Clowk.config.publishable_key)
12
12
  end
@@ -18,7 +18,7 @@ module Clowk
18
18
 
19
19
  return super unless Clowk::SDK.const_defined?(resource_class_name)
20
20
 
21
- resource_ivar = "@#{method_name}"
21
+ resource_ivar = :"@#{resource_class_name}"
22
22
  return instance_variable_get(resource_ivar) if instance_variable_defined?(resource_ivar)
23
23
 
24
24
  resource_class = Clowk::SDK.const_get(resource_class_name)
@@ -80,8 +80,8 @@ module Clowk
80
80
 
81
81
  def default_headers
82
82
  {}.tap do |headers|
83
- headers['X-Clowk-Secret-Key'] = secret_key if secret_key.present?
84
- headers['X-Clowk-Publishable-Key'] = publishable_key if publishable_key.present?
83
+ headers["X-Clowk-Secret-Key"] = secret_key if secret_key.present?
84
+ headers["X-Clowk-Publishable-Key"] = publishable_key if publishable_key.present?
85
85
  end
86
86
  end
87
87
  end
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'erb'
3
+ require "erb"
4
4
 
5
5
  module Clowk
6
6
  module SDK
7
7
  class Resource
8
8
  def self.resource_path
9
- raise NotImplementedError, 'resource_path must be implemented'
9
+ raise NotImplementedError, "resource_path must be implemented"
10
10
  end
11
11
 
12
12
  def initialize(client)
@@ -31,10 +31,10 @@ module Clowk
31
31
  # @return [Clowk::Http::Response]
32
32
  def search(raw_query = nil, **filters)
33
33
  query = if raw_query.is_a?(String)
34
- raw_query
35
- else
36
- filters.map { |k, v| "#{k}:#{v}" }.join(' ')
37
- end
34
+ raw_query
35
+ else
36
+ filters.map { |k, v| "#{k}:#{v}" }.join(" ")
37
+ end
38
38
 
39
39
  client.get("#{self.class.resource_path}/search?query=#{ERB::Util.url_encode(query)}")
40
40
  end
@@ -4,7 +4,24 @@ module Clowk
4
4
  module SDK
5
5
  class Session < Resource
6
6
  def self.resource_path
7
- 'sessions'
7
+ "sessions"
8
+ end
9
+
10
+ # @param raw_query [String, nil] Raw query string (forwarded to the base search)
11
+ # @param email [String, nil] Email to search for via the legacy ILIKE endpoint
12
+ # @param filters [Hash] Keyword filters (forwarded to the base search)
13
+ # @return [Clowk::Http::Response]
14
+ def search(raw_query = nil, email: nil, **filters)
15
+ return super(raw_query, **filters) unless email
16
+
17
+ client.get("#{self.class.resource_path}/search?email=#{ERB::Util.url_encode(email)}")
18
+ end
19
+
20
+ # Revokes a session by its session_id (clk_session_UUID)
21
+ # @param session_id [String]
22
+ # @return [Clowk::Http::Response]
23
+ def revoke(session_id)
24
+ destroy(session_id)
8
25
  end
9
26
  end
10
27
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Clowk
4
+ module SDK
5
+ class SessionConfig < Resource
6
+ def self.resource_path
7
+ "session_config"
8
+ end
9
+
10
+ def fetch
11
+ response = client.get(self.class.resource_path)
12
+
13
+ response.body_parsed&.deep_symbolize_keys
14
+ end
15
+ end
16
+ end
17
+ end
@@ -4,7 +4,7 @@ module Clowk
4
4
  module SDK
5
5
  class Subdomain < Resource
6
6
  def self.resource_path
7
- 'instances'
7
+ "instances"
8
8
  end
9
9
 
10
10
  def find_by_pk(key = nil)
@@ -4,11 +4,23 @@ module Clowk
4
4
  module SDK
5
5
  class Token < Resource
6
6
  def self.resource_path
7
- 'tokens'
7
+ "tokens"
8
8
  end
9
9
 
10
10
  def verify(token:)
11
- client.post("#{self.class.resource_path}/verify", { token: token })
11
+ client.post("#{self.class.resource_path}/verify", {token: token})
12
+ end
13
+
14
+ def verify_with_session(token:)
15
+ response = verify(token:)
16
+
17
+ unless response.success?
18
+ raise Clowk::InvalidTokenError, response.body_parsed&.dig("error") || "token verification failed"
19
+ end
20
+
21
+ data = response.body_parsed&.dig("data") || response.body_parsed
22
+
23
+ data&.deep_symbolize_keys
12
24
  end
13
25
  end
14
26
  end
@@ -4,7 +4,7 @@ module Clowk
4
4
  module SDK
5
5
  class User < Resource
6
6
  def self.resource_path
7
- 'users'
7
+ "users"
8
8
  end
9
9
  end
10
10
  end
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'uri'
3
+ require "uri"
4
4
 
5
5
  module Clowk
6
6
  class Subdomain
7
7
  CACHE_TTL = 60
8
- DEFAULT_SUBDOMAIN_BASE = 'clowk.dev'
8
+ DEFAULT_SUBDOMAIN_BASE = "clowk.dev"
9
+
10
+ @cache_mutex = Mutex.new
9
11
 
10
12
  class << self
11
13
  def resolve_url!(...)
@@ -13,24 +15,28 @@ module Clowk
13
15
  end
14
16
 
15
17
  def clear_cache!
16
- @cache = {}
18
+ @cache_mutex.synchronize { @cache = {} }
17
19
  end
18
20
 
19
21
  def read_cache(key)
20
- entry = cache[key]
22
+ @cache_mutex.synchronize do
23
+ entry = cache[key]
21
24
 
22
- return unless entry
23
- return entry[:value] if entry[:expires_at] > Time.now
25
+ return unless entry
26
+ return entry[:value] if entry[:expires_at] > Time.now
24
27
 
25
- cache.delete(key)
26
- nil
28
+ cache.delete(key)
29
+ nil
30
+ end
27
31
  end
28
32
 
29
33
  def write_cache(key, value, ttl:)
30
- cache[key] = {
31
- value: value,
32
- expires_at: Time.now + ttl
33
- }
34
+ @cache_mutex.synchronize do
35
+ cache[key] = {
36
+ value: value,
37
+ expires_at: Time.now + ttl
38
+ }
39
+ end
34
40
  end
35
41
 
36
42
  private
@@ -42,7 +48,6 @@ module Clowk
42
48
 
43
49
  def initialize(options = {})
44
50
  @publishable_key = options.fetch(:publishable_key, Clowk.config.publishable_key)
45
- @api_base_url = options.fetch(:api_base_url, Clowk.config.api_base_url)
46
51
  @subdomain_url = options.fetch(:subdomain_url, Clowk.config.subdomain_url)
47
52
  end
48
53
 
@@ -50,12 +55,12 @@ module Clowk
50
55
  return resolve_from_key if publishable_key.present?
51
56
  return normalize_url(subdomain_url) if subdomain_url.present?
52
57
 
53
- raise ConfigurationError, 'set publishable_key or subdomain_url to build Clowk URLs'
58
+ raise ConfigurationError, "set publishable_key or subdomain_url to build Clowk URLs"
54
59
  end
55
60
 
56
61
  private
57
62
 
58
- attr_reader :api_base_url, :publishable_key, :subdomain_url
63
+ attr_reader :publishable_key, :subdomain_url
59
64
 
60
65
  def resolve_from_key
61
66
  cached = self.class.read_cache(cache_key)
@@ -64,7 +69,7 @@ module Clowk
64
69
  response = client.subdomains.find_by_pk(publishable_key)
65
70
  resolved = extract_url_from_instance(response.body_parsed)
66
71
 
67
- raise ConfigurationError, 'could not resolve subdomain_url from publishable_key' if resolved.blank?
72
+ raise ConfigurationError, "could not resolve subdomain_url from publishable_key" if resolved.blank?
68
73
 
69
74
  self.class.write_cache(cache_key, resolved, ttl: CACHE_TTL)
70
75
  resolved
@@ -77,22 +82,28 @@ module Clowk
77
82
  def extract_url_from_instance(payload)
78
83
  return if payload.blank?
79
84
 
80
- instance = payload.is_a?(Hash) ? payload : {}
81
- instance_data = instance['instance'].is_a?(Hash) ? instance['instance'] : instance
85
+ root = payload.is_a?(Hash) ? payload : {}
86
+ instance_data = if root["instance"].is_a?(Hash)
87
+ root["instance"]
88
+ elsif root["data"].is_a?(Hash)
89
+ root["data"]
90
+ else
91
+ root
92
+ end
82
93
 
83
- explicit_url = instance_data['url'] || instance_data['subdomain_url'] || instance_data['instance_url']
94
+ explicit_url = instance_data["url"] || instance_data["subdomain_url"] || instance_data["instance_url"]
84
95
  return normalize_url(explicit_url) if explicit_url.present?
85
96
 
86
- host = instance_data['host'] || instance_data['domain'] || instance_data['hostname']
97
+ host = instance_data["host"] || instance_data["domain"] || instance_data["hostname"]
87
98
  return normalize_url(host_to_url(host)) if host.present?
88
99
 
89
- subdomain = instance_data['subdomain']
100
+ subdomain = instance_data["subdomain"]
90
101
  normalize_url("https://#{subdomain}.#{default_subdomain_base}") if subdomain.present?
91
102
  end
92
103
 
93
104
  def host_to_url(host)
94
105
  value = host.to_s
95
- return value if value.start_with?('http://', 'https://')
106
+ return value if value.start_with?("http://", "https://")
96
107
 
97
108
  "https://#{value}"
98
109
  end
@@ -104,17 +115,17 @@ module Clowk
104
115
  uri = URI.parse(configured)
105
116
  return DEFAULT_SUBDOMAIN_BASE if uri.host.blank?
106
117
 
107
- uri.host.split('.').drop(1).join('.')
118
+ uri.host.split(".").drop(1).join(".")
108
119
  rescue URI::InvalidURIError
109
120
  DEFAULT_SUBDOMAIN_BASE
110
121
  end
111
122
 
112
123
  def normalize_url(value)
113
- value.to_s.sub(%r{/$}, '')
124
+ value.to_s.sub(%r{/$}, "")
114
125
  end
115
126
 
116
127
  def client
117
- @client ||= Clowk::SDK::Client.new
128
+ @client ||= Clowk::SDK::Client.new(api_base_url: Clowk.config.api_base_url)
118
129
  end
119
130
  end
120
131
  end
data/lib/clowk/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clowk
4
- VERSION = '0.1.0'
4
+ VERSION = "0.3.3"
5
5
  end
data/lib/clowk.rb CHANGED
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rails'
4
- require 'rails/engine'
5
- require 'action_controller/railtie'
6
- require 'active_support'
7
- require 'active_support/core_ext/hash'
8
- require 'active_support/core_ext/object/blank'
9
- require 'rack'
3
+ require "rails"
4
+ require "rails/engine"
5
+ require "action_controller/railtie"
6
+ require "active_support"
7
+ require "active_support/core_ext/hash"
8
+ require "active_support/core_ext/object/blank"
9
+ require "rack"
10
10
 
11
- require_relative 'clowk/version'
12
- require_relative 'clowk/configuration'
11
+ require_relative "clowk/version"
12
+ require_relative "clowk/configuration"
13
13
 
14
14
  module Clowk
15
15
  class Error < StandardError; end
@@ -24,6 +24,7 @@ module Clowk
24
24
 
25
25
  def configure
26
26
  yield(config)
27
+ config.validate!
27
28
  end
28
29
 
29
30
  def reset!
@@ -33,24 +34,25 @@ module Clowk
33
34
  end
34
35
  end
35
36
 
36
- require_relative 'clowk/current'
37
- require_relative 'clowk/http/response'
38
- require_relative 'clowk/http/logger_middleware'
39
- require_relative 'clowk/http/retry_middleware'
40
- require_relative 'clowk/http/timeout_middleware'
41
- require_relative 'clowk/http/client'
42
- require_relative 'clowk/sdk/resource'
43
- require_relative 'clowk/sdk/user'
44
- require_relative 'clowk/sdk/session'
45
- require_relative 'clowk/sdk/subdomain'
46
- require_relative 'clowk/sdk/token'
47
- require_relative 'clowk/sdk/client'
48
- require_relative 'clowk/subdomain'
49
- require_relative 'clowk/jwt_verifier'
50
- require_relative 'clowk/helpers/url_helpers'
51
- require_relative 'clowk/middleware/token_extractor'
52
- require_relative 'clowk/authenticable'
53
- require_relative 'clowk/controllers/base_controller'
54
- require_relative 'clowk/controllers/callbacks_controller'
55
- require_relative 'clowk/controllers/sessions_controller'
56
- require_relative 'clowk/engine'
37
+ require_relative "clowk/current"
38
+ require_relative "clowk/http/response"
39
+ require_relative "clowk/http/logger_middleware"
40
+ require_relative "clowk/http/retry_middleware"
41
+ require_relative "clowk/http/timeout_middleware"
42
+ require_relative "clowk/http/client"
43
+ require_relative "clowk/sdk/resource"
44
+ require_relative "clowk/sdk/user"
45
+ require_relative "clowk/sdk/session"
46
+ require_relative "clowk/sdk/subdomain"
47
+ require_relative "clowk/sdk/token"
48
+ require_relative "clowk/sdk/session_config"
49
+ require_relative "clowk/sdk/client"
50
+ require_relative "clowk/subdomain"
51
+ require_relative "clowk/jwt_verifier"
52
+ require_relative "clowk/helpers/url_helpers"
53
+ require_relative "clowk/middleware/token_extractor"
54
+ require_relative "clowk/authenticable"
55
+ require_relative "clowk/controllers/base_controller"
56
+ require_relative "clowk/controllers/callbacks_controller"
57
+ require_relative "clowk/controllers/sessions_controller"
58
+ require_relative "clowk/engine"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clowk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clowk
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-03-23 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activesupport
@@ -77,6 +77,20 @@ dependencies:
77
77
  - - "<"
78
78
  - !ruby/object:Gem::Version
79
79
  version: '4.0'
80
+ - !ruby/object:Gem::Dependency
81
+ name: standard
82
+ requirement: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '1.0'
87
+ type: :development
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '1.0'
80
94
  description: Clowk Authentication, JWT verification, and future API access
81
95
  email:
82
96
  - support@clowk.in
@@ -106,6 +120,7 @@ files:
106
120
  - lib/clowk/sdk/client.rb
107
121
  - lib/clowk/sdk/resource.rb
108
122
  - lib/clowk/sdk/session.rb
123
+ - lib/clowk/sdk/session_config.rb
109
124
  - lib/clowk/sdk/subdomain.rb
110
125
  - lib/clowk/sdk/token.rb
111
126
  - lib/clowk/sdk/user.rb
@@ -113,7 +128,7 @@ files:
113
128
  - lib/clowk/version.rb
114
129
  homepage: https://clowk.in
115
130
  licenses:
116
- - AGPL-3.0
131
+ - AGPL-3.0-only
117
132
  metadata:
118
133
  rubygems_mfa_required: 'true'
119
134
  rdoc_options: []
@@ -130,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
145
  - !ruby/object:Gem::Version
131
146
  version: '0'
132
147
  requirements: []
133
- rubygems_version: 3.6.2
148
+ rubygems_version: 3.6.9
134
149
  specification_version: 4
135
150
  summary: Rails SDK for Clowk authentication
136
151
  test_files: []