clickwrap 0.2.1 → 0.3.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.
@@ -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
@@ -248,17 +261,5 @@ module Clickwrap
248
261
  "Policy #{policy_key} enables the IP-geolocation fields #{enabled.join(", ")} " \
249
262
  "without recording IP geolocation."
250
263
  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
264
  end
264
265
  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.0"
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,33 @@ 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
+
163
190
  # Provenance that travels with any stored IP-geolocation result. A policy
164
191
  # cannot keep provider-derived coordinates while stripping the uncertainty
165
192
  # 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
 
@@ -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,12 +277,15 @@ 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.
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.
281
289
 
282
290
  <%- if record_ip_addresses? -%>
283
291
  config.reason_for_recording_ip_addresses_by_default =
@@ -301,10 +309,10 @@ Clickwrap.configure do |config|
301
309
  config.review_default_request_evidence_configuration_on = <%= review_date_literal %>
302
310
  <%- else -%>
303
311
  # 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:
312
+ # deletion period to. Turning the ordinary three on later is one line —
313
+ # `config.record_request_evidence_by_default = true` — and these are the
314
+ # settings that upgrade it into a record your own team reviewed, plus a date
315
+ # so the decision gets looked at again instead of outliving its reason:
308
316
  #
309
317
  # config.reason_for_recording_ip_addresses_by_default = "Investigate disputed submissions"
310
318
  # config.delete_recorded_ip_addresses_after = 90.days
@@ -359,7 +367,9 @@ Clickwrap.configure do |config|
359
367
  Clickwrap.trusted_proxy_configuration_digest_for_rails_application
360
368
  <%- else -%>
361
369
  # A digest of the effective trusted-proxy rules, stored beside any recorded
362
- # address. Nothing here records one, so there is nothing to stamp:
370
+ # address. Nothing here records one, so there is nothing to stamp. Set it if
371
+ # you enable IP recording later: it is not required, but without it every
372
+ # recorded address carries a blank where its collection context should be.
363
373
  #
364
374
  # config.trusted_proxy_configuration_digest =
365
375
  # Clickwrap.trusted_proxy_configuration_digest_for_rails_application
@@ -368,8 +378,9 @@ Clickwrap.configure do |config|
368
378
  <%- if any_ip_geolocation_field? -%>
369
379
  # DECISION — you enabled IP-geolocation fields and explicitly selected this
370
380
  # 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".
381
+ # Clickwrap refuses to boot with fields nothing can resolve no resolver here
382
+ # and no trackdown in the bundle — rather than recording blanks that later
383
+ # read as "no result".
373
384
  #
374
385
  # Only the fields authorized above are stored, never the whole result object,
375
386
  # and every stored value keeps the provider, source, estimated state,
@@ -378,11 +389,18 @@ Clickwrap.configure do |config|
378
389
  # needed to read them.
379
390
  config.ip_geolocation_resolver = <%= ip_geolocation_resolver_class_name %>.new
380
391
  <%- 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:
392
+ # Something has to resolve an IP-geolocation field before it can be enabled,
393
+ # here or in a policy. Bundling trackdown is enough Clickwrap uses the
394
+ # official adapter for you when you have not named a resolver of your own,
395
+ # and the privacy inventory reports it as the gem's choice rather than yours.
396
+ # It requires trackdown >= 0.4 so the provider that actually answered and
397
+ # per-request source trust are preserved:
384
398
  #
385
399
  # bundle add trackdown --version ">= 0.4"
400
+ #
401
+ # Name it explicitly to pick a different provider, or when you are wiring
402
+ # Trackdown's per-request CDN trust:
403
+ #
386
404
  # config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
387
405
  <%- end -%>
388
406
 
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.0
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