roda 3.79.0 → 3.81.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +12 -0
- data/doc/release_notes/3.80.0.txt +31 -0
- data/doc/release_notes/3.81.0.txt +24 -0
- data/lib/roda/plugins/assets.rb +3 -1
- data/lib/roda/plugins/exception_page.rb +1 -1
- data/lib/roda/plugins/filter_common_logger.rb +1 -1
- data/lib/roda/plugins/hmac_paths.rb +158 -11
- data/lib/roda/plugins/indifferent_params.rb +0 -3
- data/lib/roda/plugins/not_found.rb +1 -1
- data/lib/roda/plugins/route_csrf.rb +1 -1
- data/lib/roda/plugins/sessions.rb +1 -1
- data/lib/roda/version.rb +1 -1
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a9d94f31e568bd2774c3e582152f0bcdbffdf3fcd7e5a95241e73b3fd5b7ac3c
|
4
|
+
data.tar.gz: b9ad44642acdbaa0035cbd6ece521ca3432d2e7c23d75a331172ecd6c6288c60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1cd64e84b47a1a7a01d9165d16e14d95d2c43287b58f2557d9492a201832c1388157152eab1fd12052c7691e3969e5ce1cb9c0ad706e73ad6de822dcc29e1590
|
7
|
+
data.tar.gz: 86320c603d0c9b90e2c82f16cc5229b76afe66dde5bdbccce9b28ce076e80808a35410098fe0eae5cb3f5b3180b79f4265b3d525e7861a9dd65890315d274fad
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
= 3.81.0 (2024-06-12)
|
2
|
+
|
3
|
+
* Make assets plugin :early_hints option follow Rack 3 SPEC if using Rack 3 (jeremyevans)
|
4
|
+
|
5
|
+
* Correctly parse Ruby 3.4 backtraces in exception_page plugin (jeremyevans)
|
6
|
+
|
7
|
+
* Support :until and :seconds option in hmac_paths plugin, for paths valid only until a specific time (jeremyevans)
|
8
|
+
|
9
|
+
= 3.80.0 (2024-05-10)
|
10
|
+
|
11
|
+
* Support :namespace option in hmac_paths plugin, allowing for easy per-user/per-group HMAC paths (jeremyevans)
|
12
|
+
|
1
13
|
= 3.79.0 (2024-04-12)
|
2
14
|
|
3
15
|
* Do not update template mtime when there is an error reloading templates in the render plugin (jeremyevans)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* The hmac_paths plugin now supports a :namespace option for both hmac_path and
|
4
|
+
r.hmac_path. The :namespace option makes the generated HMAC values unique
|
5
|
+
per namespace, allowing easy use of per user/group HMAC paths. This can
|
6
|
+
be useful if the same path will show different information to different
|
7
|
+
users/groups, and you want to prevent path enumeration for each user/group
|
8
|
+
(not allow paths enumerated by one user/group to be valid for a different
|
9
|
+
user/group). Example:
|
10
|
+
|
11
|
+
hmac_path('/widget/1', namespace: '1')
|
12
|
+
# => "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
13
|
+
|
14
|
+
hmac_path('/widget/1', namespace: '2')
|
15
|
+
# => "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
16
|
+
|
17
|
+
The HMAC path created with namespace: '1' will only be valid when calling
|
18
|
+
r.hmac_path with namespace: '1' (similar for namespace: '2').
|
19
|
+
|
20
|
+
It is expected that the most common use of the :namespace option is to
|
21
|
+
reference session values, so the value of each path depends on the logged in
|
22
|
+
user. You can use the :namespace_session_key plugin option to set the
|
23
|
+
default namespace for both hmac_path and r.hmac_path:
|
24
|
+
|
25
|
+
plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
|
26
|
+
namespace_session_key: 'account_id'
|
27
|
+
|
28
|
+
This will use <tt>session['account_id']</tt> (converted to a string) as the namespace
|
29
|
+
for both hmac_path and r.hmac_path, unless a specific :namespace option is
|
30
|
+
given, making it simple to implement per user/group HMAC paths across an
|
31
|
+
application.
|
@@ -0,0 +1,24 @@
|
|
1
|
+
= New Features
|
2
|
+
|
3
|
+
* The hmac_paths plugin now supports :until and :seconds options for
|
4
|
+
hmac_path, to create a path that is only valid for a specific amount of
|
5
|
+
time. :until sets a specific time that the path will be valid until,
|
6
|
+
and :seconds makes the path only valid for the given number of seconds.
|
7
|
+
|
8
|
+
hmac_path('/widget/1', until: Time.utc(2100))
|
9
|
+
# => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
|
10
|
+
|
11
|
+
Requests for the path after the given time will not be matched by
|
12
|
+
r.hmac_path.
|
13
|
+
|
14
|
+
= Other Improvements
|
15
|
+
|
16
|
+
* The early_hints plugin now correctly follows the Rack 3 SPEC when
|
17
|
+
using Rack 3. This was not caught previously because Rack only
|
18
|
+
added official support for early_hints in the last month.
|
19
|
+
|
20
|
+
* Ruby 3.4 backtraces are now parsed correctly in the exception_page
|
21
|
+
plugin.
|
22
|
+
|
23
|
+
* Some plugins that accept a block no longer issue an unused block
|
24
|
+
warning on Ruby 3.4.
|
data/lib/roda/plugins/assets.rb
CHANGED
@@ -736,7 +736,9 @@ class Roda
|
|
736
736
|
paths = assets_paths(type)
|
737
737
|
if o[:early_hints]
|
738
738
|
early_hint_as = ltype == :js ? 'script' : 'style'
|
739
|
-
|
739
|
+
early_hints = paths.map{|p| "<#{p}>; rel=preload; as=#{early_hint_as}"}
|
740
|
+
early_hints = early_hints.join("\n") if Rack.release < '3'
|
741
|
+
send_early_hints(RodaResponseHeaders::LINK=>early_hints)
|
740
742
|
end
|
741
743
|
paths.map{|p| "#{tag_start}#{h(p)}#{tag_end}"}.join("\n")
|
742
744
|
end
|
@@ -245,7 +245,7 @@ END
|
|
245
245
|
|
246
246
|
frames = exception.backtrace.map.with_index do |line, i|
|
247
247
|
frame = {:id=>i}
|
248
|
-
if line =~ /\A(.*?):(\d+)(?::in `(.*)')?\Z/
|
248
|
+
if line =~ /\A(.*?):(\d+)(?::in [`'](.*)')?\Z/
|
249
249
|
filename = frame[:filename] = $1
|
250
250
|
lineno = frame[:lineno] = $2.to_i
|
251
251
|
frame[:function] = $3
|
@@ -112,13 +112,53 @@ class Roda
|
|
112
112
|
# this for POST requests (or other HTTP verbs that can have request bodies), use +r.GET+
|
113
113
|
# instead of +r.params+ to specifically check query string parameters.
|
114
114
|
#
|
115
|
-
#
|
115
|
+
# The generated paths can be timestamped, so that they are only valid until a given time
|
116
|
+
# or for a given number of seconds after they are generated, using the :until or :seconds
|
117
|
+
# options:
|
116
118
|
#
|
117
|
-
# hmac_path('/1',
|
118
|
-
# # =>
|
119
|
+
# hmac_path('/widget/1', until: Time.utc(2100))
|
120
|
+
# # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
|
121
|
+
#
|
122
|
+
# hmac_path('/widget/1', seconds: Time.utc(2100).to_i - Time.now.to_i)
|
123
|
+
# # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
|
124
|
+
#
|
125
|
+
# The :namespace option, if provided, should be a string, and it modifies the generated HMACs
|
126
|
+
# to only match those in the same namespace. This can be used to provide different paths to
|
127
|
+
# different users or groups of users.
|
128
|
+
#
|
129
|
+
# hmac_path('/widget/1', namespace: '1')
|
130
|
+
# # => "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
131
|
+
#
|
132
|
+
# hmac_path('/widget/1', namespace: '2')
|
133
|
+
# # => "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
134
|
+
#
|
135
|
+
# The +r.hmac_path+ method accepts a :namespace option, and if a :namespace option is
|
136
|
+
# provided, it will only match an hmac path if the namespace given matches the one used
|
137
|
+
# when the hmac path was created.
|
138
|
+
#
|
139
|
+
# r.hmac_path(namespace: '1'){}
|
140
|
+
# # will match "/3793ac2a72ea399c40cbd63f154d19f0fe34cdf8d347772134c506a0b756d590/n/widget/1"
|
141
|
+
# # will not match "/0e1e748860d4fd17fe9b7c8259b1e26996502c38e465f802c2c9a0a13000087c/n/widget/1"
|
142
|
+
#
|
143
|
+
# The most common use of the :namespace option is to reference session values, so the value of
|
144
|
+
# each path depends on the logged in user. You can use the +:namespace_session_key+ plugin
|
145
|
+
# option to set the default namespace for both +hmac_path+ and +r.hmac_path+:
|
146
|
+
#
|
147
|
+
# plugin :hmac_paths, secret: 'some-secret-value-with-at-least-32-bytes',
|
148
|
+
# namespace_session_key: 'account_id'
|
149
|
+
#
|
150
|
+
# This will use <tt>session['account_id']</tt> as the default namespace for both +hmac_path+
|
151
|
+
# and +r.hmac_path+ (if the session value is not nil, it is converted to a string using +to_s+).
|
152
|
+
# You can override the default namespace by passing a +:namespace+ option when calling +hmac_path+
|
153
|
+
# and +r.hmac_path+.
|
154
|
+
#
|
155
|
+
# You can use +:root+, +:method+, +:params+, and +:namespace+ at the same time:
|
156
|
+
#
|
157
|
+
# hmac_path('/1', root: '/widget', method: :get, params: {foo: 'bar'}, namespace: '1')
|
158
|
+
# # => "/widget/c14c78a81d34d766cf334a3ddbb7a6b231bc2092ef50a77ded0028586027b14e/mpn/1?foo=bar"
|
119
159
|
#
|
120
160
|
# This gives you a path only valid for a GET request with a root of <tt>/widget</tt> and
|
121
|
-
# a query string of <tt>foo=bar</tt
|
161
|
+
# a query string of <tt>foo=bar</tt>, using namespace +1+.
|
122
162
|
#
|
123
163
|
# To handle secret rotation, you can provide an +:old_secret+ option when loading the
|
124
164
|
# plugin.
|
@@ -128,6 +168,48 @@ class Roda
|
|
128
168
|
#
|
129
169
|
# This will use +:secret+ for constructing new paths, but will respect paths generated by
|
130
170
|
# +:old_secret+.
|
171
|
+
#
|
172
|
+
# = HMAC Construction
|
173
|
+
#
|
174
|
+
# This describes the internals for how HMACs are constructed based on the options provided
|
175
|
+
# to +hmac_path+. In the examples below:
|
176
|
+
#
|
177
|
+
# * +HMAC+ is the raw HMAC-SHA256 output (first argument is secret, second is data)
|
178
|
+
# * +HMAC_hex+ is the hexidecimal version of +HMAC+
|
179
|
+
# * +secret+ is the plugin :secret option
|
180
|
+
#
|
181
|
+
# The +:secret+ plugin option is never used directly as the HMAC secret. All HMACs are
|
182
|
+
# generated with a root-specific secret. The root will be the empty if no +:root+ option
|
183
|
+
# is given. The hmac path flags are always included in the hmac calculation, prepended to the
|
184
|
+
# path:
|
185
|
+
#
|
186
|
+
# r.hmac_path('/1')
|
187
|
+
# HMAC_hex(HMAC_hex(secret, ''), '/0/1')
|
188
|
+
#
|
189
|
+
# r.hmac_path('/1', root: '/2')
|
190
|
+
# HMAC_hex(HMAC_hex(secret, '/2'), '/0/1')
|
191
|
+
#
|
192
|
+
# The +:method+ option uses an uppercase version of the method prepended to the path. This
|
193
|
+
# cannot conflict with the path itself, since paths must start with a slash.
|
194
|
+
#
|
195
|
+
# r.hmac_path('/1', method: :get)
|
196
|
+
# HMAC_hex(HMAC_hex(secret, ''), 'GET:/m/1')
|
197
|
+
#
|
198
|
+
# The +:params+ option includes the query string for the params in the HMAC:
|
199
|
+
#
|
200
|
+
# r.hmac_path('/1', params: {k: 2})
|
201
|
+
# HMAC_hex(HMAC_hex(secret, ''), '/p/1?k=2')
|
202
|
+
#
|
203
|
+
# The +:until+ and +:seconds+ option include the timestamp in the HMAC:
|
204
|
+
#
|
205
|
+
# r.hmac_path('/1', until: Time.utc(2100))
|
206
|
+
# HMAC_hex(HMAC_hex(secret, ''), '/t/4102444800/1')
|
207
|
+
#
|
208
|
+
# If a +:namespace+ option is provided, the original secret used before the +:root+ option is
|
209
|
+
# an HMAC of the +:secret+ plugin option and the given namespace.
|
210
|
+
#
|
211
|
+
# r.hmac_path('/1', namespace: '2')
|
212
|
+
# HMAC_hex(HMAC_hex(HMAC(secret, '2'), ''), '/n/1')
|
131
213
|
module HmacPaths
|
132
214
|
def self.configure(app, opts=OPTS)
|
133
215
|
hmac_secret = opts[:secret]
|
@@ -143,6 +225,10 @@ class Roda
|
|
143
225
|
|
144
226
|
app.opts[:hmac_paths_secret] = hmac_secret
|
145
227
|
app.opts[:hmac_paths_old_secret] = hmac_old_secret
|
228
|
+
|
229
|
+
if opts[:namespace_session_key]
|
230
|
+
app.opts[:hmac_paths_namespace_session_key] = opts[:namespace_session_key]
|
231
|
+
end
|
146
232
|
end
|
147
233
|
|
148
234
|
module InstanceMethods
|
@@ -152,12 +238,17 @@ class Roda
|
|
152
238
|
# valid paths. The given path should be a string starting with +/+. Options:
|
153
239
|
#
|
154
240
|
# :method :: Limits the returned path to only be valid for the given request method.
|
241
|
+
# :namespace :: Make the HMAC value depend on the given namespace. If this is not
|
242
|
+
# provided, the default namespace is used. To explicitly not use a
|
243
|
+
# namespace when there is a default namespace, pass a nil value.
|
155
244
|
# :params :: Includes parameters in the query string of the returned path, and
|
156
245
|
# limits the returned path to only be valid for that exact query string.
|
157
246
|
# :root :: Should be an empty string or string starting with +/+. This will be
|
158
247
|
# the already matched path of the routing tree using r.hmac_path. Defaults
|
159
248
|
# to the empty string, which will returns paths valid for r.hmac_path at
|
160
249
|
# the top level of the routing tree.
|
250
|
+
# :seconds :: Make the given path valid for the given integer number of seconds.
|
251
|
+
# :until :: Make the given path valid until the given Time.
|
161
252
|
def hmac_path(path, opts=OPTS)
|
162
253
|
unless path.is_a?(String) && path.getbyte(0) == 47
|
163
254
|
raise RodaError, "path must be a string starting with /"
|
@@ -168,6 +259,12 @@ class Roda
|
|
168
259
|
raise RodaError, "root must be empty string or string starting with /"
|
169
260
|
end
|
170
261
|
|
262
|
+
if valid_until = opts[:until]
|
263
|
+
valid_until = valid_until.to_i
|
264
|
+
elsif seconds = opts[:seconds]
|
265
|
+
valid_until = Time.now.to_i + seconds
|
266
|
+
end
|
267
|
+
|
171
268
|
flags = String.new
|
172
269
|
path = path.dup
|
173
270
|
|
@@ -180,6 +277,15 @@ class Roda
|
|
180
277
|
path << '?' << Rack::Utils.build_query(params)
|
181
278
|
end
|
182
279
|
|
280
|
+
if hmac_path_namespace(opts)
|
281
|
+
flags << 'n'
|
282
|
+
end
|
283
|
+
|
284
|
+
if valid_until
|
285
|
+
flags << 't'
|
286
|
+
path = "/#{valid_until}#{path}"
|
287
|
+
end
|
288
|
+
|
183
289
|
flags << '0' if flags.empty?
|
184
290
|
|
185
291
|
hmac_path = if method
|
@@ -188,7 +294,7 @@ class Roda
|
|
188
294
|
"/#{flags}#{path}"
|
189
295
|
end
|
190
296
|
|
191
|
-
"#{root}/#{hmac_path_hmac(root, hmac_path)}/#{flags}#{path}"
|
297
|
+
"#{root}/#{hmac_path_hmac(root, hmac_path, opts)}/#{flags}#{path}"
|
192
298
|
end
|
193
299
|
|
194
300
|
# The HMAC to use in hmac_path, for the given root, path, and options.
|
@@ -196,14 +302,34 @@ class Roda
|
|
196
302
|
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, hmac_path_hmac_secret(root, opts), path)
|
197
303
|
end
|
198
304
|
|
305
|
+
# The namespace to use for the hmac path. If a :namespace option is not
|
306
|
+
# provided, and a :namespace_session_key option was provided, this will
|
307
|
+
# use the value of the related session key, if present.
|
308
|
+
def hmac_path_namespace(opts=OPTS)
|
309
|
+
opts.fetch(:namespace){hmac_path_default_namespace}
|
310
|
+
end
|
311
|
+
|
199
312
|
private
|
200
313
|
|
201
314
|
# The secret used to calculate the HMAC in hmac_path. This is itself an HMAC, created
|
202
|
-
# using the secret given in the plugin, for the given root and options.
|
315
|
+
# using the secret given in the plugin, for the given root and options.
|
316
|
+
# This always returns a hexidecimal string.
|
203
317
|
def hmac_path_hmac_secret(root, opts=OPTS)
|
204
318
|
secret = opts[:secret] || self.opts[:hmac_paths_secret]
|
319
|
+
|
320
|
+
if namespace = hmac_path_namespace(opts)
|
321
|
+
secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, secret, namespace)
|
322
|
+
end
|
323
|
+
|
205
324
|
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, secret, root)
|
206
325
|
end
|
326
|
+
|
327
|
+
# The default namespace to use for hmac_path, if a :namespace option is not provided.
|
328
|
+
def hmac_path_default_namespace
|
329
|
+
if (key = opts[:hmac_paths_namespace_session_key]) && (value = session[key])
|
330
|
+
value.to_s
|
331
|
+
end
|
332
|
+
end
|
207
333
|
end
|
208
334
|
|
209
335
|
module RequestMethods
|
@@ -221,6 +347,13 @@ class Roda
|
|
221
347
|
if submitted_hmac.bytesize == 64
|
222
348
|
on String do |flags|
|
223
349
|
if flags.bytesize >= 1
|
350
|
+
if flags.include?('n') ^ !scope.hmac_path_namespace(opts).nil?
|
351
|
+
# Namespace required and not provided, or provided and not required.
|
352
|
+
# Bail early to avoid unnecessary HMAC calculation.
|
353
|
+
@remaining_path = orig_path
|
354
|
+
return
|
355
|
+
end
|
356
|
+
|
224
357
|
if flags.include?('m')
|
225
358
|
rpath = "#{env['REQUEST_METHOD'].to_s.upcase}:#{rpath}"
|
226
359
|
end
|
@@ -229,8 +362,20 @@ class Roda
|
|
229
362
|
rpath = "#{rpath}?#{env["QUERY_STRING"]}"
|
230
363
|
end
|
231
364
|
|
232
|
-
if hmac_path_valid?(mpath, rpath, submitted_hmac)
|
233
|
-
|
365
|
+
if hmac_path_valid?(mpath, rpath, submitted_hmac, opts)
|
366
|
+
if flags.include?('t')
|
367
|
+
on Integer do |int|
|
368
|
+
if int >= Time.now.to_i
|
369
|
+
always(&block)
|
370
|
+
else
|
371
|
+
# Return from method without matching
|
372
|
+
@remaining_path = orig_path
|
373
|
+
return
|
374
|
+
end
|
375
|
+
end
|
376
|
+
else
|
377
|
+
always(&block)
|
378
|
+
end
|
234
379
|
end
|
235
380
|
end
|
236
381
|
|
@@ -249,11 +394,13 @@ class Roda
|
|
249
394
|
private
|
250
395
|
|
251
396
|
# Determine whether the provided hmac matches.
|
252
|
-
def hmac_path_valid?(root, path, hmac)
|
253
|
-
if Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path), hmac)
|
397
|
+
def hmac_path_valid?(root, path, hmac, opts=OPTS)
|
398
|
+
if Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
|
254
399
|
true
|
255
400
|
elsif old_secret = roda_class.opts[:hmac_paths_old_secret]
|
256
|
-
|
401
|
+
opts = opts.dup
|
402
|
+
opts[:secret] = old_secret
|
403
|
+
Rack::Utils.secure_compare(scope.hmac_path_hmac(root, path, opts), hmac)
|
257
404
|
else
|
258
405
|
false
|
259
406
|
end
|
@@ -53,13 +53,10 @@ class Roda
|
|
53
53
|
|
54
54
|
class Params < Rack::QueryParser::Params
|
55
55
|
if Rack.release >= '3'
|
56
|
-
# rack main branch compatibility
|
57
|
-
# :nocov:
|
58
56
|
if Params < Hash
|
59
57
|
def initialize
|
60
58
|
super(&INDIFFERENT_PROC)
|
61
59
|
end
|
62
|
-
# :nocov:
|
63
60
|
else
|
64
61
|
def initialize
|
65
62
|
@size = 0
|
@@ -476,7 +476,7 @@ class Roda
|
|
476
476
|
serialized_data << json_data
|
477
477
|
|
478
478
|
cipher_secret = opts[:cipher_secret]
|
479
|
-
if
|
479
|
+
if opts[:per_cookie_cipher_secret]
|
480
480
|
version = "\1"
|
481
481
|
per_cookie_secret_base = SecureRandom.random_bytes(32)
|
482
482
|
cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, per_cookie_secret_base)
|
data/lib/roda/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: roda
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.81.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jeremy Evans
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-06-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -254,6 +254,8 @@ extra_rdoc_files:
|
|
254
254
|
- doc/release_notes/3.78.0.txt
|
255
255
|
- doc/release_notes/3.79.0.txt
|
256
256
|
- doc/release_notes/3.8.0.txt
|
257
|
+
- doc/release_notes/3.80.0.txt
|
258
|
+
- doc/release_notes/3.81.0.txt
|
257
259
|
- doc/release_notes/3.9.0.txt
|
258
260
|
files:
|
259
261
|
- CHANGELOG
|
@@ -340,6 +342,8 @@ files:
|
|
340
342
|
- doc/release_notes/3.78.0.txt
|
341
343
|
- doc/release_notes/3.79.0.txt
|
342
344
|
- doc/release_notes/3.8.0.txt
|
345
|
+
- doc/release_notes/3.80.0.txt
|
346
|
+
- doc/release_notes/3.81.0.txt
|
343
347
|
- doc/release_notes/3.9.0.txt
|
344
348
|
- lib/roda.rb
|
345
349
|
- lib/roda/cache.rb
|
@@ -505,7 +509,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
505
509
|
- !ruby/object:Gem::Version
|
506
510
|
version: '0'
|
507
511
|
requirements: []
|
508
|
-
rubygems_version: 3.5.
|
512
|
+
rubygems_version: 3.5.9
|
509
513
|
signing_key:
|
510
514
|
specification_version: 4
|
511
515
|
summary: Routing tree web toolkit
|