clickwrap 0.3.1 → 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 +4 -4
- data/CHANGELOG.md +69 -0
- data/guides/migrating.md +23 -0
- data/guides/request-evidence.md +24 -0
- data/lib/clickwrap/canonical_json.rb +18 -2
- data/lib/clickwrap/configuration.rb +22 -9
- data/lib/clickwrap/dsl/policy_builder.rb +35 -9
- data/lib/clickwrap/models/request_evidence.rb +12 -5
- data/lib/clickwrap/request_evidence_policy.rb +14 -0
- data/lib/clickwrap/version.rb +1 -1
- data/lib/generators/clickwrap/install_generator.rb +7 -4
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4e284e0a7a0474b4dc4d3031424074d45c06dfb3f4432d78a3c64c2ec9ca77ba
|
|
4
|
+
data.tar.gz: 1b09f1436d6f8b18bcfb7f39b4f4f33e311438bb79a020a6b9f47c777cdada15
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1778cebf0236280efaab60e63ec3f6591742e12188c27c152923d6ee58ad1eb04bc71c74b14f1b606ed93ea3212d8f307fc31e284db0a595235f20848311907e
|
|
7
|
+
data.tar.gz: 653515517233ee6b0d94245098c0d5dff6bd48cf3c708183b86158c9147a579aeac9ab345d5b52eda9ac7d53fc8f291feedfc2e286291da26a38fe123cf88f0e
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.3] - 2026-08-31
|
|
10
|
+
|
|
11
|
+
### Fixed — the actor class survives a reload
|
|
12
|
+
|
|
13
|
+
- **`config.actor_class` followed the constant it had cached, not the one the
|
|
14
|
+
app has.** In a Rails app with reloading, every capture after the first code
|
|
15
|
+
change of a session raised
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Clickwrap was asked to record User as the actor, but
|
|
19
|
+
`config.actor_class_name` says the records that can act are User.
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Both halves name the same class, which reads as a contradiction and sends
|
|
23
|
+
whoever hits it to an initializer that is correct. They were two different
|
|
24
|
+
Ruby objects printing the same name: `actor_class` memoized the
|
|
25
|
+
constantized `Class` and re-resolved only when the class NAME changed —
|
|
26
|
+
which never happens, since the name is set once in an initializer — while
|
|
27
|
+
Zeitwerk replaces the class behind that name on every edit. The cache then
|
|
28
|
+
held a class no living record was an instance of, so
|
|
29
|
+
`actor.is_a?(config.actor_class)` was false for an ordinary `User` and
|
|
30
|
+
capture refused to record it.
|
|
31
|
+
|
|
32
|
+
The memo is gone. `constantize` after the first load is a `const_get` and
|
|
33
|
+
this is called once per capture rather than in a loop, so it was never
|
|
34
|
+
buying anything; `parent_controller_class` has always resolved this way.
|
|
35
|
+
Production was never affected — classes are not reloaded there — but every
|
|
36
|
+
development session was, from its second edit onwards.
|
|
37
|
+
|
|
38
|
+
## [0.3.2] - 2026-08-20
|
|
39
|
+
|
|
40
|
+
### Fixed — two guards that looked like they held
|
|
41
|
+
|
|
42
|
+
- **Canonical JSON refuses invalid bytes whatever their encoding tag.**
|
|
43
|
+
`valid_encoding?` is always true on an `ASCII-8BIT` string — BINARY has no
|
|
44
|
+
invalid byte sequences by definition — and BINARY is exactly what Rack and
|
|
45
|
+
CDN headers deliver, so the UTF-8 guard was a no-op for the ten
|
|
46
|
+
request-evidence values most likely to be malformed. Invalid bytes passed
|
|
47
|
+
through and produced canonical JSON that was not itself valid UTF-8, which
|
|
48
|
+
RFC 8785 forbids and a verifier in another language may reject or normalize
|
|
49
|
+
into a different digest. The guard now normalizes the tag before validating.
|
|
50
|
+
**No digest ever written changes**: bytes that are valid UTF-8 canonicalize
|
|
51
|
+
byte-identically whether they arrive tagged BINARY or UTF-8, which a test
|
|
52
|
+
pins. And a stored value that can no longer be canonicalized now reports a
|
|
53
|
+
binding mismatch instead of raising out of the integrity check — a check
|
|
54
|
+
that crashes tells an operator nothing except that the tool broke.
|
|
55
|
+
- **`record_ip_geolocation(country: nil)` is refused instead of quietly
|
|
56
|
+
enabling three fields.** With plain `nil` keyword defaults an explicit nil
|
|
57
|
+
was indistinguishable from an omitted keyword, so a policy written as
|
|
58
|
+
`record_ip_geolocation(country: settings[:geo])` with an empty setting fell
|
|
59
|
+
through to the coarse-trio default — enabling a category of personal data as
|
|
60
|
+
a side effect, which the frictionless pass never relaxed. A sentinel now
|
|
61
|
+
tells the two apart: unmentioned still gets the coarse trio, `false` still
|
|
62
|
+
reaches the coherence check that names `do_not_record_ip_geolocation`, and
|
|
63
|
+
`nil` raises a sentence.
|
|
64
|
+
- **A scaffolding `legal_basis_reference:` is refused like a scaffolding
|
|
65
|
+
`because:`.** The reference lands in the compiled policy revision and every
|
|
66
|
+
receipt built from it, permanently, where "TODO: ask legal" reads as a
|
|
67
|
+
reviewed determination rather than an omission. The option stays optional;
|
|
68
|
+
text the host actually wrote has to be text they meant.
|
|
69
|
+
|
|
70
|
+
### Documentation
|
|
71
|
+
|
|
72
|
+
- The installer's purpose and retention prompts now describe what the
|
|
73
|
+
installer actually does since 0.3.x: a blank purpose is accepted (the gem
|
|
74
|
+
records its own stated purpose, marked as the gem's), a blank period keeps
|
|
75
|
+
pace with the evidence it corroborates, and only scaffolding text or a
|
|
76
|
+
negative period stops generation.
|
|
77
|
+
|
|
9
78
|
## [0.3.1] - 2026-08-20
|
|
10
79
|
|
|
11
80
|
### Changed — the rest of the collection friction, and the principle behind removing it
|
data/guides/migrating.md
CHANGED
|
@@ -333,3 +333,26 @@ never satisfy a human-action predicate.)
|
|
|
333
333
|
| [FinePrint README at the audited commit](https://github.com/openstax/fine_print/blob/3b75fbcbcfb048ecd2f4ee7c4f0b9bd3d10f7603/README.md#L7-L25) | Pinned source code |
|
|
334
334
|
| [FinePrint signature model at the audited commit](https://github.com/openstax/fine_print/blob/3b75fbcbcfb048ecd2f4ee7c4f0b9bd3d10f7603/app/models/fine_print/signature.rb#L1-L33) | Pinned source code |
|
|
335
335
|
| The never-synthesize rule, the `unknown:` vocabulary, and the dry-run-first workflow | Product-design inference |
|
|
336
|
+
|
|
337
|
+
## Completeness: enumerate writers, not rows
|
|
338
|
+
|
|
339
|
+
A migration inventory built by looking at what the database HOLDS will miss
|
|
340
|
+
what the code can WRITE. One production integration classified 52 distinct
|
|
341
|
+
audit event types from its data — the codebase carried ~150: everything rare
|
|
342
|
+
(incident and recovery paths that had never fired) and everything new
|
|
343
|
+
(writers added after the cutover) was invisible to the data-only sweep, and
|
|
344
|
+
one of the missed stores turned out to hold the product's most consequential
|
|
345
|
+
assent act.
|
|
346
|
+
|
|
347
|
+
Sweep three ways, and reconcile the lists:
|
|
348
|
+
|
|
349
|
+
1. **Schema**: every table and column whose name says assent — accepted,
|
|
350
|
+
declared, consent, attest, authoriz, acknowledg, signed.
|
|
351
|
+
2. **Writers**: every place code can create such a record — grep the literals
|
|
352
|
+
(`event_type:`, ledger inserts), not the rows.
|
|
353
|
+
3. **Structure**: every model already carrying `has_clickwrap_evidence`, and
|
|
354
|
+
every dual-write site.
|
|
355
|
+
|
|
356
|
+
Then write a disposition for every entry — import it, "not a clickwrap" with
|
|
357
|
+
the reason, or "provider record" — in a tracked document. Silently ignoring a
|
|
358
|
+
store is not a valid disposition.
|
data/guides/request-evidence.md
CHANGED
|
@@ -575,3 +575,27 @@ can never undo or stand in for the Clickwrap event. Clickwrap ships no Footprint
|
|
|
575
575
|
| [Trackdown v0.4.0 result object](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/location_result.rb), [configuration](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/configuration.rb), [Cloudflare provider](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/providers/cloudflare_provider.rb), [MaxMind provider](https://github.com/rameerez/trackdown/blob/v0.4.0/lib/trackdown/providers/maxmind_provider.rb), [PR #9](https://github.com/rameerez/trackdown/pull/9) | Pinned released source and project change record |
|
|
576
576
|
| [Footprinted pinned model](https://github.com/rameerez/footprinted/blob/03b714bd3fa31368a8ce6695433386128fb6f91c/lib/footprinted/footprint.rb#L7-L52), [tracking concern](https://github.com/rameerez/footprinted/blob/03b714bd3fa31368a8ce6695433386128fb6f91c/lib/footprinted/model.rb#L7-L65) | Pinned source code |
|
|
577
577
|
| The field selection, the ordering of evidentiary priority, and every API prescription above | Product-design inference |
|
|
578
|
+
|
|
579
|
+
## Rendering recorded values: encodings survive encryption
|
|
580
|
+
|
|
581
|
+
Rack hands header strings to Ruby tagged `ASCII-8BIT`, and encrypted
|
|
582
|
+
attributes preserve the original encoding through decryption — so a recorded
|
|
583
|
+
`browser_user_agent` read back from the annex is BINARY-tagged even when its
|
|
584
|
+
bytes are perfectly valid UTF-8. Concatenating it into a UTF-8 view buffer
|
|
585
|
+
raises `Encoding::CompatibilityError`, and because only live captures carry
|
|
586
|
+
an annex, the failure hides until the first real signup reaches an admin
|
|
587
|
+
screen (it took down a production operator panel exactly that way).
|
|
588
|
+
|
|
589
|
+
Normalize at the render boundary, not in storage — the stored bytes are the
|
|
590
|
+
evidence:
|
|
591
|
+
|
|
592
|
+
```ruby
|
|
593
|
+
# clickwrap-doc-test: syntax-only — a host view helper, not gem configuration
|
|
594
|
+
def rendered_recorded_text(value)
|
|
595
|
+
utf8 = value.to_s.dup.force_encoding(Encoding::UTF_8)
|
|
596
|
+
utf8.valid_encoding? ? utf8 : utf8.scrub
|
|
597
|
+
end
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
The gem's own screens never render raw annex values, so they are not exposed;
|
|
601
|
+
any host view that does — an operator panel, an export page — is.
|
|
@@ -132,10 +132,26 @@ module Clickwrap
|
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
def write_string(string, buffer)
|
|
135
|
-
|
|
135
|
+
# `valid_encoding?` is ALWAYS true on an ASCII-8BIT string — BINARY has
|
|
136
|
+
# no invalid byte sequences by definition — and BINARY is exactly what
|
|
137
|
+
# Rack and CDN headers deliver, so this guard was a no-op for the
|
|
138
|
+
# strings most likely to be malformed. Ten request-evidence columns
|
|
139
|
+
# reach it.
|
|
140
|
+
#
|
|
141
|
+
# Normalizing the tag before validating is safe for every digest ever
|
|
142
|
+
# written: bytes that ARE valid UTF-8 canonicalize byte-identically
|
|
143
|
+
# whether they arrive tagged BINARY or UTF-8 (measured, and pinned by a
|
|
144
|
+
# test below). What changes is only the case that was broken —
|
|
145
|
+
# genuinely invalid bytes used to emit canonical JSON that was itself
|
|
146
|
+
# not valid UTF-8, which RFC 8785 forbids and a verifier in another
|
|
147
|
+
# language may reject or normalize into a different digest. That is the
|
|
148
|
+
# "still verifiable years later" promise failing silently, so it is
|
|
149
|
+
# refused at write time instead.
|
|
150
|
+
utf8 = string.encoding == Encoding::UTF_8 ? string : string.dup.force_encoding(Encoding::UTF_8)
|
|
151
|
+
raise SerializationError, "Canonical JSON strings must be valid UTF-8" unless utf8.valid_encoding?
|
|
136
152
|
|
|
137
153
|
buffer << '"'
|
|
138
|
-
|
|
154
|
+
utf8.each_char do |char|
|
|
139
155
|
escape = ESCAPES[char]
|
|
140
156
|
buffer << if escape
|
|
141
157
|
escape
|
|
@@ -307,16 +307,29 @@ module Clickwrap
|
|
|
307
307
|
@describe_authentication_with = ensure_callable(value, "describe_authentication_with")
|
|
308
308
|
end
|
|
309
309
|
|
|
310
|
-
# The constantized actor class, resolved lazily on
|
|
311
|
-
# purpose: the initializer that sets `config.actor_class_name = "User"`
|
|
312
|
-
# before the User model is necessarily loaded.
|
|
310
|
+
# The constantized actor class, resolved lazily on EVERY use. Lazy on
|
|
311
|
+
# purpose: the initializer that sets `config.actor_class_name = "User"`
|
|
312
|
+
# runs before the User model is necessarily loaded.
|
|
313
|
+
#
|
|
314
|
+
# Not memoized, deliberately. This used to cache the resolved Class object
|
|
315
|
+
# and re-resolve only when the class NAME changed — which is never, since
|
|
316
|
+
# the name is set once in an initializer. In a Rails app with reloading,
|
|
317
|
+
# the class behind that name is replaced on every code change: Zeitwerk
|
|
318
|
+
# unloads the old User and defines a new one, same name, different object.
|
|
319
|
+
# The cache then held a class no living record was an instance of, and
|
|
320
|
+
# `actor.is_a?(config.actor_class)` failed for a perfectly ordinary User.
|
|
321
|
+
#
|
|
322
|
+
# What that looked like from the outside was worse than the bug: the
|
|
323
|
+
# ConfigurationError names the offending class and the configured class,
|
|
324
|
+
# and both printed "User". The message read as a contradiction, sent
|
|
325
|
+
# people to an initializer that was correct, and came back after every
|
|
326
|
+
# edit — the first signup of a dev session worked and the second did not.
|
|
327
|
+
#
|
|
328
|
+
# `constantize` after the first load is a const_get, and this is called
|
|
329
|
+
# once per capture rather than in a loop, so there was nothing to save.
|
|
330
|
+
# `parent_controller_class` below has always resolved this way.
|
|
313
331
|
def actor_class
|
|
314
|
-
|
|
315
|
-
if @actor_class.nil? || @actor_class_name_at_resolution != name
|
|
316
|
-
@actor_class = name.constantize
|
|
317
|
-
@actor_class_name_at_resolution = name
|
|
318
|
-
end
|
|
319
|
-
@actor_class
|
|
332
|
+
actor_class_name.constantize
|
|
320
333
|
end
|
|
321
334
|
|
|
322
335
|
def parent_controller_class = parent_controller_class_name.constantize
|
|
@@ -257,9 +257,21 @@ module Clickwrap
|
|
|
257
257
|
# and nothing finer, because that is what "IP geolocation" means in this
|
|
258
258
|
# gem when nobody narrows it. Naming even one field means you are
|
|
259
259
|
# choosing the set yourself, and then the set is exactly what you named.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
260
|
+
# Distinguishes "the policy never mentioned this field" from "the policy
|
|
261
|
+
# passed nil for it". With plain `nil` defaults the two are identical, and
|
|
262
|
+
# `record_ip_geolocation(country: settings[:geo])` with an empty setting
|
|
263
|
+
# silently enabled the coarse trio — enabling a category of personal data
|
|
264
|
+
# as a side effect, which is the one thing the frictionless pass never
|
|
265
|
+
# relaxed.
|
|
266
|
+
UNMENTIONED_FIELD = Object.new.freeze
|
|
267
|
+
private_constant :UNMENTIONED_FIELD
|
|
268
|
+
|
|
269
|
+
def record_ip_geolocation(country: UNMENTIONED_FIELD, region: UNMENTIONED_FIELD,
|
|
270
|
+
city: UNMENTIONED_FIELD, postal_code: UNMENTIONED_FIELD,
|
|
271
|
+
latitude_and_longitude: UNMENTIONED_FIELD,
|
|
272
|
+
timezone: UNMENTIONED_FIELD, continent: UNMENTIONED_FIELD,
|
|
273
|
+
metro_code: UNMENTIONED_FIELD,
|
|
274
|
+
accuracy_radius_in_kilometers: UNMENTIONED_FIELD,
|
|
263
275
|
using: nil, encrypted: nil, delete_after: nil, retain_until: nil,
|
|
264
276
|
fail_if_unavailable: false, because: nil,
|
|
265
277
|
legal_basis_reference: nil,
|
|
@@ -415,14 +427,28 @@ module Clickwrap
|
|
|
415
427
|
end
|
|
416
428
|
end
|
|
417
429
|
|
|
418
|
-
#
|
|
419
|
-
# policy named it and turned it off".
|
|
420
|
-
#
|
|
421
|
-
#
|
|
422
|
-
# still reaches the coherence check that tells it to say
|
|
430
|
+
# An UNMENTIONED field means "the policy did not mention this"; `false`
|
|
431
|
+
# means "the policy named it and turned it off". A policy that mentions
|
|
432
|
+
# nothing gets the coarse trio; a policy that explicitly sets every field
|
|
433
|
+
# to false still reaches the coherence check that tells it to say
|
|
423
434
|
# `do_not_record_ip_geolocation` instead.
|
|
435
|
+
#
|
|
436
|
+
# An explicit `nil` is neither, and is refused rather than guessed: it is
|
|
437
|
+
# almost always a variable that came out empty, and treating it as "the
|
|
438
|
+
# policy said nothing" would turn a missing setting into three enabled
|
|
439
|
+
# fields of personal data.
|
|
424
440
|
def default_ip_geolocation_fields_when_none_named(named)
|
|
425
|
-
|
|
441
|
+
ambiguous = named.select { |_, value| value.nil? }.keys
|
|
442
|
+
unless ambiguous.empty?
|
|
443
|
+
raise DefinitionError,
|
|
444
|
+
"Policy #{@key} passes nil for #{ambiguous.join(", ")} in " \
|
|
445
|
+
"`record_ip_geolocation`. Say `true` or `false` for each field you name, " \
|
|
446
|
+
"or leave it out entirely — Clickwrap will not read an empty value as " \
|
|
447
|
+
"permission to record it, and it will not read it as silence either."
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
mentioned = named.reject { |_, value| value.equal?(UNMENTIONED_FIELD) }
|
|
451
|
+
return named.transform_values { |value| value == true } if mentioned.any?
|
|
426
452
|
|
|
427
453
|
Vocabulary::IP_GEOLOCATION_DATA_FIELDS.to_h do |field|
|
|
428
454
|
[field, Vocabulary::COARSE_IP_GEOLOCATION_DATA_FIELDS.include?(field)]
|
|
@@ -302,11 +302,18 @@ module Clickwrap
|
|
|
302
302
|
key = Clickwrap.config.request_evidence_binding_key_for(key_id)
|
|
303
303
|
return false if key.nil?
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
305
|
+
# A stored value that can no longer be canonicalized cannot reproduce its
|
|
306
|
+
# binding, and saying so is the honest answer — an integrity check that
|
|
307
|
+
# raises tells an operator nothing except that the tool broke.
|
|
308
|
+
begin
|
|
309
|
+
computed = Digest.keyed_digest(
|
|
310
|
+
CanonicalJson.generate(binding_body_for(category)),
|
|
311
|
+
key: key,
|
|
312
|
+
algorithm: digest_algorithm
|
|
313
|
+
)
|
|
314
|
+
rescue CanonicalJson::SerializationError
|
|
315
|
+
return false
|
|
316
|
+
end
|
|
310
317
|
Digest.secure_compare?(computed, digest)
|
|
311
318
|
end
|
|
312
319
|
|
|
@@ -229,6 +229,20 @@ module Clickwrap
|
|
|
229
229
|
"purpose."
|
|
230
230
|
end
|
|
231
231
|
|
|
232
|
+
# The legal-basis reference goes into the compiled policy revision and
|
|
233
|
+
# every receipt built from it, permanently. Scaffolding there is worse
|
|
234
|
+
# than an omission: an omission reads as "the host said nothing", while
|
|
235
|
+
# "TODO: ask legal" reads as a reviewed determination to anyone who finds
|
|
236
|
+
# it later. Same rule as `because:` — the option is optional, but text
|
|
237
|
+
# the host actually wrote has to be text they meant.
|
|
238
|
+
if ReviewedText.placeholder?(setting.legal_basis_reference)
|
|
239
|
+
raise DefinitionError,
|
|
240
|
+
"Policy #{policy_key} records #{category} with a `legal_basis_reference:` " \
|
|
241
|
+
"that is still scaffolding text (#{setting.legal_basis_reference.inspect}). " \
|
|
242
|
+
"Replace it with the application's own reference, or drop the option — " \
|
|
243
|
+
"Clickwrap would rather record nothing than record a TODO as a legal basis."
|
|
244
|
+
end
|
|
245
|
+
|
|
232
246
|
return unless setting.delete_after && setting.delete_after.to_i <= 0
|
|
233
247
|
|
|
234
248
|
raise DefinitionError,
|
data/lib/clickwrap/version.rb
CHANGED
|
@@ -980,15 +980,18 @@ module Clickwrap
|
|
|
980
980
|
|
|
981
981
|
def ask_purpose(label)
|
|
982
982
|
say "\n Why does the application need #{label}? One plain sentence, in your own"
|
|
983
|
-
say " words — it goes into the initializer and the privacy inventory.
|
|
984
|
-
say "
|
|
983
|
+
say " words — it goes into the initializer and the privacy inventory. Leave it"
|
|
984
|
+
say " blank and Clickwrap records its own stated purpose instead, marked as the"
|
|
985
|
+
say " gem's; a scaffolding answer (TODO, FIXME) stops generation before any"
|
|
986
|
+
say " files are written."
|
|
985
987
|
ask(" Purpose:").to_s.strip
|
|
986
988
|
end
|
|
987
989
|
|
|
988
990
|
def ask_retention_days(label)
|
|
989
991
|
say "\n After how many days should Clickwrap delete #{label}?"
|
|
990
|
-
say "
|
|
991
|
-
say "
|
|
992
|
+
say " Enter the number of days your application reviewed, or leave it blank and"
|
|
993
|
+
say " #{label} keeps pace with the evidence it corroborates — kept until a"
|
|
994
|
+
say " reviewed disposition removes it. A negative number stops generation."
|
|
992
995
|
ask(" Days:").to_s.strip.to_i
|
|
993
996
|
end
|
|
994
997
|
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clickwrap
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-08-
|
|
10
|
+
date: 2026-08-31 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: actionpack
|