clowk 0.3.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: f3298d33abdf85d08387832031c8bbdb6efa36785b73c59ece84a3dec4deb064
4
- data.tar.gz: b533eb6930465ea11188d0f2f8e23758575f113972d9aea81d5bdcb87a21af32
3
+ metadata.gz: 7b02c176f355917aa070a2b183a5844911e769b49ff7906cc40c13bbcd68cbf6
4
+ data.tar.gz: 7ba591ce54755e2ff3d6a586ef7942ae96442efa6ae93caff5c64e0ac313cbd1
5
5
  SHA512:
6
- metadata.gz: 486ff44e3a9e781cad6c87584d40079ca43724c113432a76513078525b8df9a8d64a82c593a4be9bb7db87dc9c3bce7a6ec611c14fbefe2d7074904d8901b758
7
- data.tar.gz: e06afce3a59cbb24d8c388837b51582c30d2597dec2e9e08337343040ca9cbe1041b2442c573436faa2c958adf3bd6b15ac3170d82447189ae235386a16a5fd6
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
@@ -15,20 +15,28 @@ module Clowk
15
15
  enforce_session_method = :"#{scope}_enforce_session!"
16
16
 
17
17
  base.class_eval do
18
- define_method(current_method) do
19
- clowk_current_resource
18
+ unless current_method == :clowk_current_resource
19
+ define_method(current_method) do
20
+ clowk_current_resource
21
+ end
20
22
  end
21
23
 
22
- define_method(authenticate_method) do
23
- clowk_authenticate!
24
+ unless authenticate_method == :clowk_authenticate!
25
+ define_method(authenticate_method) do
26
+ clowk_authenticate!
27
+ end
24
28
  end
25
29
 
26
- define_method(signed_in_method) do
27
- clowk_current_resource.present?
30
+ unless signed_in_method == :clowk_signed_in?
31
+ define_method(signed_in_method) do
32
+ clowk_signed_in?
33
+ end
28
34
  end
29
35
 
30
- define_method(enforce_session_method) do
31
- clowk_enforce_session!
36
+ unless enforce_session_method == :clowk_enforce_session!
37
+ define_method(enforce_session_method) do
38
+ clowk_enforce_session!
39
+ end
32
40
  end
33
41
 
34
42
  helper_method current_method, authenticate_method, signed_in_method, :current_token if respond_to?(:helper_method)
@@ -47,7 +55,7 @@ module Clowk
47
55
  end
48
56
 
49
57
  def current_token
50
- stored_session&.dig('token') || extracted_token
58
+ stored_session&.dig("token") || extracted_token
51
59
  end
52
60
 
53
61
  def clowk_signed_in?
@@ -59,7 +67,7 @@ module Clowk
59
67
  end
60
68
 
61
69
  def clowk_session_active?
62
- clowk_session_status&.dig(:status) == 'active'
70
+ clowk_session_status&.dig(:status) == "active"
63
71
  end
64
72
 
65
73
  def clowk_enforce_session!
@@ -74,21 +82,13 @@ module Clowk
74
82
  return
75
83
  end
76
84
 
77
- if request.format.json?
78
- render json: { error: 'Session expired or inactive' }, status: :unauthorized
79
- else
80
- redirect_to clowk_sign_in_path(return_to: request.fullpath)
81
- end
85
+ clowk_handle_expired_session(session_info)
82
86
  end
83
87
 
84
88
  def clowk_authenticate!
85
89
  return clowk_current_resource if clowk_signed_in?
86
90
 
87
- if request.format.json?
88
- render json: { error: 'Unauthorized' }, status: :unauthorized
89
- else
90
- redirect_to clowk_sign_in_path(return_to: request.fullpath)
91
- end
91
+ clowk_handle_unauthenticated
92
92
  end
93
93
 
94
94
  def clowk_sign_out!
@@ -100,6 +100,22 @@ module Clowk
100
100
 
101
101
  private
102
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
+
103
119
  def verified_request_payload
104
120
  return unless extracted_token
105
121
 
@@ -123,7 +139,7 @@ module Clowk
123
139
  end
124
140
 
125
141
  def stored_user_payload
126
- payload = stored_session&.dig('user') || stored_session&.dig(:user)
142
+ payload = stored_session&.dig("user") || stored_session&.dig(:user)
127
143
  payload&.deep_symbolize_keys
128
144
  end
129
145
 
@@ -143,7 +159,7 @@ module Clowk
143
159
  end
144
160
 
145
161
  def resolve_session_status
146
- cached = stored_session&.dig('session_status') || stored_session&.dig(:session_status)
162
+ cached = stored_session&.dig("session_status") || stored_session&.dig(:session_status)
147
163
 
148
164
  return cached&.deep_symbolize_keys if cached
149
165
 
@@ -156,9 +172,11 @@ module Clowk
156
172
  result = client.tokens.verify_with_session(token: current_token)
157
173
  status = result&.dig(:session)
158
174
 
159
- session[Clowk.config.session_key] = stored_session.merge('session_status' => status) if status && stored_session
175
+ session[Clowk.config.session_key] = stored_session.merge("session_status" => status) if status && stored_session
160
176
 
161
177
  status
178
+ rescue Clowk::InvalidTokenError
179
+ nil
162
180
  end
163
181
  end
164
182
  end
@@ -2,9 +2,8 @@
2
2
 
3
3
  module Clowk
4
4
  class Configuration
5
+ attr_accessor :api_base_url
5
6
  attr_accessor :app_base_url
6
- attr_accessor :after_sign_in_path
7
- attr_accessor :after_sign_out_path
8
7
  attr_accessor :callback_path
9
8
  attr_accessor :cookie_key
10
9
  attr_accessor :http_logger
@@ -25,24 +24,56 @@ module Clowk
25
24
  attr_accessor :on_session_expired
26
25
 
27
26
  def initialize
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
44
  @enforce_active_session = false
45
45
  @on_session_expired = nil
46
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
77
+ end
47
78
  end
48
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
@@ -47,5 +47,19 @@ module Clowk
47
47
  def to_h
48
48
  attributes.merge(id: id)
49
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
50
64
  end
51
- 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, nil).presence || derive_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)
@@ -65,13 +65,6 @@ module Clowk
65
65
 
66
66
  attr_reader :api_base_url, :publishable_key, :secret_key
67
67
 
68
- def derive_api_base_url
69
- base = Clowk.config.subdomain_url.to_s.strip
70
- return if base.empty?
71
-
72
- "#{base.sub(%r{/$}, '')}/api/v1"
73
- end
74
-
75
68
  def http
76
69
  @http ||= Clowk::Http.new(
77
70
  base_url: api_base_url,
@@ -87,8 +80,8 @@ module Clowk
87
80
 
88
81
  def default_headers
89
82
  {}.tap do |headers|
90
- headers['X-Clowk-Secret-Key'] = secret_key if secret_key.present?
91
- 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?
92
85
  end
93
86
  end
94
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,12 +4,16 @@ module Clowk
4
4
  module SDK
5
5
  class Session < Resource
6
6
  def self.resource_path
7
- 'sessions'
7
+ "sessions"
8
8
  end
9
9
 
10
- # @param email [String] Email to search for (ILIKE match)
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)
11
13
  # @return [Clowk::Http::Response]
12
- def search(email:)
14
+ def search(raw_query = nil, email: nil, **filters)
15
+ return super(raw_query, **filters) unless email
16
+
13
17
  client.get("#{self.class.resource_path}/search?email=#{ERB::Util.url_encode(email)}")
14
18
  end
15
19
 
@@ -4,7 +4,7 @@ module Clowk
4
4
  module SDK
5
5
  class SessionConfig < Resource
6
6
  def self.resource_path
7
- 'session_config'
7
+ "session_config"
8
8
  end
9
9
 
10
10
  def fetch
@@ -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,16 +4,21 @@ 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
12
  end
13
13
 
14
14
  def verify_with_session(token:)
15
15
  response = verify(token:)
16
- data = response.body_parsed&.dig('data') || response.body_parsed
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
17
22
 
18
23
  data&.deep_symbolize_keys
19
24
  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,12 +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
- API_BASE_URL = 'https://api.clowk.dev/api/v1'
8
7
  CACHE_TTL = 60
9
- DEFAULT_SUBDOMAIN_BASE = 'clowk.dev'
8
+ DEFAULT_SUBDOMAIN_BASE = "clowk.dev"
9
+
10
+ @cache_mutex = Mutex.new
10
11
 
11
12
  class << self
12
13
  def resolve_url!(...)
@@ -14,24 +15,28 @@ module Clowk
14
15
  end
15
16
 
16
17
  def clear_cache!
17
- @cache = {}
18
+ @cache_mutex.synchronize { @cache = {} }
18
19
  end
19
20
 
20
21
  def read_cache(key)
21
- entry = cache[key]
22
+ @cache_mutex.synchronize do
23
+ entry = cache[key]
22
24
 
23
- return unless entry
24
- return entry[:value] if entry[:expires_at] > Time.now
25
+ return unless entry
26
+ return entry[:value] if entry[:expires_at] > Time.now
25
27
 
26
- cache.delete(key)
27
- nil
28
+ cache.delete(key)
29
+ nil
30
+ end
28
31
  end
29
32
 
30
33
  def write_cache(key, value, ttl:)
31
- cache[key] = {
32
- value: value,
33
- expires_at: Time.now + ttl
34
- }
34
+ @cache_mutex.synchronize do
35
+ cache[key] = {
36
+ value: value,
37
+ expires_at: Time.now + ttl
38
+ }
39
+ end
35
40
  end
36
41
 
37
42
  private
@@ -50,7 +55,7 @@ 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
@@ -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
@@ -78,27 +83,27 @@ module Clowk
78
83
  return if payload.blank?
79
84
 
80
85
  root = payload.is_a?(Hash) ? payload : {}
81
- instance_data = if root['instance'].is_a?(Hash)
82
- root['instance']
83
- elsif root['data'].is_a?(Hash)
84
- root['data']
85
- else
86
- root
87
- end
88
-
89
- explicit_url = instance_data['url'] || instance_data['subdomain_url'] || instance_data['instance_url']
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
93
+
94
+ explicit_url = instance_data["url"] || instance_data["subdomain_url"] || instance_data["instance_url"]
90
95
  return normalize_url(explicit_url) if explicit_url.present?
91
96
 
92
- host = instance_data['host'] || instance_data['domain'] || instance_data['hostname']
97
+ host = instance_data["host"] || instance_data["domain"] || instance_data["hostname"]
93
98
  return normalize_url(host_to_url(host)) if host.present?
94
99
 
95
- subdomain = instance_data['subdomain']
100
+ subdomain = instance_data["subdomain"]
96
101
  normalize_url("https://#{subdomain}.#{default_subdomain_base}") if subdomain.present?
97
102
  end
98
103
 
99
104
  def host_to_url(host)
100
105
  value = host.to_s
101
- return value if value.start_with?('http://', 'https://')
106
+ return value if value.start_with?("http://", "https://")
102
107
 
103
108
  "https://#{value}"
104
109
  end
@@ -110,17 +115,17 @@ module Clowk
110
115
  uri = URI.parse(configured)
111
116
  return DEFAULT_SUBDOMAIN_BASE if uri.host.blank?
112
117
 
113
- uri.host.split('.').drop(1).join('.')
118
+ uri.host.split(".").drop(1).join(".")
114
119
  rescue URI::InvalidURIError
115
120
  DEFAULT_SUBDOMAIN_BASE
116
121
  end
117
122
 
118
123
  def normalize_url(value)
119
- value.to_s.sub(%r{/$}, '')
124
+ value.to_s.sub(%r{/$}, "")
120
125
  end
121
126
 
122
127
  def client
123
- @client ||= Clowk::SDK::Client.new(api_base_url: API_BASE_URL)
128
+ @client ||= Clowk::SDK::Client.new(api_base_url: Clowk.config.api_base_url)
124
129
  end
125
130
  end
126
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.3.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,25 +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/session_config'
48
- require_relative 'clowk/sdk/client'
49
- require_relative 'clowk/subdomain'
50
- require_relative 'clowk/jwt_verifier'
51
- require_relative 'clowk/helpers/url_helpers'
52
- require_relative 'clowk/middleware/token_extractor'
53
- require_relative 'clowk/authenticable'
54
- require_relative 'clowk/controllers/base_controller'
55
- require_relative 'clowk/controllers/callbacks_controller'
56
- require_relative 'clowk/controllers/sessions_controller'
57
- 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.3.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-27 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
@@ -114,7 +128,7 @@ files:
114
128
  - lib/clowk/version.rb
115
129
  homepage: https://clowk.in
116
130
  licenses:
117
- - AGPL-3.0
131
+ - AGPL-3.0-only
118
132
  metadata:
119
133
  rubygems_mfa_required: 'true'
120
134
  rdoc_options: []
@@ -131,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
145
  - !ruby/object:Gem::Version
132
146
  version: '0'
133
147
  requirements: []
134
- rubygems_version: 3.6.2
148
+ rubygems_version: 3.6.9
135
149
  specification_version: 4
136
150
  summary: Rails SDK for Clowk authentication
137
151
  test_files: []