clickwrap 0.2.1 → 0.3.1

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.
@@ -125,7 +125,7 @@ module Clickwrap
125
125
  "browser_user_agent" => default_category(config, :browser_user_agent),
126
126
  "ip_geolocation" => default_category(config, :ip_geolocation).merge(
127
127
  "fields" => config.enabled_default_ip_geolocation_fields,
128
- "resolver" => describe_resolver(config.ip_geolocation_resolver),
128
+ "resolver" => describe_resolver(config),
129
129
  "fail_capture_when_unavailable" => config.fail_capture_when_ip_geolocation_is_unavailable
130
130
  ),
131
131
  "review_default_request_evidence_configuration_on" =>
@@ -138,14 +138,25 @@ module Clickwrap
138
138
  }
139
139
  end
140
140
 
141
+ # `because` is the purpose that will actually be recorded, and
142
+ # `purpose_source` says who wrote it. A `"gem_default"` purpose is a real
143
+ # purpose and it is stored with the evidence — but nobody should mistake
144
+ # Clickwrap's own sentence for one the host's team reviewed, so the
145
+ # inventory never lets the two look alike.
141
146
  def default_category(config, category)
147
+ recorded = default_recorded?(config, category)
148
+
142
149
  {
143
- "recorded_by_default" => default_recorded?(config, category),
144
- "because" => config.public_send(:"reason_for_recording_#{plural_for(category)}_by_default"),
150
+ "recorded_by_default" => recorded,
151
+ "because" => recorded ? config.reason_for_recording_by_default(category) : nil,
152
+ "purpose_source" => (config.reason_for_recording_by_default_source(category) if recorded),
145
153
  "legal_basis_reference" =>
146
154
  config.public_send(:"legal_basis_reference_for_recording_#{plural_for(category)}_by_default"),
147
155
  "encrypted" => config.public_send(:"encrypt_recorded_#{plural_for(category)}"),
148
- "delete_after_seconds" => config.public_send(:"delete_recorded_#{plural_for(category)}_after")&.to_i
156
+ "delete_after_seconds" => config.public_send(:"delete_recorded_#{plural_for(category)}_after")&.to_i,
157
+ "kept_indefinitely" => config.keeps_recorded_request_evidence_indefinitely?(category),
158
+ "reason_for_keeping_indefinitely" =>
159
+ config.reason_for_keeping_recorded_request_evidence_indefinitely(category)
149
160
  }
150
161
  end
151
162
 
@@ -186,6 +197,7 @@ module Clickwrap
186
197
  entry = {
187
198
  "recorded" => setting.record?,
188
199
  "because" => setting.because,
200
+ "purpose_source" => policy.request_evidence.purpose_source_for(category),
189
201
  "legal_basis_reference" => setting.legal_basis_reference,
190
202
  "data_protection_impact_assessment_reference" =>
191
203
  setting.data_protection_impact_assessment_reference,
@@ -201,7 +213,7 @@ module Clickwrap
201
213
  entry.merge(
202
214
  "fields" => policy.request_evidence.enabled_ip_geolocation_fields,
203
215
  "resolver_named_by_policy" => policy.request_evidence.ip_geolocation_resolver_name&.to_s,
204
- "resolver" => describe_resolver(Clickwrap.config.ip_geolocation_resolver)
216
+ "resolver" => describe_resolver(Clickwrap.config)
205
217
  )
206
218
  end
207
219
 
@@ -283,10 +295,19 @@ module Clickwrap
283
295
  }
284
296
  end
285
297
 
286
- def describe_resolver(resolver)
298
+ # Reports the resolver in force, and whether the host named it or
299
+ # Clickwrap adopted the official trackdown adapter because their bundle
300
+ # already carried it. Asking never causes that adoption to happen — an
301
+ # inventory describes a configuration, it does not make one.
302
+ def describe_resolver(config)
303
+ resolver = config.ip_geolocation_resolver_in_force
287
304
  return { "configured" => false } if resolver.nil?
288
305
 
289
- { "configured" => true, "class" => resolver.class.name }
306
+ {
307
+ "configured" => true,
308
+ "class" => resolver.class.name,
309
+ "source" => config.ip_geolocation_resolver_was_adopted_automatically? ? "gem_default" : "host"
310
+ }
290
311
  end
291
312
 
292
313
  def plural_for(category)
@@ -244,7 +244,10 @@ module Clickwrap
244
244
 
245
245
  # Provenance is recorded even when the value is not, because "which reader
246
246
  # was asked, under which reviewed proxy configuration" is what tells a
247
- # later reader how much the address is worth.
247
+ # later reader how much the address is worth. A nil digest is part of
248
+ # that answer rather than a hole in it: it says no reviewed proxy
249
+ # configuration was recorded when this address was observed, which is
250
+ # exactly what a reader should know about it.
248
251
  provenance = {
249
252
  ip_address_reader_name: ip_address_reader_name,
250
253
  trusted_proxy_configuration_digest: policy.trusted_proxy_configuration_digest
@@ -484,13 +487,17 @@ module Clickwrap
484
487
  # --- Retention ------------------------------------------------------------
485
488
 
486
489
  # Every recorded field leaves here with a disposal answer: a date, the name
487
- # of a host rule that will produce one, or the explicit reviewed decision
488
- # to keep it as long as the evidence it corroborates. Keeping forever is
489
- # never a silent default it is a named declaration in the retention class
490
- # (`keep_recorded_..._indefinitely`) or the initializer
491
- # (`keep_recorded_..._indefinitely!(because: "…")`) and a recorded field
492
- # with no answer at all is a configuration bug caught before the row is
493
- # written rather than a row nobody ever decided about.
490
+ # of a host rule that will produce one, or no schedule at all — which means
491
+ # it keeps pace with the evidence it corroborates, exactly like an
492
+ # indefinite core event. A blank schedule is the answer, not a gap: the
493
+ # planner never lists these rows, and the receipt reports them as recorded
494
+ # rather than as anything pending.
495
+ #
496
+ # Until 0.3.0 this raised when nothing had named a schedule, on the theory
497
+ # that keeping forever must never be silent. The theory held; the cost did
498
+ # not. It refused captures for hosts who had simply not written a sentence,
499
+ # and pushed integrators toward recording nothing — which is worse evidence
500
+ # than evidence kept under the same posture as the agreement it belongs to.
494
501
  #
495
502
  # `retain_until` names a host calculation instead of a duration because real
496
503
  # record-keeping schedules are not always durations — "five years, or three
@@ -504,15 +511,7 @@ module Clickwrap
504
511
  return { "#{category}_delete_after": now + class_rule.duration } if class_rule&.duration?
505
512
  return { "#{category}_retain_until_rule": class_rule.host_event_name.to_s } if class_rule&.host_event?
506
513
 
507
- # Indefinite — declared in the class or application-wide — stamps
508
- # nothing: the blank schedule plus the recorded declaration IS the
509
- # disposal answer, exactly like an indefinite core event.
510
- if class_rule&.indefinite? ||
511
- Clickwrap.config.keeps_recorded_request_evidence_indefinitely?(category)
512
- return {}
513
- end
514
-
515
- raise ConfigurationError, missing_retention_message(category)
514
+ {}
516
515
  end
517
516
 
518
517
  def retention_class_rule_for(category)
@@ -521,16 +520,6 @@ module Clickwrap
521
520
  Clickwrap.retention_class!(policy.retention_class_key).rule_for(category)
522
521
  end
523
522
 
524
- def missing_retention_message(category)
525
- "Clickwrap is about to record #{category} for policy #{policy_key} and nothing says what " \
526
- "should ever happen to it. Give the policy a rule — `delete_after:` with a reviewed " \
527
- "period, or `retain_until:` naming a host retention calculation — add a #{category} " \
528
- "rule (or `keep_recorded_#{category}_indefinitely`) to retention class " \
529
- "#{policy.retention_class_key.inspect}, or answer it application-wide with " \
530
- "`keep_recorded_..._indefinitely!(because: \"…\")`. Keeping forever is never silent, " \
531
- "and Clickwrap will not choose for you."
532
- end
533
-
534
523
  # --- Failing closed -------------------------------------------------------
535
524
 
536
525
  # A policy can decide that evidence it cannot get is worse than no capture
@@ -8,9 +8,15 @@ module Clickwrap
8
8
  # not squeamishness about useful data: it is that high-quality evidence is
9
9
  # purpose-specific. An IP address is personal data, and keeping it on your own
10
10
  # infrastructure does not remove the duty to have a reason for it, protect it,
11
- # and stop keeping it eventually. So each field is enabled by name, with a
12
- # plain-English purpose and a retention decision attached, and the policy that
13
- # enables it is the server's, never the browser's.
11
+ # and stop keeping it eventually. So each field is enabled by name, and the
12
+ # policy that enables it is the server's, never the browser's.
13
+ #
14
+ # Naming a field is the whole requirement. Every recorded category still
15
+ # leaves here carrying a purpose and a disposal posture, because a snapshot
16
+ # read years from now has to answer both questions — but since 0.3.0 those
17
+ # answers have honest gem-supplied defaults instead of being the entry fee. A
18
+ # host who writes their own keeps their own words, and the privacy inventory
19
+ # reports which of the two is looking back at you.
14
20
  #
15
21
  # None of these fields is identity or physical location. An IP address is a
16
22
  # network observation. IP geolocation is a provider's estimate about that
@@ -61,9 +67,11 @@ module Clickwrap
61
67
  trusted_proxy_configuration_digest: nil, review_configuration_on: nil)
62
68
  @policy_key = policy_key
63
69
  @retention_class_key = retention_class_key&.to_s
70
+ @purpose_sources = {}
64
71
  @ip_address = normalized_setting(:ip_address, ip_address || NOT_RECORDED)
65
72
  @browser_user_agent = normalized_setting(:browser_user_agent, browser_user_agent || NOT_RECORDED)
66
73
  @ip_geolocation = normalized_setting(:ip_geolocation, ip_geolocation || NOT_RECORDED)
74
+ @purpose_sources.freeze
67
75
  @ip_geolocation_fields = normalize_geolocation_fields(ip_geolocation_fields)
68
76
  @ip_geolocation_resolver_name = ip_geolocation_resolver_name&.to_sym
69
77
  @trusted_proxy_configuration_digest = trusted_proxy_configuration_digest&.to_s
@@ -87,6 +95,15 @@ module Clickwrap
87
95
 
88
96
  def records_anything? = records_ip_address? || records_browser_user_agent? || records_ip_geolocation?
89
97
 
98
+ # Who wrote the purpose stored for this category: `"host"` when the policy
99
+ # or the initializer supplied one, `"gem_default"` when Clickwrap filled in
100
+ # its own. Deliberately kept off `to_snapshot`: the snapshot is a released
101
+ # evidence format, and the answer is derivable from the configuration a
102
+ # reader already has.
103
+ def purpose_source_for(category)
104
+ @purpose_sources[category.to_s]
105
+ end
106
+
90
107
  def setting_for(category)
91
108
  case category.to_sym
92
109
  when :ip_address then ip_address
@@ -132,7 +149,6 @@ module Clickwrap
132
149
  FIELD_CATEGORIES.each { |category| validate_category!(category) }
133
150
  validate_geolocation_coherence!
134
151
  validate_named_resolver!
135
- validate_trusted_proxy_configuration_digest!
136
152
  end
137
153
 
138
154
  def normalized_setting(category, setting)
@@ -153,7 +169,16 @@ module Clickwrap
153
169
  "`config.encrypt_recorded_*` setting, or omit `encrypted:` to inherit it."
154
170
  end
155
171
 
156
- Setting.new(**setting.to_h, encrypted: configured)
172
+ # The compiled revision always carries a purpose, so a reader years from
173
+ # now never finds a recorded field with nothing beside it saying what it
174
+ # was for. Whose sentence it is gets remembered separately.
175
+ @purpose_sources[category.to_s] = setting.because.presence ? "host" : "gem_default"
176
+
177
+ Setting.new(
178
+ **setting.to_h,
179
+ encrypted: configured,
180
+ because: setting.because.presence || Vocabulary::DEFAULT_REQUEST_EVIDENCE_PURPOSE
181
+ )
157
182
  end
158
183
 
159
184
  def validate_named_resolver!
@@ -164,8 +189,9 @@ module Clickwrap
164
189
  raise DefinitionError,
165
190
  "Policy #{policy_key} records IP geolocation using resolver " \
166
191
  "#{ip_geolocation_resolver_name.inspect}, but no resolver is registered under " \
167
- "that name. Configure `config.ip_geolocation_resolver` for " \
168
- "`:application_default`, or register the named resolver with " \
192
+ "that name. Bundle `trackdown` (>= 0.4) and Clickwrap uses it for " \
193
+ "`:application_default` with no wiring line at all, or set " \
194
+ "`config.ip_geolocation_resolver` yourself, or register the named resolver with " \
169
195
  "`config.register_ip_geolocation_resolver`. Registered resolvers: " \
170
196
  "#{Clickwrap.config.ip_geolocation_resolver_names.join(", ").presence || "(none)"}."
171
197
  end
@@ -186,34 +212,21 @@ module Clickwrap
186
212
  "capabilities: #{error.message}"
187
213
  end
188
214
 
215
+ # A missing purpose and a missing disposal answer both have defaults now.
216
+ # What is left refuses only the two things a default cannot honestly stand
217
+ # in for: scaffolding text the host actually wrote, and a deletion clock
218
+ # that is not a period.
189
219
  def validate_category!(category)
190
220
  setting = setting_for(category)
191
221
  return unless setting.record?
192
222
 
193
- if setting.because.to_s.strip.empty?
194
- raise DefinitionError,
195
- "Policy #{policy_key} records #{category} but gives no `because:`. Say in one " \
196
- "plain sentence why this policy needs it right now. \"We might need it someday\" " \
197
- "is not a purpose, and a privacy notice mentioning the field is not one either."
198
- end
199
-
200
223
  if ReviewedText.placeholder?(setting.because)
201
224
  raise DefinitionError,
202
225
  "Policy #{policy_key} records #{category}, but its `because:` is still " \
203
226
  "scaffolding text (#{setting.because.inspect}). Replace it with the " \
204
- "application's reviewed, present-tense reason; Clickwrap never treats a TODO " \
205
- "as a data-collection purpose."
206
- end
207
-
208
- if setting.delete_after.nil? && setting.retain_until.nil? && retention_class_key.nil? &&
209
- !Clickwrap.config.keeps_recorded_request_evidence_indefinitely?(category)
210
- raise DefinitionError,
211
- "Policy #{policy_key} records #{category} but nothing says what should ever " \
212
- "happen to it. Give it `delete_after:` with a duration, or `retain_until:` " \
213
- "naming a host event rule, attach a retention class with a rule for this " \
214
- "category, or answer it application-wide with " \
215
- "`keep_recorded_..._indefinitely!(because: \"…\")`. Keeping forever is never " \
216
- "silent, and Clickwrap will not choose for you."
227
+ "application's reviewed, present-tense reason, or drop the option and let " \
228
+ "Clickwrap record its own stated purpose; a TODO is never a data-collection " \
229
+ "purpose."
217
230
  end
218
231
 
219
232
  return unless setting.delete_after && setting.delete_after.to_i <= 0
@@ -228,8 +241,11 @@ module Clickwrap
228
241
 
229
242
  if ip_geolocation.record? && enabled.empty?
230
243
  raise DefinitionError,
231
- "Policy #{policy_key} calls `record_ip_geolocation` but enables no field. " \
232
- "Name the fields you actually need, for example `country: true`."
244
+ "Policy #{policy_key} calls `record_ip_geolocation` and then turns every field " \
245
+ "off, which cannot mean anything. Name the fields you want, for example " \
246
+ "`country: true`; call `record_ip_geolocation` with no fields at all for the " \
247
+ "coarse country, region, and city; or say `do_not_record_ip_geolocation` if that " \
248
+ "is what you meant."
233
249
  end
234
250
 
235
251
  if ip_geolocation.record? &&
@@ -248,17 +264,5 @@ module Clickwrap
248
264
  "Policy #{policy_key} enables the IP-geolocation fields #{enabled.join(", ")} " \
249
265
  "without recording IP geolocation."
250
266
  end
251
-
252
- def validate_trusted_proxy_configuration_digest!
253
- return unless records_ip_address? || records_ip_geolocation?
254
- return unless trusted_proxy_configuration_digest.to_s.strip.empty?
255
-
256
- raise DefinitionError,
257
- "Policy #{policy_key} records an IP address or derives IP geolocation, but " \
258
- "`config.trusted_proxy_configuration_digest` is blank. Review the deployment's " \
259
- "trusted-proxy topology, digest that reviewed configuration, and set the prefixed " \
260
- "digest (for example `sha256:...`). This records which proxy decision produced the " \
261
- "address; it does not claim that the decision was correct."
262
- end
263
267
  end
264
268
  end
@@ -15,7 +15,6 @@ module Clickwrap
15
15
  Clickwrap.policies.each do |policy|
16
16
  validate_documents!(policy)
17
17
  retention_class = validate_retention_class!(policy)
18
- validate_request_evidence_retention!(policy, retention_class)
19
18
  validate_host_calculations!(policy, retention_class)
20
19
  validate_authority_adapter!(policy)
21
20
  validate_ip_geolocation_resolver!(policy)
@@ -105,37 +104,13 @@ module Clickwrap
105
104
  )
106
105
  end
107
106
 
108
- def validate_request_evidence_retention!(policy, retention_class)
109
- RequestEvidencePolicy::FIELD_CATEGORIES.each do |category|
110
- setting = policy.request_evidence.setting_for(category)
111
- next unless setting.record?
112
- next if setting.delete_after || setting.retain_until || retention_class.rule_for(category)
113
- # The application-wide answer counts too: recording enabled in the
114
- # initializer carries its disposal decision in the same place —
115
- # either a global clock or the explicit, reasoned keep-indefinitely.
116
- next if config_answers_disposal_for?(category)
117
-
118
- raise DefinitionError,
119
- "Policy #{policy.key} records #{category}, but nothing says when to dispose of " \
120
- "it. Add `delete_after:`/`retain_until:` to the policy, a plain-English " \
121
- "request-evidence rule (or `keep_recorded_#{category}_indefinitely`) to " \
122
- "retention class #{retention_class.key}, or answer it application-wide in the " \
123
- "initializer with `delete_recorded_..._after` or " \
124
- "`keep_recorded_..._indefinitely!(because: \"…\")`."
125
- end
126
- end
127
-
128
- def config_answers_disposal_for?(category)
129
- clock =
130
- case category.to_sym
131
- when :ip_address then Clickwrap.config.delete_recorded_ip_addresses_after
132
- when :browser_user_agent then Clickwrap.config.delete_recorded_browser_user_agents_after
133
- else Clickwrap.config.delete_recorded_ip_geolocation_after
134
- end
135
-
136
- clock.present? || Clickwrap.config.keeps_recorded_request_evidence_indefinitely?(category)
137
- end
138
-
107
+ # There is deliberately no disposal check here any more. A recorded
108
+ # category with no clock anywhere — not on the policy, not in its
109
+ # retention class, not in the initializer — means it keeps pace with the
110
+ # evidence it corroborates, which is what the core event does by default
111
+ # too. What still fails below is a rule that names a calculation nobody
112
+ # registered: a deadline waiting on a typo never arrives, and that is a
113
+ # different thing from a deliberate absence of one.
139
114
  def validate_host_calculations!(policy, retention_class)
140
115
  referenced = retention_class.rules.values.filter_map do |rule|
141
116
  rule.host_event_name&.to_sym
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clickwrap
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.1"
5
5
 
6
6
  # The canonical schema version for receipts, event digests, and presentation
7
7
  # manifests. This is deliberately independent of VERSION: gem releases may
@@ -160,6 +160,42 @@ module Clickwrap
160
160
  accuracy_radius_in_kilometers
161
161
  ].freeze
162
162
 
163
+ # The three coarse fields `config.record_request_evidence_by_default = true`
164
+ # turns on. Coarse means administrative area, not a point: a country, a
165
+ # region, and a city are what a provider can estimate from an address with
166
+ # any confidence at all. Everything finer — a postal code, coordinates, a
167
+ # timezone, a metro code — stays its own separately named decision, because
168
+ # a switch that reads "record request evidence" should not hand somebody
169
+ # coordinates they never asked for.
170
+ COARSE_IP_GEOLOCATION_DATA_FIELDS = %w[country region city].freeze
171
+
172
+ # The purpose Clickwrap records when a host enables request evidence
173
+ # without writing a purpose of their own. It is the gem's own sentence, not
174
+ # a reviewed host decision, and the privacy inventory says which of the two
175
+ # it is looking at. It exists because the alternative — refusing to boot
176
+ # until somebody writes a sentence — was pushing integrators to record
177
+ # nothing at all, and no corroboration is worse evidence than corroboration
178
+ # collected under the gem's stated purpose.
179
+ DEFAULT_REQUEST_EVIDENCE_PURPOSE =
180
+ "Corroborate who performed each recorded act, from where, on what client — to defend " \
181
+ "the recorded agreement itself."
182
+
183
+ # The reason recorded when a host keeps request evidence indefinitely
184
+ # without writing their own. Same posture as the purpose above: the
185
+ # declaration is still recorded and still readable years later; only the
186
+ # obligation to phrase it yourself is gone.
187
+ DEFAULT_REASON_FOR_KEEPING_REQUEST_EVIDENCE_INDEFINITELY =
188
+ "Corroboration lives as long as the evidence it corroborates"
189
+
190
+ # The reason recorded when a host turns off encryption for request evidence
191
+ # without writing their own. Calling the method is still the ceremony —
192
+ # `deliberately_store_request_evidence_unencrypted!` is a sentence a
193
+ # reviewer finds in a diff and cannot misread — but the gem no longer
194
+ # demands the sentence be phrased twice.
195
+ DEFAULT_REASON_FOR_STORING_REQUEST_EVIDENCE_UNENCRYPTED =
196
+ "The application deliberately stores request evidence unencrypted; the reason lives " \
197
+ "outside Clickwrap"
198
+
163
199
  # Provenance that travels with any stored IP-geolocation result. A policy
164
200
  # cannot keep provider-derived coordinates while stripping the uncertainty
165
201
  # needed to interpret them.
@@ -28,9 +28,13 @@ module Clickwrap
28
28
  # spelling of the off-by-default posture. It disappears after generation;
29
29
  # it is deliberately not a runtime concept. There is no
30
30
  # `record_network_context`, `record_everything`, `full_evidence`, or
31
- # regulation-named mode switch anywhere in this gem, because a flag that
32
- # enables a whole category of personal data is the thing this gem exists not
33
- # to do, and no flag can make a legal determination on a host's behalf.
31
+ # regulation-named mode switch anywhere in this gem, because a flag whose
32
+ # name hides what it collects is the thing this gem exists not to ship, and
33
+ # no flag can make a legal determination on a host's behalf. The runtime's
34
+ # one grouped switch, `config.record_request_evidence_by_default`, is not
35
+ # one of those: it names its three fields and can never grow a fourth. The
36
+ # installer still asks about them one at a time, because a purpose the host
37
+ # wrote beats the honest default the gem would otherwise record.
34
38
  class InstallGenerator < Rails::Generators::Base
35
39
  include ActiveRecord::Generators::Migration
36
40
 
@@ -1156,23 +1160,30 @@ module Clickwrap
1156
1160
  validate_ip_geolocation_resolver_class_name!
1157
1161
  end
1158
1162
 
1163
+ # A missing purpose and a missing deletion period are both fine, and the
1164
+ # generated file simply omits those lines: the gem records its own stated
1165
+ # purpose and keeps the field as long as the evidence it corroborates.
1166
+ # Scaffolding text is the one thing still refused, because a `TODO` the
1167
+ # installer writes into a shipped initializer is worse than no line at
1168
+ # all — the gem would reject it at boot anyway.
1159
1169
  def validate_enabled_category!(label, enabled:, because:, delete_after_days:,
1160
1170
  reason_option:, retention_option:)
1161
1171
  return unless enabled
1162
1172
 
1163
- unless Clickwrap::ReviewedText.present_and_reviewed?(because)
1173
+ if Clickwrap::ReviewedText.placeholder?(because)
1164
1174
  raise Thor::Error,
1165
- "Clickwrap cannot enable #{label} with a blank or scaffolding reason. " \
1166
- "Give the application's reviewed, present-tense reason with " \
1167
- "#{reason_option}=\"...\", or turn that category off. No files were written."
1175
+ "Clickwrap cannot enable #{label} with a scaffolding reason " \
1176
+ "(#{because.inspect}). Give the application's reviewed, present-tense reason " \
1177
+ "with #{reason_option}=\"...\", or omit it entirely and let Clickwrap record " \
1178
+ "its own stated purpose. No files were written."
1168
1179
  end
1169
1180
 
1170
- return if delete_after_days.positive?
1181
+ return unless delete_after_days.negative?
1171
1182
 
1172
1183
  raise Thor::Error,
1173
- "Clickwrap cannot enable #{label} without a positive deletion period. " \
1174
- "Set #{retention_option}=DAYS to the period your application reviewed, or " \
1175
- "turn that category off. No files were written."
1184
+ "#{retention_option} cannot be negative (got #{delete_after_days}). Give the " \
1185
+ "number of days your application reviewed, or omit it and #{label} will keep " \
1186
+ "pace with the evidence it corroborates. No files were written."
1176
1187
  end
1177
1188
 
1178
1189
  def validate_ip_geolocation_coordinates!
@@ -28,8 +28,11 @@
28
28
  #
29
29
  # There is deliberately no `maximum_evidence`, `full_evidence`, `legal_proof`,
30
30
  # or regulation-named mode setting. No runtime flag can make a legal
31
- # determination for you, and an option that turns on a category of personal data
32
- # as a side effect of something else is exactly what this gem exists not to do.
31
+ # determination for you, and an option whose name does not say what it collects
32
+ # is exactly what this gem exists not to ship. The one grouped switch that does
33
+ # exist, `config.record_request_evidence_by_default`, names its own contents:
34
+ # an IP address, a browser user agent, and a coarse country/region/city
35
+ # estimate — nothing finer, ever.
33
36
  <%- if recipe -%>
34
37
  #
35
38
  # This file was scaffolded with `--request-evidence-recipe=<%= recipe %>`. The
@@ -251,10 +254,12 @@ Clickwrap.configure do |config|
251
254
  # proof that anyone was physically anywhere.
252
255
  <%- if records_any_request_evidence? -%>
253
256
  #
254
- # You enabled some of these during install. The generator required an explicit
255
- # purpose and deletion period before writing this file; review both below. The
256
- # annex table itself came with `--with-request-evidence`, which this install
257
- # added for you.
257
+ # You enabled some of these during install, and the installer asked for an
258
+ # explicit purpose and deletion period before writing this file; review both
259
+ # below. The gem itself would have accepted the fields without them and
260
+ # recorded its own stated purpose instead — the installer asks because a
261
+ # sentence your team wrote is worth more than one the gem supplied. The annex
262
+ # table came with `--with-request-evidence`, which this install added for you.
258
263
  <%- else -%>
259
264
  #
260
265
  # Everything here is off, which is the safe default — and the annex table was
@@ -272,26 +277,35 @@ Clickwrap.configure do |config|
272
277
 
273
278
  # --- Request evidence: why, how long, and how it is protected ---------------
274
279
  #
275
- # An enabled field with a blank purpose or no deletion rule is a configuration
276
- # error at boot, not a warning. "We turned it on and never decided why" is the
277
- # failure mode these settings exist to prevent. There is no keep-forever
278
- # default anywhere in this gem, and deleting a value removes the encrypted
279
- # annex entry and appends a disposition event the historical agreement,
280
- # declaration, or authorization stays intact and verifiable without it.
281
-
282
- <%- if record_ip_addresses? -%>
280
+ # Both of these are optional, and the installer wrote yours because you gave
281
+ # them. A field enabled with no purpose records the one Clickwrap states, and
282
+ # the privacy inventory marks it as the gem's sentence rather than yours; a
283
+ # field with no deletion period keeps pace with the evidence it corroborates.
284
+ # What is refused is a purpose that is still scaffolding text, and a deletion
285
+ # period set alongside `keep_recorded_..._indefinitely!` for the same field.
286
+ # Deleting a value removes the encrypted annex entry and appends a disposition
287
+ # event the historical agreement, declaration, or authorization stays intact
288
+ # and verifiable without it.
289
+
290
+ <%- if record_ip_addresses? && reason_for_recording_ip_addresses.present? -%>
283
291
  config.reason_for_recording_ip_addresses_by_default =
284
292
  <%= reason_for_recording_ip_addresses.inspect %>
293
+ <%- end -%>
294
+ <%- if record_ip_addresses? && delete_recorded_ip_addresses_after_days.positive? -%>
285
295
  config.delete_recorded_ip_addresses_after = <%= delete_recorded_ip_addresses_after_days %>.days
286
296
  <%- end -%>
287
- <%- if record_browser_user_agents? -%>
297
+ <%- if record_browser_user_agents? && reason_for_recording_browser_user_agents.present? -%>
288
298
  config.reason_for_recording_browser_user_agents_by_default =
289
299
  <%= reason_for_recording_browser_user_agents.inspect %>
300
+ <%- end -%>
301
+ <%- if record_browser_user_agents? && delete_recorded_browser_user_agents_after_days.positive? -%>
290
302
  config.delete_recorded_browser_user_agents_after = <%= delete_recorded_browser_user_agents_after_days %>.days
291
303
  <%- end -%>
292
- <%- if any_ip_geolocation_field? -%>
304
+ <%- if any_ip_geolocation_field? && reason_for_recording_ip_geolocation.present? -%>
293
305
  config.reason_for_recording_ip_geolocation_by_default =
294
306
  <%= reason_for_recording_ip_geolocation.inspect %>
307
+ <%- end -%>
308
+ <%- if any_ip_geolocation_field? && delete_recorded_ip_geolocation_after_days.positive? -%>
295
309
  config.delete_recorded_ip_geolocation_after = <%= delete_recorded_ip_geolocation_after_days %>.days
296
310
  <%- end -%>
297
311
  <%- if records_any_request_evidence? -%>
@@ -301,10 +315,10 @@ Clickwrap.configure do |config|
301
315
  config.review_default_request_evidence_configuration_on = <%= review_date_literal %>
302
316
  <%- else -%>
303
317
  # Nothing above is enabled, so there is nothing to give a purpose or a
304
- # deletion period to. Enabling a field is never one line — the gem refuses an
305
- # enabled field with no recorded purpose and no deletion rule — and it wants a
306
- # review date so the decision gets looked at again by a person instead of
307
- # outliving its reason:
318
+ # deletion period to. Turning the ordinary three on later is one line —
319
+ # `config.record_request_evidence_by_default = true` — and these are the
320
+ # settings that upgrade it into a record your own team reviewed, plus a date
321
+ # so the decision gets looked at again instead of outliving its reason:
308
322
  #
309
323
  # config.reason_for_recording_ip_addresses_by_default = "Investigate disputed submissions"
310
324
  # config.delete_recorded_ip_addresses_after = 90.days
@@ -324,8 +338,9 @@ Clickwrap.configure do |config|
324
338
  # Turning one of these off puts the raw value in plain text in your database,
325
339
  # and therefore in every ordinary backup and database dump. It is allowed,
326
340
  # because some applications have a reviewed reason, but it is never a quiet
327
- # one-character change: Clickwrap refuses `false` until you have said why in
328
- # `config.deliberately_store_request_evidence_unencrypted!(because: "…")`.
341
+ # one-character change: Clickwrap refuses `false` until you have written
342
+ # `config.deliberately_store_request_evidence_unencrypted!` above it. Writing
343
+ # that line IS the whole ceremony — its `because:` is optional.
329
344
  #
330
345
  # config.encrypt_recorded_ip_addresses = true
331
346
  # config.encrypt_recorded_browser_user_agents = true
@@ -359,7 +374,9 @@ Clickwrap.configure do |config|
359
374
  Clickwrap.trusted_proxy_configuration_digest_for_rails_application
360
375
  <%- else -%>
361
376
  # A digest of the effective trusted-proxy rules, stored beside any recorded
362
- # address. Nothing here records one, so there is nothing to stamp:
377
+ # address. Nothing here records one, so there is nothing to stamp. Set it if
378
+ # you enable IP recording later: it is not required, but without it every
379
+ # recorded address carries a blank where its collection context should be.
363
380
  #
364
381
  # config.trusted_proxy_configuration_digest =
365
382
  # Clickwrap.trusted_proxy_configuration_digest_for_rails_application
@@ -368,8 +385,9 @@ Clickwrap.configure do |config|
368
385
  <%- if any_ip_geolocation_field? -%>
369
386
  # DECISION — you enabled IP-geolocation fields and explicitly selected this
370
387
  # resolver. Make its gem and data source available in every environment:
371
- # Clickwrap refuses to boot with fields it has nothing to resolve, rather than
372
- # recording blanks that later read as "no result".
388
+ # Clickwrap refuses to boot with fields nothing can resolve no resolver here
389
+ # and no trackdown in the bundle — rather than recording blanks that later
390
+ # read as "no result".
373
391
  #
374
392
  # Only the fields authorized above are stored, never the whole result object,
375
393
  # and every stored value keeps the provider, source, estimated state,
@@ -378,11 +396,18 @@ Clickwrap.configure do |config|
378
396
  # needed to read them.
379
397
  config.ip_geolocation_resolver = <%= ip_geolocation_resolver_class_name %>.new
380
398
  <%- else -%>
381
- # Required before any IP-geolocation field can be enabled, here or in a
382
- # policy. The official optional resolver requires trackdown >= 0.4 so the
383
- # provider that actually answered and per-request source trust are preserved:
399
+ # Something has to resolve an IP-geolocation field before it can be enabled,
400
+ # here or in a policy. Bundling trackdown is enough Clickwrap uses the
401
+ # official adapter for you when you have not named a resolver of your own,
402
+ # and the privacy inventory reports it as the gem's choice rather than yours.
403
+ # It requires trackdown >= 0.4 so the provider that actually answered and
404
+ # per-request source trust are preserved:
384
405
  #
385
406
  # bundle add trackdown --version ">= 0.4"
407
+ #
408
+ # Name it explicitly to pick a different provider, or when you are wiring
409
+ # Trackdown's per-request CDN trust:
410
+ #
386
411
  # config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
387
412
  <%- end -%>
388
413
 
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.2.1
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-08-19 00:00:00.000000000 Z
10
+ date: 2026-08-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: actionpack