action_controller-stashed_redirects 0.1.0 → 0.2.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: 57726ac99490f85375fd07300740792ad9ce85efda8c4eb728794c4243391636
4
- data.tar.gz: 2a3059590ed5e4032477d5600304668a5af6c4b4b55a51a31a3246a59be64e5e
3
+ metadata.gz: 48ec4248e68dd1601b20102049942417dd5fb85a6411b3e23940457653cb7bf8
4
+ data.tar.gz: 6e94e481bd7777dee9dc8f418aca0ffba90211058ac5c674585c582c5d133573
5
5
  SHA512:
6
- metadata.gz: bf260715badd07d6a091254fecbc3890ff21b69317fd0c05c8d50971a18f47be87a182da0f9ea6ffe2c4140d71b101f30003c24fe4ab28feb8671945aa260def
7
- data.tar.gz: 949bf794275255b14aeb5b7e46e6f0558218cf736eb7a1b686063c02af2c545128e8dc55f2e0984a097b844bb8f0dcc7ea89575e67181b1a06793f239d40294a
6
+ metadata.gz: d917ddb4a35845beb4b4dc57db8dc276e9017b0b5156c7dd3f321af6c17ecd050c468a6ad4cef51181a50c8c103a677d56762b328de5508862400f992b3519c2
7
+ data.tar.gz: cda0947e46c98f256995b240cbd4aded5771e8ae34352e1f228e6b65010664dd408e23f080f587fad1df6ff0c71dd6022018a7f7a14c98f0e8ced4bc85c20f19
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2022-04-21
4
+
5
+ - `stash_redirect_for` raises `ArgumentError` on invalid redirect URL
6
+
7
+ Protects against storing a URL that `redirect_to` can't redirect to later.
8
+
9
+ - `redirect_from_stashed` raises `ActionController::StashedRedirects::MissingRedirectError`
10
+
11
+ Useful to add a specific general fallback:
12
+
13
+ ```ruby
14
+ class ApplicationController < ActionController::Base
15
+ rescue_from(ActionController::StashedRedirects::MissingRedirectError) { redirect_to root_url }
16
+ end
17
+ ```
18
+
3
19
  ## [0.1.0] - 2022-04-21
4
20
 
5
21
  - Initial release
data/Gemfile CHANGED
@@ -8,5 +8,5 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "debug"
11
- gem "minitest", "~> 5.0"
11
+ gem "minitest"
12
12
  gem "railties"
data/Gemfile.lock CHANGED
@@ -1,89 +1,121 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- action_controller-stashed_redirects (0.1.0)
4
+ action_controller-stashed_redirects (0.2.0)
5
5
  actionpack (>= 7.0)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
9
9
  specs:
10
- actionpack (7.0.2.3)
11
- actionview (= 7.0.2.3)
12
- activesupport (= 7.0.2.3)
13
- rack (~> 2.0, >= 2.2.0)
10
+ actionpack (7.1.2)
11
+ actionview (= 7.1.2)
12
+ activesupport (= 7.1.2)
13
+ nokogiri (>= 1.8.5)
14
+ racc
15
+ rack (>= 2.2.4)
16
+ rack-session (>= 1.0.1)
14
17
  rack-test (>= 0.6.3)
15
- rails-dom-testing (~> 2.0)
16
- rails-html-sanitizer (~> 1.0, >= 1.2.0)
17
- actionview (7.0.2.3)
18
- activesupport (= 7.0.2.3)
18
+ rails-dom-testing (~> 2.2)
19
+ rails-html-sanitizer (~> 1.6)
20
+ actionview (7.1.2)
21
+ activesupport (= 7.1.2)
19
22
  builder (~> 3.1)
20
- erubi (~> 1.4)
21
- rails-dom-testing (~> 2.0)
22
- rails-html-sanitizer (~> 1.1, >= 1.2.0)
23
- activesupport (7.0.2.3)
23
+ erubi (~> 1.11)
24
+ rails-dom-testing (~> 2.2)
25
+ rails-html-sanitizer (~> 1.6)
26
+ activesupport (7.1.2)
27
+ base64
28
+ bigdecimal
24
29
  concurrent-ruby (~> 1.0, >= 1.0.2)
30
+ connection_pool (>= 2.2.5)
31
+ drb
25
32
  i18n (>= 1.6, < 2)
26
33
  minitest (>= 5.1)
34
+ mutex_m
27
35
  tzinfo (~> 2.0)
36
+ base64 (0.2.0)
37
+ bigdecimal (3.1.5)
28
38
  builder (3.2.4)
29
- concurrent-ruby (1.1.10)
39
+ concurrent-ruby (1.2.2)
40
+ connection_pool (2.4.1)
30
41
  crass (1.0.6)
31
- debug (1.5.0)
32
- irb (>= 1.3.6)
33
- reline (>= 0.2.7)
34
- erubi (1.10.0)
35
- i18n (1.10.0)
42
+ debug (1.9.1)
43
+ irb (~> 1.10)
44
+ reline (>= 0.3.8)
45
+ drb (2.2.0)
46
+ ruby2_keywords
47
+ erubi (1.12.0)
48
+ i18n (1.14.1)
36
49
  concurrent-ruby (~> 1.0)
37
- io-console (0.5.11)
38
- irb (1.4.1)
39
- reline (>= 0.3.0)
40
- loofah (2.16.0)
50
+ io-console (0.7.1)
51
+ irb (1.11.0)
52
+ rdoc
53
+ reline (>= 0.3.8)
54
+ loofah (2.22.0)
41
55
  crass (~> 1.0.2)
42
- nokogiri (>= 1.5.9)
43
- method_source (1.0.0)
44
- minitest (5.15.0)
45
- nokogiri (1.13.4-aarch64-linux)
56
+ nokogiri (>= 1.12.0)
57
+ minitest (5.20.0)
58
+ mutex_m (0.2.0)
59
+ nokogiri (1.16.0-aarch64-linux)
46
60
  racc (~> 1.4)
47
- nokogiri (1.13.4-arm64-darwin)
61
+ nokogiri (1.16.0-arm-linux)
48
62
  racc (~> 1.4)
49
- nokogiri (1.13.4-x86-linux)
63
+ nokogiri (1.16.0-arm64-darwin)
50
64
  racc (~> 1.4)
51
- nokogiri (1.13.4-x86_64-linux)
65
+ nokogiri (1.16.0-x86-linux)
52
66
  racc (~> 1.4)
53
- racc (1.6.0)
54
- rack (2.2.3)
55
- rack-test (1.1.0)
56
- rack (>= 1.0, < 3)
57
- rails-dom-testing (2.0.3)
58
- activesupport (>= 4.2.0)
67
+ nokogiri (1.16.0-x86_64-linux)
68
+ racc (~> 1.4)
69
+ psych (5.1.2)
70
+ stringio
71
+ racc (1.7.3)
72
+ rack (3.0.8)
73
+ rack-session (2.0.0)
74
+ rack (>= 3.0.0)
75
+ rack-test (2.1.0)
76
+ rack (>= 1.3)
77
+ rackup (2.1.0)
78
+ rack (>= 3)
79
+ webrick (~> 1.8)
80
+ rails-dom-testing (2.2.0)
81
+ activesupport (>= 5.0.0)
82
+ minitest
59
83
  nokogiri (>= 1.6)
60
- rails-html-sanitizer (1.4.2)
61
- loofah (~> 2.3)
62
- railties (7.0.2.3)
63
- actionpack (= 7.0.2.3)
64
- activesupport (= 7.0.2.3)
65
- method_source
84
+ rails-html-sanitizer (1.6.0)
85
+ loofah (~> 2.21)
86
+ nokogiri (~> 1.14)
87
+ railties (7.1.2)
88
+ actionpack (= 7.1.2)
89
+ activesupport (= 7.1.2)
90
+ irb
91
+ rackup (>= 1.0.0)
66
92
  rake (>= 12.2)
67
- thor (~> 1.0)
68
- zeitwerk (~> 2.5)
69
- rake (13.0.6)
70
- reline (0.3.1)
93
+ thor (~> 1.0, >= 1.2.2)
94
+ zeitwerk (~> 2.6)
95
+ rake (13.1.0)
96
+ rdoc (6.6.2)
97
+ psych (>= 4.0.0)
98
+ reline (0.4.2)
71
99
  io-console (~> 0.5)
72
- thor (1.2.1)
73
- tzinfo (2.0.4)
100
+ ruby2_keywords (0.0.5)
101
+ stringio (3.1.0)
102
+ thor (1.3.0)
103
+ tzinfo (2.0.6)
74
104
  concurrent-ruby (~> 1.0)
75
- zeitwerk (2.5.4)
105
+ webrick (1.8.1)
106
+ zeitwerk (2.6.12)
76
107
 
77
108
  PLATFORMS
78
109
  arm64-darwin-20
110
+ arm64-darwin-23
79
111
  linux
80
112
 
81
113
  DEPENDENCIES
82
114
  action_controller-stashed_redirects!
83
115
  debug
84
- minitest (~> 5.0)
116
+ minitest
85
117
  railties
86
118
  rake (~> 13.0)
87
119
 
88
120
  BUNDLED WITH
89
- 2.3.11
121
+ 2.5.4
data/README.md CHANGED
@@ -12,13 +12,14 @@ class ApplicationController < ActionController::Base
12
12
 
13
13
  private
14
14
  def authenticate
15
- redirect_to new_session_url unless Current.user
15
+ # Pass `redirect_url:` to pass the URL we're currently on.
16
+ redirect_to new_session_url(redirect_url: request.url) unless Current.user
16
17
  end
17
18
  end
18
19
 
19
20
  class SessionsController < ApplicationController
20
21
  # Stash a redirect at the start of the session authentication flow,
21
- # from either params[:redirect_url] or request.referer in that order.
22
+ # from `params[:redirect_url]` automatically.
22
23
  stash_redirect_for :sign_in, on: :new
23
24
 
24
25
  def new
@@ -28,6 +29,8 @@ class SessionsController < ApplicationController
28
29
  if User.authenticate_by(session_params)
29
30
  # On success, redirect the user back to where they first tried to access before being authenticated.
30
31
  redirect_from_stashed :sign_in
32
+ else
33
+ render :new, status: :unprocessable_entity
31
34
  end
32
35
  end
33
36
  end
@@ -37,6 +40,105 @@ See the internal documentation for more usage information.
37
40
 
38
41
  Only internal redirects are allowed, so attackers can't pass an external `redirect_url`.
39
42
 
43
+ ### Making a sudo authentication system
44
+
45
+ Consider a flow where you want to require super-user, or sudo, privileges for a given action, e.g. type in your password before you can change your credit card.
46
+
47
+ We'll make a `require_sudo` API that we can annotate our controller with like this:
48
+
49
+ ```ruby
50
+ class Billing::CreditCardsController < ApplicationController
51
+ require_sudo # Require sudo on all actions in this controller.
52
+ # require_sudo_on :edit, :update # Or just for some actions.
53
+
54
+ def edit
55
+ end
56
+
57
+ def update
58
+ Current.user.billing.credit_cards.find(params[:id]).update!(credit_card_params)
59
+ end
60
+ end
61
+ ```
62
+
63
+ `require_sudo` or `require_sudo_on` can come from a controller concern like this:
64
+
65
+ ```ruby
66
+ # app/controllers/concerns/sudo/examination.rb
67
+ module Sudo::Examination
68
+ extend ActiveSupport::Concern
69
+
70
+ class_methods do
71
+ def require_sudo_on(*actions, **options) = require_sudo(only: *actions, **options)
72
+ def require_sudo(...) = before_action(:require_sudo, ...)
73
+ end
74
+
75
+ private
76
+ def require_sudo
77
+ if sudo.exam_needed?
78
+ raise "Non-get: can't redirect back here, make sure you do …something with an interstitial page?" unless request.get?
79
+ redirect_to new_sudo_authentications_url(redirect_url: request.url)
80
+ end
81
+ end
82
+
83
+ def sudo = Sudo.new(session)
84
+ end
85
+
86
+ # Which we include in ApplicationController:
87
+ class ApplicationController < ActionController::Base
88
+ include Sudo::Examination
89
+ end
90
+ ```
91
+
92
+ Notice how in `redirect_to new_sudo_authentications_url(redirect_url: request.original_url)` we're passing the `redirect_url:` along that `ActionController::StashedRedirects` will need.
93
+ It's pointing back to the page we're on, which required sudo authentication, so we can redirect back to it after the sudo exam has been passed.
94
+
95
+ Next up, we can add an in-memory PORO model to give the behavior some better names:
96
+
97
+ ```ruby
98
+ # app/models/sudo.rb
99
+ class Sudo < Data.define(:store)
100
+ def passed!
101
+ store[:sudo_expires_at] = 15.minutes.from_now
102
+ end
103
+
104
+ def exam_needed?
105
+ expires_at = store[:sudo_expires_at]
106
+ expires_at.nil? || Time.parse(expires_at).past?
107
+ end
108
+ end
109
+ ```
110
+
111
+ Finally, we can add the authenticating sudo controller itself, where `stash_redirect_for` will use the `redirect_url:` from earlier:
112
+
113
+ ```ruby
114
+ # app/controllers/sudo/authentications_controller.rb
115
+ class Sudo::AuthenticationsController < ApplicationController
116
+ stash_redirect_for :sudo, on: :new
117
+
118
+ def new
119
+ redirect_from_stashed :sudo unless sudo.exam_needed?
120
+ end
121
+
122
+ def create
123
+ if pass_sudo_exam?
124
+ sudo.passed!
125
+ redirect_from_stashed :sudo
126
+ else
127
+ render :new, status: :unprocessable_entity
128
+ end
129
+ end
130
+ private def pass_sudo_exam? = Current.user.authenticate_password(params[:password])
131
+ end
132
+
133
+ # config/routes.rb
134
+ namespace :sudo do
135
+ resources :authentications
136
+ end
137
+ ```
138
+
139
+ Users can now fill-in their password, which will hit `sudo/authentications#create` and redirect them back to the edit form on the
140
+ credit cards flow if it's the correct password.
141
+
40
142
  ## Installation
41
143
 
42
144
  Install the gem and add to the application's Gemfile by executing:
@@ -55,7 +157,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
55
157
 
56
158
  ## Contributing
57
159
 
58
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/action_controller-stashed_redirects.
160
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kaspth/action_controller-stashed_redirects.
59
161
 
60
162
  ## License
61
163
 
data/Rakefile CHANGED
@@ -1,12 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "rake/testtask"
5
-
6
- Rake::TestTask.new(:test) do |t|
7
- t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/test_*.rb"]
10
- end
11
-
12
- task default: :test
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.summary = "Embed a controller flow within another by stashing the final redirect upfront and performing it after completing."
12
12
  spec.homepage = "https://github.com/kaspth/action_controller-stashed_redirects"
13
13
  spec.license = "MIT"
14
- spec.required_ruby_version = ">= 2.7.0"
14
+ spec.required_ruby_version = ">= 3.0.0"
15
15
 
16
16
  spec.metadata["homepage_uri"] = spec.homepage
17
17
  spec.metadata["source_code_uri"] = spec.homepage
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActionController
4
4
  module StashedRedirects
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support"
4
- require_relative "stashed_redirects/version"
5
4
 
6
5
  # Pass between different controller flows via stashed redirects
7
6
  #
@@ -9,67 +8,93 @@ require_relative "stashed_redirects/version"
9
8
  module ActionController::StashedRedirects
10
9
  extend ActiveSupport::Concern
11
10
 
12
- class_methods do
13
- # Adds a before_action to stash a redirect for a given `on:` action.
14
- #
15
- # stash_redirect_for :sudo_authentication, on: :new
16
- # stash_redirect_for :sign_in, from: :referer, on: :new
17
- # stash_redirect_for :sign_in, from: -> { update_post_path(@post) }
18
- def stash_redirect_for(purpose, on:, from: nil)
19
- before_action(-> { stash_redirect_for(purpose, from: from.respond_to?(:call) ? instance_exec(&from) : from) }, only: on)
11
+ autoload :VERSION, "action_controller/stashed_redirects/version"
12
+
13
+ # Allow a general `rescue ActionController::StashedRedirects::Error`.
14
+ Error = Module.new
15
+
16
+ class MissingRedirectError < StandardError
17
+ include Error
18
+
19
+ attr_reader :purpose
20
+
21
+ def initialize(purpose)
22
+ super "can't extract a stashed redirect_url to redirect_to"
23
+ @purpose = purpose
20
24
  end
21
25
  end
22
26
 
23
- # Stashes a redirect URL in the `session` under the given +purpose+.
24
- #
25
- # An explicit +redirect_url+ can be passsed, otherwise the redirect URL is
26
- # derived from `params[:redirect_url]` then falling back to `request.referer`.
27
- #
28
- # stash_redirect_for :sign_in
29
- # stash_redirect_for :sign_in, from: url_from(params[:redirect_url]) || root_url
30
- # stash_redirect_for :sign_in, from: :param # Only derive the redirect URL from `params[:redirect_url]`.
31
- # stash_redirect_for :sign_in, from: :referer # Only derive the redirect URL from `request.referer`.
32
- def stash_redirect_for(purpose, from: nil)
33
- if url = derive_stash_redirect_url_from(from)
34
- session[KEY_GENERATOR.(purpose)] = url
35
- else
36
- raise ArgumentError, "missing a redirect_url to stash, pass one via from: or via a redirect_url URL param"
27
+ class_methods do
28
+ # Adds a `before_action` to stash a redirect in a given `on:` action.
29
+ #
30
+ # stash_redirect_for :sign_in, on: :new
31
+ # stash_redirect_for :sign_in, on: %i[ new edit ]
32
+ # stash_redirect_for :sign_in, on: :new, url: -> { update_post_path(@post) }
33
+ def stash_redirect_for(purpose, on:, url: DEFAULT_URL)
34
+ before_action(-> { stash_redirect_for(purpose, url: url) }, only: on)
37
35
  end
38
36
  end
39
37
 
40
- # Finds and deletes the redirect stashed in `session` under the given +purpose+, then redirects.
41
- #
42
- # redirect_from_stashed :login
43
- #
44
- # Raises if no stashed redirect is found under the given +purpose+.
45
- #
46
- # Relies on +redirect_to+'s open redirect protection, see it's documentation for more.
47
- def redirect_from_stashed(purpose)
48
- redirect_to stashed_redirect_url_for(purpose)
49
- end
38
+ private
39
+ # Stashes a redirect URL in the `session` under the given +purpose+.
40
+ #
41
+ # An explicit +redirect_url+ can be passed in `from:`, otherwise the redirect URL is
42
+ # derived from `params[:redirect_url]` then falling back to `request.referer` on GET requests.
43
+ #
44
+ # stash_redirect_for :sign_in
45
+ # stash_redirect_for :sign_in, from: url_from(params[:redirect_url]) || root_url
46
+ # stash_redirect_for :sign_in, from: :param # Only derive the redirect URL from `params[:redirect_url]`.
47
+ # stash_redirect_for :sign_in, from: :referer # Only derive the redirect URL from `request.referer`.
48
+ def stash_redirect_for(purpose, url: DEFAULT_URL)
49
+ if url = derive_stash_redirect_url_from(url)
50
+ session[KEY_GENERATOR.(purpose)] = url
51
+ else
52
+ raise ArgumentError, "missing a redirect_url to stash, pass one via from: or via a redirect_url URL param"
53
+ end
54
+ end
50
55
 
51
- # Deletes the redirect stashed in the `session` under the given +purpose+ and returns it if any.
52
- #
53
- # discard_stashed_redirect_for :login # => the login redirect URL or nil.
54
- def discard_stashed_redirect_for(purpose)
55
- session.delete(KEY_GENERATOR.(purpose))
56
- end
56
+ # Finds and deletes the redirect stashed in `session` under the given +purpose+, then redirects.
57
+ #
58
+ # redirect_from_stashed :sign_in
59
+ #
60
+ # Raises if no stashed redirect is found under the given +purpose+.
61
+ #
62
+ # Relies on +redirect_to+'s open redirect protection, see it's documentation for more.
63
+ def redirect_from_stashed(purpose)
64
+ redirect_to stashed_redirect_url_for(purpose)
65
+ end
57
66
 
58
- private
59
- KEY_GENERATOR = ->(purpose) { "__url_stash_#{purpose}" }
60
- private_constant :KEY_GENERATOR
67
+ # Deletes and returns the redirect stashed in the `session` under the given +purpose+ if any.
68
+ #
69
+ # discard_stashed_redirect_for :sign_in # => the sign_in redirect URL or nil.
70
+ def discard_stashed_redirect_for(purpose)
71
+ session.delete(KEY_GENERATOR.(purpose))
72
+ end
61
73
 
62
74
  def stashed_redirect_url_for(purpose)
63
- raise ArgumentError, "can't extract a stashed redirect_url from session, none found" \
64
- unless redirect_url = discard_stashed_redirect_for(purpose)
65
-
66
- url_from(redirect_url)
75
+ url_from(discard_stashed_redirect_for(purpose)) or raise MissingRedirectError, purpose
67
76
  end
68
77
 
69
- def derive_stash_redirect_url_from(from)
70
- from ||= %i[ param referer ]
71
- { param: params[:redirect_url], referer: request.get? && request.referer }.values_at(*from).find(&:present?) || from
78
+ def derive_stash_redirect_url_from(url)
79
+ case url
80
+ when DEFAULT_URL then redirect_url
81
+ when String then url_from url
82
+ when Symbol, Proc then url_from instance_exec(self, &url)
83
+ end
72
84
  end
85
+
86
+ # Looks up a redirect URL from `params[:redirect_url]` using
87
+ # `url_from` as the protection mechanism to ensure it's a valid internal redirect.
88
+ #
89
+ # Can be passed to `redirect_to` with a fallback:
90
+ #
91
+ # redirect_to redirect_url || users_url
92
+ def redirect_url = url_from(params[:redirect_url])
93
+
94
+ DEFAULT_URL = Object.new
95
+
96
+ KEY_GENERATOR = ->(purpose) { "__url_stash_#{purpose}" }
97
+ private_constant :KEY_GENERATOR
73
98
  end
74
99
 
75
100
  ActiveSupport.on_load(:action_controller) { include ActionController::StashedRedirects }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_controller-stashed_redirects
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kasper Timm Hansen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-04-21 00:00:00.000000000 Z
11
+ date: 2024-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -55,14 +55,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
55
55
  requirements:
56
56
  - - ">="
57
57
  - !ruby/object:Gem::Version
58
- version: 2.7.0
58
+ version: 3.0.0
59
59
  required_rubygems_version: !ruby/object:Gem::Requirement
60
60
  requirements:
61
61
  - - ">="
62
62
  - !ruby/object:Gem::Version
63
63
  version: '0'
64
64
  requirements: []
65
- rubygems_version: 3.3.11
65
+ rubygems_version: 3.5.10
66
66
  signing_key:
67
67
  specification_version: 4
68
68
  summary: Embed a controller flow within another by stashing the final redirect upfront