clickwrap 0.2.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 601c5b3f678c4b70f6a9b5466230a445f41bbb6c944a3479ce60ef252f189768
4
- data.tar.gz: 8137ed6901d3b33f93b18d1300ecf548eec2bb5e90cb59410d7614060e80d73d
3
+ metadata.gz: be18f8fc09eee066b71a234a134440010c6cf2beb82987b4bbe3636dde2f9133
4
+ data.tar.gz: a2c66a9be09cf999de30d890c4338e084146414cadbe0747022a3aedc8a4ef5f
5
5
  SHA512:
6
- metadata.gz: 6313bfced4a79586e71d32ee58c4e216a6a7f96b1e0910fb066f877bc3c0465fb0408bcf1d5690f630472f92584f27209ed87a1c686bae20dae1116b2fd654cc
7
- data.tar.gz: c950b0d74657b7b9965b00a7cd6274bbe62ce5fb827e20a6d076980a4b168f19a215fc3f3900a817422cc036e57698f7ef5ae77fbcdc76a7f41793421889dd66
6
+ metadata.gz: 891f939a6b827915a5d2703ecf98b0ed9b9e761d3689a1117e8ba3df6ae0450c50a3f9d53edd0cbf844e996a7be447b080070b6ab2d916409b5d1ecfdc851ddc
7
+ data.tar.gz: 2387391b3d960d05dcdc783221bb5653de21406ecd0173b8fbafa1dc17d716549044a22bc719740892553b8a9c66beb8b42417c61e9bc7f585d569849fa43d9d
data/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.2.1] - 2026-08-19
10
+
11
+ ### Added — request evidence can keep pace with the evidence it corroborates
12
+
13
+ - **`keep_recorded_{ip_addresses,browser_user_agents,ip_geolocation}_indefinitely!(because:)`.**
14
+ By-default request evidence used to demand a deletion clock, which — after
15
+ 0.2.0 flipped core evidence to keep-indefinitely — scheduled the
16
+ corroboration (IP, user agent, geolocation) to expire before the agreement
17
+ it corroborates. The third option now exists and must be said out loud with
18
+ a reason, like every escape hatch here. Declaring both a clock and
19
+ keep-indefinitely is refused as opposite decisions.
20
+
21
+ ### Documentation
22
+
23
+ - The request-evidence README section now argues FOR recording: IP + user
24
+ agent + geolocation are what cement a recorded act to a person when the
25
+ dispute is "that wasn't me", and the recommended posture is on-by-default
26
+ with the purpose written down. The discipline is unchanged — no silent
27
+ enablement, per-field decisions, encryption, reviewed proxy provenance.
28
+
9
29
  ## [0.2.0] - 2026-08-19
10
30
 
11
31
  ### Changed — evidence is kept indefinitely by default
data/README.md CHANGED
@@ -718,9 +718,43 @@ Golden fixtures make a verifier regression for any released receipt schema fail
718
718
 
719
719
  With the engine mounted, users can view and download their own receipts, and operator access is always host-authorized. Read the [receipts and verification guide](guides/receipts-and-verification.md) for exports, bundles, and what each verification tier does and doesn't establish.
720
720
 
721
- ## Request evidence is off by default
721
+ ## Request evidence: record it it's what cements the act to a person
722
722
 
723
- `clickwrap` always records its event ID, server time, capture channel, and policy version. It records **no** IP addresses, browser user-agents, or IP geolocation unless a policy names the field with a purpose and a retention rule:
723
+ `clickwrap` always records its event ID, server time, capture channel, and policy version. That proves **what was offered and what came back**. But an agreement dispute is rarely about the words — it's "that wasn't me" — and the answer to that is request evidence: the IP address, the browser, and where in the world the request came from, bound into the same digest-linked record at the same instant. Years later, "this exact sentence was accepted from this address, on this client, from this city, at this second, in the transaction that created the account" is a different conversation from "the row says yes". **Our recommendation is to record IP + user agent + geolocation on every assent policy** you already hold a purpose (defending the very agreement being made) and the evidence lives encrypted, in its own annex, deletable on its own schedule if your counsel ever decides so.
724
+
725
+ What the gem refuses to do is turn it on *silently*. Every field is a separate named decision with a written purpose — there is deliberately no `maximum_evidence` switch — so the recommended posture is three explicit blocks in your initializer:
726
+
727
+ ```ruby
728
+ # clickwrap-doc-test: syntax-only — the resolver needs trackdown installed
729
+ Clickwrap.configure do |config|
730
+ config.record_ip_address_by_default = true
731
+ config.reason_for_recording_ip_addresses_by_default =
732
+ "Corroborate who performed each recorded act, to defend the agreement itself"
733
+ config.keep_recorded_ip_addresses_indefinitely!(
734
+ because: "Corroboration must live exactly as long as the evidence it corroborates")
735
+
736
+ config.record_browser_user_agent_by_default = true
737
+ config.reason_for_recording_browser_user_agents_by_default =
738
+ "Corroborate the client context of each recorded act"
739
+ config.keep_recorded_browser_user_agents_indefinitely!(
740
+ because: "Corroboration must live exactly as long as the evidence it corroborates")
741
+
742
+ config.record_ip_geolocation_country_by_default = true
743
+ config.record_ip_geolocation_region_by_default = true
744
+ config.record_ip_geolocation_city_by_default = true
745
+ config.reason_for_recording_ip_geolocation_by_default =
746
+ "Corroborate where each recorded act was performed from"
747
+ config.keep_recorded_ip_geolocation_indefinitely!(
748
+ because: "Corroboration must live exactly as long as the evidence it corroborates")
749
+ config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
750
+
751
+ config.review_default_request_evidence_configuration_on = Date.new(2027, 8, 1)
752
+ end
753
+ ```
754
+
755
+ (`keep_recorded_..._indefinitely!` matches the retention default since 0.2.0 — evidence keeps until deletion is an explicit reviewed act. A corroboration that expires before the agreement it corroborates is a scheduled weakening of the record; if your counsel wants a clock instead, `delete_recorded_..._after` is the same one-line decision in the other direction.)
756
+
757
+ A single regulated surface can also name a field per policy instead of by default:
724
758
 
725
759
  ```ruby
726
760
  Clickwrap.policy :regulated_authorization do
@@ -228,10 +228,14 @@ module Clickwrap
228
228
  @encrypt_recorded_ip_geolocation = true
229
229
 
230
230
  # nil means "every policy that enables the field must supply its own
231
- # rule". There is no keep-forever default anywhere in this gem.
231
+ # rule" or, for by-default recording, that the host has said
232
+ # `keep_recorded_..._indefinitely!(because: "…")` out loud. Keeping
233
+ # forever is never silent: it is either the per-policy retention class's
234
+ # explicit business, or a named, reasoned sentence in the initializer.
232
235
  @delete_recorded_ip_addresses_after = nil
233
236
  @delete_recorded_browser_user_agents_after = nil
234
237
  @delete_recorded_ip_geolocation_after = nil
238
+ @keep_recorded_request_evidence_indefinitely = {}
235
239
 
236
240
  # Rails' request.remote_ip is the conventional reader. The host remains
237
241
  # responsible for configuring and testing trusted proxies correctly:
@@ -895,13 +899,22 @@ module Clickwrap
895
899
  "application's reviewed, present-tense reason, or turn that default off."
896
900
  end
897
901
 
898
- next unless delete_after.nil?
902
+ if delete_after.present? && keeps_recorded_request_evidence_indefinitely?(category)
903
+ raise ConfigurationError,
904
+ "Clickwrap is told both to delete recorded #{category} after " \
905
+ "#{delete_after.inspect} and to keep it indefinitely. Those are opposite " \
906
+ "decisions — keep exactly one."
907
+ end
908
+
909
+ next if delete_after.present? || keeps_recorded_request_evidence_indefinitely?(category)
899
910
 
900
911
  raise ConfigurationError,
901
- "Clickwrap is set to record #{category} for every policy by default, but " \
902
- "`delete_recorded_#{plural_for(category)}_after` is nil, so nothing would ever " \
903
- "delete it. Set a reviewed period, or turn the default off and let each policy " \
904
- "choose its own retention rule."
912
+ "Clickwrap is set to record #{category} for every policy by default, but nothing " \
913
+ "says how long to keep it. Either set a reviewed period with " \
914
+ "`delete_recorded_#{plural_for(category)}_after`, or keep it as long as the " \
915
+ "evidence it corroborates with " \
916
+ "`keep_recorded_#{plural_for(category)}_indefinitely!(because: \"…\")` — or turn " \
917
+ "the default off and let each policy choose its own retention rule."
905
918
  end
906
919
  end
907
920
 
@@ -913,6 +926,16 @@ module Clickwrap
913
926
  end
914
927
  end
915
928
 
929
+ def declare_indefinite_request_evidence!(category, because)
930
+ if because.to_s.strip.empty?
931
+ raise ConfigurationError,
932
+ "keep_recorded_#{plural_for(category)}_indefinitely! needs a `because:` " \
933
+ "explaining the reviewed decision."
934
+ end
935
+
936
+ @keep_recorded_request_evidence_indefinitely[category] = because
937
+ end
938
+
916
939
  def validate_trusted_proxy_configuration!
917
940
  records_ip_derived_evidence =
918
941
  record_ip_address_by_default || enabled_default_ip_geolocation_fields.any?
@@ -1098,6 +1121,32 @@ module Clickwrap
1098
1121
  # The deliberate, named escape hatch referenced by `ensure_encryption_choice`.
1099
1122
  # It exists so that turning encryption off is a sentence a reviewer can find
1100
1123
  # in a diff, with the host's own reason attached, rather than a `false`.
1124
+ # The named escape hatch for by-default request evidence with no deletion
1125
+ # clock: request evidence exists to corroborate evidence that (since 0.2.0)
1126
+ # keeps indefinitely by default, and a corroboration that expires before
1127
+ # the thing it corroborates is a scheduled weakening of the record. Same
1128
+ # rule as every escape hatch here: keeping forever must be a sentence a
1129
+ # reviewer can find in a diff, with the host's own reason attached.
1130
+ def keep_recorded_ip_addresses_indefinitely!(because:)
1131
+ declare_indefinite_request_evidence!(:ip_address, because)
1132
+ end
1133
+
1134
+ def keep_recorded_browser_user_agents_indefinitely!(because:)
1135
+ declare_indefinite_request_evidence!(:browser_user_agent, because)
1136
+ end
1137
+
1138
+ def keep_recorded_ip_geolocation_indefinitely!(because:)
1139
+ declare_indefinite_request_evidence!(:ip_geolocation, because)
1140
+ end
1141
+
1142
+ def keeps_recorded_request_evidence_indefinitely?(category)
1143
+ @keep_recorded_request_evidence_indefinitely.key?(category.to_sym)
1144
+ end
1145
+
1146
+ def reason_for_keeping_recorded_request_evidence_indefinitely(category)
1147
+ @keep_recorded_request_evidence_indefinitely[category.to_sym]
1148
+ end
1149
+
1101
1150
  def deliberately_store_request_evidence_unencrypted!(because:)
1102
1151
  if because.to_s.strip.empty?
1103
1152
  raise ConfigurationError,
@@ -69,6 +69,21 @@ module Clickwrap
69
69
  assign_rule!(:ip_geolocation, host_event_name:)
70
70
  end
71
71
 
72
+ # Keeping the annex as long as the core event it corroborates, said in
73
+ # the retention class itself. A corroboration that expires before the
74
+ # evidence it corroborates is a scheduled weakening of the record.
75
+ def keep_recorded_ip_address_indefinitely
76
+ assign_rule!(:ip_address, indefinite: true)
77
+ end
78
+
79
+ def keep_recorded_browser_user_agent_indefinitely
80
+ assign_rule!(:browser_user_agent, indefinite: true)
81
+ end
82
+
83
+ def keep_recorded_ip_geolocation_indefinitely
84
+ assign_rule!(:ip_geolocation, indefinite: true)
85
+ end
86
+
72
87
  def compile = RetentionClass.new(key: @key, rules: @rules)
73
88
 
74
89
  private
@@ -483,10 +483,14 @@ module Clickwrap
483
483
 
484
484
  # --- Retention ------------------------------------------------------------
485
485
 
486
- # Every recorded field leaves here with a disposal rule: a date, or the name
487
- # of a host rule that will produce one. There is no keep-forever default
488
- # anywhere in this gem, and a recorded field with neither is a configuration
489
- # bug caught before the row is written rather than a row nobody ever deletes.
486
+ # 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
494
  #
491
495
  # `retain_until` names a host calculation instead of a duration because real
492
496
  # record-keeping schedules are not always durations — "five years, or three
@@ -500,6 +504,14 @@ module Clickwrap
500
504
  return { "#{category}_delete_after": now + class_rule.duration } if class_rule&.duration?
501
505
  return { "#{category}_retain_until_rule": class_rule.host_event_name.to_s } if class_rule&.host_event?
502
506
 
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
+
503
515
  raise ConfigurationError, missing_retention_message(category)
504
516
  end
505
517
 
@@ -510,11 +522,13 @@ module Clickwrap
510
522
  end
511
523
 
512
524
  def missing_retention_message(category)
513
- "Clickwrap is about to record #{category} for policy #{policy_key} and nothing says when " \
514
- "to delete it. Give the policy a rule — `delete_after:` with a reviewed period, or " \
515
- "`retain_until:` naming a host retention calculation — or add a #{category} rule to " \
516
- "retention class #{policy.retention_class_key.inspect}. Clickwrap has no keep-forever " \
517
- "default and will not choose a period for you."
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."
518
532
  end
519
533
 
520
534
  # --- Failing closed -------------------------------------------------------
@@ -205,12 +205,15 @@ module Clickwrap
205
205
  "as a data-collection purpose."
206
206
  end
207
207
 
208
- if setting.delete_after.nil? && setting.retain_until.nil? && retention_class_key.nil?
208
+ if setting.delete_after.nil? && setting.retain_until.nil? && retention_class_key.nil? &&
209
+ !Clickwrap.config.keeps_recorded_request_evidence_indefinitely?(category)
209
210
  raise DefinitionError,
210
- "Policy #{policy_key} records #{category} but never says when to delete it. " \
211
- "Give it `delete_after:` with a duration, or `retain_until:` naming a host event " \
212
- "rule, or attach a retention class with a rule for this category. Clickwrap has " \
213
- "no keep-forever default."
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."
214
217
  end
215
218
 
216
219
  return unless setting.delete_after && setting.delete_after.to_i <= 0
@@ -110,15 +110,32 @@ module Clickwrap
110
110
  setting = policy.request_evidence.setting_for(category)
111
111
  next unless setting.record?
112
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)
113
117
 
114
118
  raise DefinitionError,
115
- "Policy #{policy.key} records #{category}, but neither that policy nor retention " \
116
- "class #{retention_class.key} says when to dispose of it. Add " \
117
- "`delete_after:`/`retain_until:` to the policy or the matching plain-English " \
118
- "request-evidence rule to the retention class."
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: \"…\")`."
119
125
  end
120
126
  end
121
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
+
122
139
  def validate_host_calculations!(policy, retention_class)
123
140
  referenced = retention_class.rules.values.filter_map do |rule|
124
141
  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.0"
4
+ VERSION = "0.2.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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clickwrap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez