glib-web 6.9.3 → 6.10.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: be34b6a047f58554b81fa3f72658076bcdefc2e06ec5b9d34f64a48208307ad2
4
- data.tar.gz: b9daa421b06c9d995534d88ff8a7cc11024ca59291f67ceaaae4d96c7d2e7bed
3
+ metadata.gz: ebde832f6f50b0556f57e6b2d716d7315fa01c0def0b75953f8ecf5de60ca547
4
+ data.tar.gz: 4d217bd99f1a415bca302a08528a1da652788fc5cca031009e132079d2716381
5
5
  SHA512:
6
- metadata.gz: 64e8c8220a4e59a60f6909f389dc7a1c2c0ed3e9086eb3ed4c6e8df979cdf2c87e8620dc79b75bb48eab574733a31f2787ad7bb4d281f4890e7d3feb592aab08
7
- data.tar.gz: 8331b3ed70b4c3dd56dab59a9fcfaf9e59ef4fde9a1daf7c61a728598585685c7a0700ef4ebe9200bd9080dcd039d61d99358a4e623b3e739e47649dc83d704d
6
+ metadata.gz: bb08b62b519b2a2cfc7c9b1767fce93c4373b63b43082715211d44d051a58aafa5e94d4d5509fd907e86a2b0f6a4ce9c35ec7842ce29d2d21993cadcdf044796
7
+ data.tar.gz: 715c3de7e58cfa4a443cc73b75b5c3d6a47bc5e9eff3eeb5cd812d119e0dff76e5c14832163abeb9ce891ea5c12b4922918b6133aa90a2848ff31fa49f40e664
@@ -22,7 +22,11 @@ module Glib::Auth
22
22
  # - Need to find a solution where we can reuse a single public policy
23
23
  # after_action :verify_authorized
24
24
 
25
- helper_method :policy, :can?, :cannot?
25
+ # Guarded because not every includer has `helper_method`: ActionController::Base
26
+ # does, but bare ActionController::API does not (gems like jbuilder may include
27
+ # ActionController::Helpers into it in a booted app, and a plain non-controller
28
+ # class never has it). Same guard Pundit's own included block uses.
29
+ helper_method :policy, :can?, :cannot? if respond_to?(:helper_method)
26
30
  end
27
31
 
28
32
  def assert_current_user_present
@@ -0,0 +1,37 @@
1
+ module Glib::Auth
2
+ # Bearer-token authentication for machine-facing APIs. The token's storage column and
3
+ # the user model are app-specific, so the lookup is a template method the app's base
4
+ # controller implements (return the user for a valid token, nil otherwise).
5
+ module TokenAuthenticatable
6
+ extend ActiveSupport::Concern
7
+
8
+ # Public to mirror the browser side, where Devise's current_user is public: glib
9
+ # hands policies the controller (Glib::ApplicationPolicy#controller), and host-app
10
+ # policies call these with an EXPLICIT receiver (`controller.current_user`,
11
+ # `delegate :user_signed_in?, to: :controller`). A private current_user would raise
12
+ # NoMethodError on the API line only -- the two surfaces must behave the same.
13
+ def current_user
14
+ @current_user
15
+ end
16
+
17
+ # Provided by Devise's helpers on the browser side; defined here so glib policies
18
+ # that delegate it to the controller work unchanged on API controllers.
19
+ def user_signed_in?
20
+ current_user.present?
21
+ end
22
+
23
+ private
24
+ def authenticate_api_token!
25
+ token = request.headers['Authorization'].to_s.sub(/\ABearer\s+/i, '')
26
+ @current_user = glib_find_user_by_api_token(token) if token.present?
27
+ return if @current_user
28
+
29
+ render json: { error: 'unauthorized' }, status: :unauthorized
30
+ end
31
+
32
+ # App implements: return the user for a valid token, nil otherwise.
33
+ def glib_find_user_by_api_token(_token)
34
+ raise NotImplementedError, "#{self.class} must define glib_find_user_by_api_token(token)"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,52 @@
1
+ # The ApplicationController analog for agent- or machine-facing JSON APIs.
2
+ #
3
+ # Posture (do not re-derive it from a sibling app):
4
+ # - The Bearer token IS the credential; possession decides authentication. There is
5
+ # deliberately no cookie/session path in: ActionController::API has no CSRF protection,
6
+ # so a cookie-authenticated write endpoint here would be a CSRF target. Do not add one.
7
+ # - Authorization works exactly as on the browser side: policies decide, one per resource,
8
+ # derived by `glib_authorize_resource` from the controller name (override with
9
+ # `class:` when the derived name is wrong, as on any glib controller).
10
+ # - Every subclass defines `glib_load_resource` (glib has no default) and is covered by a
11
+ # policy, including index-only controllers (define it as a no-op there).
12
+ # - Opt OUT of authentication with `skip_before_action :authenticate_api_token!` for a
13
+ # public endpoint; never opt in ad hoc.
14
+ #
15
+ # Callback order is declaration order and is load-bearing:
16
+ # authenticate_api_token! -> glib_load_resource -> glib_authorize_resource
17
+ # Token auth is declared before glib_auth_init so no request can reach record loading or
18
+ # authorization unauthenticated. Keep the includes in that order in subclasses too.
19
+ #
20
+ # Unhandled exceptions keep Rails' default error body, which is HTML in development
21
+ # unless the app is api-only. Consumers wanting JSON there should set
22
+ # `config.debug_exception_response_format = :api` or add their own
23
+ # `rescue_from` (the browser side's json_libs_rescue_500 does this for the same reason).
24
+ class Glib::ApiController < ActionController::API
25
+ include Glib::Auth::TokenAuthenticatable
26
+ before_action :authenticate_api_token!
27
+
28
+ include Glib::Auth::Policy
29
+ glib_auth_init
30
+
31
+ rescue_from ActiveRecord::RecordNotFound do
32
+ render json: { error: 'not found' }, status: :not_found
33
+ end
34
+
35
+ rescue_from ActiveRecord::RecordInvalid do |e|
36
+ render json: { errors: e.record.errors }, status: :unprocessable_entity
37
+ end
38
+
39
+ rescue_from ActionController::ParameterMissing do
40
+ render json: { error: 'bad request' }, status: :bad_request
41
+ end
42
+
43
+ # The parent class, not Glib::Auth::Policy::UnauthorizedError: this covers both glib's
44
+ # raise sites (the subclass) and Pundit's own authorize(), which raises the parent --
45
+ # otherwise a consumer calling authorize() would get a 500 instead of JSON.
46
+ rescue_from Pundit::NotAuthorizedError do
47
+ # Token auth halts unauthenticated requests earlier, so this is normally 403;
48
+ # keep the 401 branch so the class is safe if a subclass skips token auth.
49
+ status = current_user ? :forbidden : :unauthorized
50
+ render json: { error: status.to_s }, status: status
51
+ end
52
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
5
+ module Glib
6
+ module Test
7
+ # Suite-wide RuboCop auto-remediation, lifted from the per-project
8
+ # `test/linters/rubocop_test.rb` convention (originated in neowork-str) so
9
+ # projects get it from the gem via a CONSCIOUS opt-in: include
10
+ # Glib::Test::RubocopGuards once in the test setup. Never auto-require
11
+ # this from Glib::TestHelpers — that would enable a file-rewriting test
12
+ # in every project without a decision. It lints the WHOLE repo, so files
13
+ # nobody touches cannot silently accumulate offenses.
14
+ #
15
+ # Runs `rubocop -a` (safe autocorrect), which REWRITES files on disk to
16
+ # fix offenses, then asserts nothing remains AND nothing was rewritten.
17
+ # The mutation is deliberate (the repo's auto-remediation convention —
18
+ # i18n-tasks normalize runs the same way): a run that dirties a file is
19
+ # reported as a FAILURE so the rewrite cannot go unnoticed — that is the
20
+ # drift signal. Do NOT strip the `-a`; review the diff, keep the corrected
21
+ # output, and re-run. If a long-passing test suddenly breaks, investigate
22
+ # WHY first: usually a non-canonical file was committed, or a rubocop
23
+ # version bump changed a cop's rules.
24
+ #
25
+ # It self-registers with Minitest, so it runs on EVERY `rails test`
26
+ # invocation, single-file runs included. That is cheap on purpose: RuboCop
27
+ # caches per-file results, so a full-repo run on an unchanged tree costs
28
+ # about a second on a mid-size app.
29
+ #
30
+ # Deliberately does NOT skip under ENV['CI']: the local rails-test
31
+ # wrapper sets CI=true, and skipping here would silence this layer
32
+ # exactly where it runs. Opt out per-run with SKIP_FULL_SUITE_RUBOCOP=1.
33
+ class FullSuiteRubocop < ::ActiveSupport::TestCase
34
+ # `--format simple` offense lines: "C: 12: 3: [Correctable] Cop/Name: message"
35
+ # (line/column right-aligned to width 3, so the padding is 0-2 spaces).
36
+ OFFENSE_LINE = /^[A-Z]: *\d+: *\d+: /
37
+
38
+ def test_rubocop_autocorrect_then_clean
39
+ skip 'opted out via SKIP_FULL_SUITE_RUBOCOP' if ENV['SKIP_FULL_SUITE_RUBOCOP'] == '1'
40
+
41
+ # `scrub`: stderr is merged in, and one invalid byte would turn the
42
+ # scans below into an ArgumentError instead of a lint failure.
43
+ output = `bundle exec rubocop -a --format simple 2>&1`.scrub
44
+ clean = $CHILD_STATUS.success?
45
+
46
+ # Three distinct outcomes hide in one output, and each needs a
47
+ # different action — the message must say which, or an agent told
48
+ # only "offenses detected" reaches for a `rubocop:disable`:
49
+ # [Corrected] -a already rewrote it on disk (exit 0 if that's all)
50
+ # [Correctable] survived -a because only the UNSAFE -A can fix it
51
+ # neither no autocorrect exists — semantic, read the cop
52
+ # Counted on the offense LINES only, so a cop message that happens to
53
+ # contain a marker cannot shift a bucket.
54
+ offenses = output.lines.grep(OFFENSE_LINE)
55
+ corrected = offenses.count { |line| line.include?('[Corrected]') }
56
+ unsafe = offenses.count { |line| line.include?('[Correctable]') }
57
+ semantic = offenses.size - corrected - unsafe
58
+
59
+ # A Proc message is built only on failure (Minitest calls it lazily).
60
+ assert(clean && corrected.zero?, -> { failure_message(output, corrected: corrected, unsafe: unsafe, semantic: semantic) })
61
+ end
62
+
63
+ private
64
+ def failure_message(output, corrected:, unsafe:, semantic:)
65
+ parts = []
66
+ if corrected.positive?
67
+ parts << "`rubocop -a` REWROTE #{corrected} offense(s) on disk (the [Corrected] lines below). That is the " \
68
+ 'drift signal, not a problem with this test: review `git diff`, keep the corrected output, ' \
69
+ 'and re-run — do not strip the -a.'
70
+ end
71
+ if unsafe.positive?
72
+ parts << "#{unsafe} offense(s) marked [Correctable] survived `-a` because their fix is UNSAFE: apply it " \
73
+ 'with `rubocop -A <file>` only after checking what it changes, or fix by hand.'
74
+ end
75
+ if semantic.positive?
76
+ parts << "#{semantic} offense(s) have NO autocorrect, so they are semantic, not mechanical: fix each the " \
77
+ "way the cop's own message prescribes — DevDoc cops name their prescribed alternative in the " \
78
+ "offense itself (full docs: the dev-doc repo's docs/cops/). A `rubocop:disable` is only for " \
79
+ "the case a cop's own doc calls genuinely unavoidable; disables already present in a file are " \
80
+ 'narrow exceptions, not precedent.'
81
+ end
82
+ parts << 'RuboCop did not finish cleanly (see output below).' if parts.empty?
83
+ "#{parts.join("\n\n")}\n\n#{output}"
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Glib
4
+ module Test
5
+ # Conscious opt-in for the suite-time RuboCop guard. Including this module
6
+ # ONCE in the project's test setup IS the project's explicit decision to
7
+ # run it. It must stay a separate include precisely because
8
+ # Glib::TestHelpers is mixed in for unrelated reasons (crawlers, travel
9
+ # helpers): auto-requiring the guard from there would enable it without
10
+ # any conscious decision, and FullSuiteRubocop runs `rubocop -a`, which
11
+ # REWRITES files on disk.
12
+ #
13
+ # class ActiveSupport::TestCase
14
+ # include Glib::Test::RubocopGuards # opts in: full-suite `rubocop -a`
15
+ # end
16
+ #
17
+ # This module is defined as soon as the gem loads (glib/test_helpers
18
+ # requires it at file level), so the include works on its own — in
19
+ # ActiveSupport::TestCase or in a single test class — with no ordering
20
+ # dependency on Glib::TestHelpers.
21
+ #
22
+ # What the include switches on: Glib::Test::FullSuiteRubocop — `rubocop -a`
23
+ # over the whole repo on every `rails test` invocation, failing when
24
+ # anything was rewritten or remains (runs under CI too).
25
+ #
26
+ # MIGRATION NOTE: in the published releases before this change (6.9.3 and
27
+ # earlier as released), Glib::TestHelpers auto-required a report-only
28
+ # ChangedFilesRubocop guard (working-tree edits only, skipped under CI),
29
+ # so every project had it without asking. That guard is retired:
30
+ # FullSuiteRubocop is a superset, and the local rails-test wrapper sets
31
+ # CI=true, where the old guard never ran anyway. A project bumping to the
32
+ # first release that contains this change must add the include line above
33
+ # or it silently has no suite-time RuboCop guard at all.
34
+ module RubocopGuards
35
+ extend ActiveSupport::Concern
36
+
37
+ included do
38
+ require 'glib/test/full_suite_rubocop'
39
+ end
40
+ end
41
+ end
42
+ end
@@ -1,15 +1,18 @@
1
+ # The suite-time RuboCop guard (FullSuiteRubocop) is NOT auto-loaded by this
2
+ # module: it runs `rubocop -a` and rewrites files, so switching it on is a
3
+ # conscious per-project opt-in (`include Glib::Test::RubocopGuards`). Requiring
4
+ # rubocop_guards here only DEFINES that inert module — nothing runs until a
5
+ # project explicitly includes it — and doing so at file level (this file is
6
+ # required when the gem loads) means the include works anywhere, with no
7
+ # dependency on Glib::TestHelpers being included first.
8
+ require 'glib/test/rubocop_guards'
9
+
1
10
  module Glib
2
11
  module TestHelpers
3
12
  extend ActiveSupport::Concern
4
13
 
5
14
  included do
6
15
  extend ClassMethods
7
-
8
- # Auto-load ChangedFilesRubocop only when this module is mixed into a
9
- # test class. AllHelpers also includes Glib::TestHelpers into view
10
- # helper modules in production controllers; in that context
11
- # ActiveSupport::TestCase is not loaded, so we skip the require.
12
- require 'glib/test/changed_files_rubocop' if defined?(::ActiveSupport::TestCase)
13
16
  end
14
17
 
15
18
  module ClassMethods
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glib-web
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.9.3
4
+ version: 6.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ''
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-29 00:00:00.000000000 Z
11
+ date: 2026-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activestorage
@@ -205,6 +205,7 @@ files:
205
205
  - app/controllers/concerns/glib/analytics/funnel.rb
206
206
  - app/controllers/concerns/glib/auth/policy.rb
207
207
  - app/controllers/concerns/glib/auth/response.rb
208
+ - app/controllers/concerns/glib/auth/token_authenticatable.rb
208
209
  - app/controllers/concerns/glib/form_stepper/controller.rb
209
210
  - app/controllers/concerns/glib/form_stepper/step.rb
210
211
  - app/controllers/concerns/glib/json/dynamic_text.rb
@@ -215,6 +216,7 @@ files:
215
216
  - app/controllers/concerns/glib/json/ui.rb
216
217
  - app/controllers/concerns/glib/json/validation.rb
217
218
  - app/controllers/concerns/glib/params/allowlist.rb
219
+ - app/controllers/glib/api_controller.rb
218
220
  - app/controllers/glib/blob_url_generators_controller.rb
219
221
  - app/controllers/glib/errors_controller.rb
220
222
  - app/controllers/glib/glib_direct_uploads_controller.rb
@@ -595,8 +597,9 @@ files:
595
597
  - lib/glib/rubocop/cops/localize.rb
596
598
  - lib/glib/rubocop/cops/test_name_parentheses.rb
597
599
  - lib/glib/snapshot.rb
598
- - lib/glib/test/changed_files_rubocop.rb
600
+ - lib/glib/test/full_suite_rubocop.rb
599
601
  - lib/glib/test/parallel_coverage.rb
602
+ - lib/glib/test/rubocop_guards.rb
600
603
  - lib/glib/test_helpers.rb
601
604
  - lib/glib/time_freezable_mailer.rb
602
605
  - lib/glib/time_returning_mailer.rb
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'English'
4
- require 'shellwords'
5
-
6
- module Glib
7
- module Test
8
- # Fast RuboCop guard for the subset-run workflow.
9
- #
10
- # Lives in glib-web's test helper so any project that includes
11
- # `Glib::TestHelpers` into `ActiveSupport::TestCase` picks it up. Minitest
12
- # runs all loaded test classes on every invocation, so this fires on every
13
- # `rails test` — but it lints ONLY the files you have changed in the
14
- # working tree (unstaged + staged + untracked). That keeps it fast and
15
- # fails the run the moment an edited file has an offense.
16
- #
17
- # Skip with `SKIP_CHANGED_RUBOCOP=1` during tight debug loops. Auto-skips
18
- # on CI because gem caches put thousands of vendored `.rb` files into the
19
- # working tree (the "working-tree diff == your edits" premise breaks).
20
- class ChangedFilesRubocop < ::ActiveSupport::TestCase
21
- # Extensions RuboCop targets by default (see its AllCops/Include). We
22
- # rely on `--force-exclusion` to honour the project's AllCops/Exclude.
23
- RUBOCOP_EXTENSIONS = %w[.rb .rake .jbuilder .gemspec].freeze
24
-
25
- def test_changed_files_are_rubocop_clean
26
- skip 'opted out via SKIP_CHANGED_RUBOCOP' if ENV['SKIP_CHANGED_RUBOCOP'] == '1'
27
- skip 'CI lints every file separately; this guard is for local working-tree edits' if ENV['CI']
28
-
29
- files = changed_ruby_files
30
- # IMPORTANT: never call rubocop with zero paths — it would lint the whole repo.
31
- skip 'no changed Ruby files in the working tree' if files.empty?
32
-
33
- output = `bundle exec rubocop --force-exclusion --format simple #{files.shelljoin} 2>&1`
34
-
35
- assert(
36
- $CHILD_STATUS.success?,
37
- 'RuboCop offenses in files you changed (fix them with `rubocop -a`):' \
38
- "\n\n#{output}"
39
- )
40
- end
41
-
42
- private
43
- def changed_ruby_files
44
- # `git status --porcelain` covers unstaged, staged, and untracked in
45
- # one spawn. Each line is `XY PATH` (or `XY ORIG -> PATH` for
46
- # renames); PATH starts at index 3. Deleted files are dropped by the
47
- # File.file? check below (no on-disk entry to lint).
48
- `git status --porcelain`
49
- .split("\n")
50
- .filter_map { |line| line[3..]&.split(' -> ')&.last }
51
- .select { |path| RUBOCOP_EXTENSIONS.include?(File.extname(path)) && File.file?(path) }
52
- end
53
- end
54
- end
55
- end