liberty 0.3.0 → 0.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cd2f5e057e9a8a59d5fd94c67b47972793d0662a010e06a824271a9717dc1af
4
- data.tar.gz: 2a03907e15c4100c6dbff7d6d4af219c00d72e94d91ab4c1bdf31cb32b6f0ca8
3
+ metadata.gz: 9450a8fadf0c6726d4249f2b6a62f4918045795f199be8ec8bec29412bbaebc1
4
+ data.tar.gz: 55ea606054dabaeec94add7f6b5fe63a5d38add6eca13dc9f4fc1ef2754ad5c2
5
5
  SHA512:
6
- metadata.gz: 85adf8822f586339d802a8389d48babd728fb522d3df1c2c6bd3383b36ab5f4d2bf395cd6f6ffb01b20a6ddffa083e6757bad0f4203fcd51f40c3f77a37124a3
7
- data.tar.gz: 4fb75c34eb3382e668b63b4ea12dfc5d35de40fc7d1734fa2d89023c0ec3ad9c9ea6fb38153aa952d016cdcad05153bd4c3dfa2df276e7fd6da352afc2fe8f78
6
+ metadata.gz: 17e9cd7824c5b5038daee7fd5d3d441886865dc78562797cd502b8f96f6fd9cd48b2ed92da934265321757a2237e3682092e8653b1b4370260d73b3d9caef253
7
+ data.tar.gz: 7416e1f47f06cb05dd7fbaadc777f71bbac9bc828a5954f4db401c52e42abc88777ee3fd49ad1e24e22e412953e7d2b188b2a6983b153ea36f535f02a5f7d48c
data/.qlty/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *
2
+ !configs
3
+ !configs/**
4
+ !hooks
5
+ !hooks/**
6
+ !qlty.toml
7
+ !.gitignore
data/.qlty/qlty.toml ADDED
@@ -0,0 +1,81 @@
1
+ # This file was automatically generated by `qlty init`.
2
+ # You can modify it to suit your needs.
3
+ # We recommend you to commit this file to your repository.
4
+ #
5
+ # This configuration is used by both Qlty CLI and Qlty Cloud.
6
+ #
7
+ # Qlty CLI -- Code quality toolkit for developers
8
+ # Qlty Cloud -- Fully automated Code Health Platform
9
+ #
10
+ # Try Qlty Cloud: https://qlty.sh
11
+ #
12
+ # For a guide to configuration, visit https://qlty.sh/d/config
13
+ # Or for a full reference, visit https://qlty.sh/d/qlty-toml
14
+ config_version = "0"
15
+
16
+ exclude_patterns = [
17
+ "*_min.*",
18
+ "*-min.*",
19
+ "*.min.*",
20
+ "**/.yarn/**",
21
+ "**/*.d.ts",
22
+ "**/assets/**",
23
+ "**/bower_components/**",
24
+ "**/build/**",
25
+ "**/cache/**",
26
+ "**/config/**",
27
+ "**/db/**",
28
+ "**/deps/**",
29
+ "**/dist/**",
30
+ "**/extern/**",
31
+ "**/external/**",
32
+ "**/generated/**",
33
+ "**/Godeps/**",
34
+ "**/gradlew/**",
35
+ "**/mvnw/**",
36
+ "**/node_modules/**",
37
+ "**/protos/**",
38
+ "**/seed/**",
39
+ "**/target/**",
40
+ "**/templates/**",
41
+ "**/testdata/**",
42
+ "**/vendor/**",
43
+ ]
44
+
45
+ test_patterns = [
46
+ "**/test/**",
47
+ "**/spec/**",
48
+ "**/*.test.*",
49
+ "**/*.spec.*",
50
+ "**/*_test.*",
51
+ "**/*_spec.*",
52
+ "**/test_*.*",
53
+ "**/spec_*.*",
54
+ ]
55
+
56
+ [smells]
57
+ mode = "comment"
58
+
59
+ [[source]]
60
+ name = "default"
61
+ default = true
62
+
63
+
64
+ [[plugin]]
65
+ name = "actionlint"
66
+
67
+ [[plugin]]
68
+ name = "osv-scanner"
69
+
70
+ [[plugin]]
71
+ name = "ripgrep"
72
+ mode = "comment"
73
+
74
+ [[plugin]]
75
+ name = "standardrb"
76
+
77
+ [[plugin]]
78
+ name = "trufflehog"
79
+
80
+ [[plugin]]
81
+ name = "zizmor"
data/CHANGELOG.md CHANGED
@@ -1,3 +1,28 @@
1
+ ## [0.4.0] - 2026-09-15
2
+
3
+ ### Breaking changes
4
+
5
+ - Endpoints now offer `authenticated?` and `authorized?` hooks for plugging in your own auth handlers. Both default to `false` to prevent endpoints from being exposed accidentally, so every existing endpoint responds with a 401 until it overrides them. Applications upgrading from older versions must override these methods, either to return `true` or with code that handles auth.
6
+
7
+ ### Added
8
+
9
+ - Responds with `401 Authentication required` when `authenticated?` returns `false`, and with `403 Forbidden` when `authorized?` returns `false`. The endpoint's status, headers, and content are only consulted when both return `true`.
10
+ - Adds a `www_authenticate_header` hook to endpoints. When it returns a challenge, such as `Bearer realm="api"`, 401 responses include it as the `WWW-Authenticate` header. It defaults to `nil`, which omits the header.
11
+ - Exposes the request's `Authorization` header as `request.headers[:authorization]` so endpoints can read credentials.
12
+ - Responds to `HEAD` requests that fail auth with an empty body and a `content-length` header, as the Rack specification requires.
13
+
14
+ ## [0.3.1] - 2026-09-07
15
+
16
+ ### Fixed
17
+
18
+ - Responds to `HEAD` requests with an empty body, as the Rack specification requires. Status and headers, including `content-length`, still describe what the matching `GET` would return.
19
+ - Responds to `HEAD` requests for unknown routes with a 404 and an empty body.
20
+ - Builds the 404 response per request instead of reusing a frozen constant, which Rack::Lint rejects.
21
+
22
+ ### Changed
23
+
24
+ - Adds `qlty` and `flog` tasks to the default Rake task, alongside `spec` and `standard`.
25
+
1
26
  ## [0.3.0] - 2026-09-06
2
27
 
3
28
  ### Breaking changes
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- liberty (0.3.0)
4
+ liberty (0.4.0)
5
5
  json (~> 2.3)
6
6
  mustermann (~> 4.0)
7
7
  mustermann-contrib (~> 4.0)
data/README.md CHANGED
@@ -50,6 +50,14 @@ Here's an example for an HTTP get to the `/hello` route, which returns a hello w
50
50
  class MyEndpoint < Liberty::Endpoint
51
51
  responds_to :get, '/hello'
52
52
 
53
+ def authenticated?
54
+ true
55
+ end
56
+
57
+ def authorized?
58
+ true
59
+ end
60
+
53
61
  def status
54
62
  200
55
63
  end
@@ -83,6 +91,49 @@ end
83
91
  If you configure your CORS headers before you launch your application, `Endpoints` will
84
92
  automatically respond with the right headers.
85
93
 
94
+ ### Authentication & Authorization
95
+
96
+ Every `Endpoint` has two hooks: `#authenticated?` and `#authorized?`. Both default to `false`,
97
+ so an endpoint that does not implement them responds with `401 Authentication required`. Override
98
+ them to plug in your own auth. Liberty checks `#authenticated?` first and responds with a 401 when
99
+ it returns `false`. It then checks `#authorized?` and responds with `403 Forbidden` when that
100
+ returns `false`. Your endpoint's status, headers, and content are only consulted when both return
101
+ `true`. The raw `Authorization` header is available as `request.headers[:authorization]`.
102
+
103
+ A 401 response should tell the client how to authenticate. Override `#www_authenticate_header`
104
+ to return the challenge, such as `Bearer realm="api"`, and Liberty adds it as the
105
+ `WWW-Authenticate` header on 401 responses. When it returns `nil` the header is omitted.
106
+
107
+ Here's an example of how to use `#authenticated?` and `#authorized?`:
108
+
109
+ ```ruby
110
+ class YourEndpoint < Liberty::Endpoint
111
+ responds_to :get, "/your_endpoint"
112
+
113
+ def authenticated?
114
+ !current_user.nil?
115
+ end
116
+
117
+ def www_authenticate_header
118
+ 'Bearer realm="api"'
119
+ end
120
+
121
+ def authorized?
122
+ current_user.admin?
123
+ end
124
+
125
+ def current_user
126
+ @current_user ||= Sessions.find_by_token(bearer_token)
127
+ end
128
+
129
+ private
130
+
131
+ def bearer_token
132
+ request.headers[:authorization].to_s.delete_prefix("Bearer ")
133
+ end
134
+ end
135
+ ```
136
+
86
137
  ## Installation
87
138
 
88
139
  Install the gem and add to the application's Gemfile by executing:
data/Rakefile CHANGED
@@ -7,4 +7,25 @@ RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  require "standard/rake"
9
9
 
10
- task default: %i[spec standard]
10
+ FLOG_LIMIT = 10.0
11
+
12
+ def qlty_bin
13
+ installed = File.expand_path("~/.qlty/bin/qlty")
14
+ ENV.fetch("QLTY_BIN") { File.exist?(installed) ? installed : "qlty" }
15
+ end
16
+
17
+ desc "Run qlty lint checks and report code smells"
18
+ task :qlty do
19
+ sh qlty_bin, "check", "--all", "--no-progress"
20
+ sh qlty_bin, "smells", "--all", "--quiet"
21
+ end
22
+
23
+ desc "Fail when the average flog score per method reaches #{FLOG_LIMIT}"
24
+ task :flog do
25
+ report = `bundle exec flog -m lib`
26
+ puts report
27
+ average = report[/^\s*([\d.]+): flog\/method average/, 1].to_f
28
+ abort("Quality gate failed: average flog #{average} is #{FLOG_LIMIT} or higher") if average >= FLOG_LIMIT
29
+ end
30
+
31
+ task default: %i[spec standard qlty flog]
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liberty
4
+ module Adapters
5
+ class ErrorResponse
6
+ attr_reader :endpoint
7
+
8
+ def initialize(endpoint)
9
+ @endpoint = endpoint
10
+ end
11
+
12
+ def to_rack_response
13
+ [status, headers, rack_body]
14
+ end
15
+
16
+ private
17
+
18
+ def headers
19
+ {
20
+ Response::CONTENT_LENGTH => body.bytesize.to_s,
21
+ Response::CONTENT_TYPE => Response::MIME_TYPE_TEXT
22
+ }
23
+ end
24
+
25
+ def rack_body
26
+ endpoint.request.head? ? [] : [body]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error_response"
4
+
5
+ module Liberty
6
+ module Adapters
7
+ class ForbiddenResponse < ErrorResponse
8
+ private
9
+
10
+ def status
11
+ 403
12
+ end
13
+
14
+ def body
15
+ "Forbidden"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -7,6 +7,7 @@ module Liberty
7
7
  class Request
8
8
  PARSED_BODY = "parsed_body"
9
9
  ROUTER_PARAMS = "router.params"
10
+ AUTHORIZATION = "HTTP_AUTHORIZATION"
10
11
 
11
12
  attr_reader :env
12
13
 
@@ -15,18 +16,27 @@ module Liberty
15
16
  end
16
17
 
17
18
  def headers
18
- @headers ||= env ? {
19
- accept_media_types: accept_media_types,
20
- preferred_media_type: preferred_media_type
21
- } : {}
19
+ @headers ||= env ? env_headers : {}
22
20
  end
23
21
 
24
22
  def params
25
23
  @params ||= env ? all_params : {}
26
24
  end
27
25
 
26
+ def head?
27
+ env ? rack_request.head? : false
28
+ end
29
+
28
30
  private
29
31
 
32
+ def env_headers
33
+ {
34
+ accept_media_types: accept_media_types,
35
+ preferred_media_type: preferred_media_type,
36
+ authorization: env[AUTHORIZATION]
37
+ }
38
+ end
39
+
30
40
  def all_params
31
41
  form_params.merge!(body_params).merge!(url_params)
32
42
  end
@@ -69,7 +79,9 @@ module Liberty
69
79
  end
70
80
 
71
81
  def symbolize_keys(hash)
72
- hash.each_with_object({}) { |(key, value), obj| obj[key.to_sym] = value }
82
+ hash.each_with_object({}) do |(key, value), obj|
83
+ obj[key.to_sym] = value
84
+ end
73
85
  end
74
86
 
75
87
  def rack_request
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "forbidden_response"
4
+ require_relative "unauthenticated_response"
5
+
3
6
  module Liberty
4
7
  module Adapters
5
8
  class Response
@@ -17,15 +20,34 @@ module Liberty
17
20
  end
18
21
 
19
22
  def to_rack_response
20
- [status, headers, [content]]
23
+ return unauthenticated_response unless endpoint.authenticated?
24
+ return forbidden_response unless endpoint.authorized?
25
+
26
+ [status, headers, rack_body]
21
27
  end
22
28
 
23
29
  private
24
30
 
31
+ def unauthenticated_response
32
+ UnauthenticatedResponse.new(endpoint).to_rack_response
33
+ end
34
+
35
+ def forbidden_response
36
+ ForbiddenResponse.new(endpoint).to_rack_response
37
+ end
38
+
25
39
  def status
26
40
  endpoint.status
27
41
  end
28
42
 
43
+ def rack_body
44
+ head? ? [] : [content]
45
+ end
46
+
47
+ def head?
48
+ endpoint.request.head?
49
+ end
50
+
29
51
  def headers
30
52
  headers = {CONTENT_LENGTH => content_length}
31
53
  headers[CONTENT_TYPE] = content_type if content_type
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "error_response"
4
+
5
+ module Liberty
6
+ module Adapters
7
+ class UnauthenticatedResponse < ErrorResponse
8
+ WWW_AUTHENTICATE = "www-authenticate"
9
+
10
+ private
11
+
12
+ def status
13
+ 401
14
+ end
15
+
16
+ def headers
17
+ challenge ? super.merge(WWW_AUTHENTICATE => challenge) : super
18
+ end
19
+
20
+ def challenge
21
+ endpoint.www_authenticate_header
22
+ end
23
+
24
+ def body
25
+ "Authentication required"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -22,6 +22,17 @@ module Liberty
22
22
  request.headers[:preferred_media_type]
23
23
  end
24
24
 
25
+ def authenticated?
26
+ false
27
+ end
28
+
29
+ def www_authenticate_header
30
+ end
31
+
32
+ def authorized?
33
+ false
34
+ end
35
+
25
36
  def status
26
37
  DEFAULT_STATUS
27
38
  end
@@ -15,7 +15,9 @@ module Liberty
15
15
  PATH_INFO = "PATH_INFO"
16
16
  ROUTER_PARAMS = "router.params"
17
17
  DYNAMIC_PREFIX = /:/
18
- NOT_FOUND_RESPONSE = [404, {"content-length" => "9"}, ["Not Found"]].freeze
18
+ NOT_FOUND_STATUS = 404
19
+ NOT_FOUND_BODY = "Not Found"
20
+ CONTENT_LENGTH = "content-length"
19
21
 
20
22
  def initialize
21
23
  @apps = {}
@@ -46,7 +48,7 @@ module Liberty
46
48
 
47
49
  def call(env)
48
50
  endpoint = find_endpoint(env[REQUEST_METHOD], env[PATH_INFO]) { |params| env[ROUTER_PARAMS] = params }
49
- endpoint ? endpoint.call(env) : NOT_FOUND_RESPONSE
51
+ endpoint ? endpoint.call(env) : not_found_response(env)
50
52
  end
51
53
 
52
54
  def print(stdout = $stdout)
@@ -55,6 +57,18 @@ module Liberty
55
57
 
56
58
  private
57
59
 
60
+ def not_found_response(env)
61
+ [NOT_FOUND_STATUS, not_found_headers, not_found_body(env)]
62
+ end
63
+
64
+ def not_found_headers
65
+ {CONTENT_LENGTH => NOT_FOUND_BODY.bytesize.to_s}
66
+ end
67
+
68
+ def not_found_body(env)
69
+ (env[REQUEST_METHOD] == HEAD) ? [] : [NOT_FOUND_BODY]
70
+ end
71
+
58
72
  def register_route(verb, path, to)
59
73
  dynamic?(path) ? register_dynamic_route(verb, path, to) : register_static_route(verb, path, to)
60
74
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Liberty
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liberty
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alan Ridlehoover
@@ -102,6 +102,8 @@ executables: []
102
102
  extensions: []
103
103
  extra_rdoc_files: []
104
104
  files:
105
+ - ".qlty/.gitignore"
106
+ - ".qlty/qlty.toml"
105
107
  - ".rspec"
106
108
  - ".ruby-version"
107
109
  - ".standard.yml"
@@ -114,11 +116,14 @@ files:
114
116
  - Rakefile
115
117
  - assets/liberty.png
116
118
  - lib/liberty.rb
119
+ - lib/liberty/adapters/error_response.rb
120
+ - lib/liberty/adapters/forbidden_response.rb
117
121
  - lib/liberty/adapters/parsers/factory.rb
118
122
  - lib/liberty/adapters/parsers/json.rb
119
123
  - lib/liberty/adapters/parsers/null.rb
120
124
  - lib/liberty/adapters/request.rb
121
125
  - lib/liberty/adapters/response.rb
126
+ - lib/liberty/adapters/unauthenticated_response.rb
122
127
  - lib/liberty/application.rb
123
128
  - lib/liberty/cors.rb
124
129
  - lib/liberty/cors/middleware.rb