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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +212 -2
- data/README.md +64 -24
- data/guides/integrating.md +14 -10
- data/guides/naming.md +14 -6
- data/guides/request-evidence.md +80 -15
- data/lib/clickwrap/configuration.rb +158 -74
- data/lib/clickwrap/doctor.rb +7 -2
- data/lib/clickwrap/dsl/policy_builder.rb +31 -6
- data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +16 -0
- data/lib/clickwrap/privacy.rb +28 -7
- data/lib/clickwrap/request_evidence_extractor.rb +16 -27
- data/lib/clickwrap/request_evidence_policy.rb +45 -41
- data/lib/clickwrap/services/validate_policy_references.rb +7 -32
- data/lib/clickwrap/version.rb +1 -1
- data/lib/clickwrap/vocabulary.rb +36 -0
- data/lib/generators/clickwrap/install_generator.rb +22 -11
- data/lib/generators/clickwrap/templates/initializer.rb.erb +53 -28
- 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: 55f77c92e4ac5400a7c7fba781bd2a592875f3deef59fd8b3c1d6269e302ffc6
|
|
4
|
+
data.tar.gz: f15af7f288d8349bc8cd322b28b472e68bca750a8c17718648d7fa2db3877021
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fceb30a214fe8e4cdfcc19a775f8082fefe61424eba2e463958240dec07a0c8d099e1d3c6e5e0b9d67bd47eb11a445ee591f4b520c08f847479fbed277f197f2
|
|
7
|
+
data.tar.gz: e8d53673c7a5d9ba553078ace895a187e404b6a486525ee56d362f584e262f3bb9f9000287bf57311b22a40e2049140867f5eefdb300e945a7d69d25ff31dcf4
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,210 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.1] - 2026-08-20
|
|
10
|
+
|
|
11
|
+
### Changed — the rest of the collection friction, and the principle behind removing it
|
|
12
|
+
|
|
13
|
+
0.3.0 made *enabling* request evidence one switch and gave the initializer
|
|
14
|
+
defaults honest gem-supplied purposes. It left the friction standing
|
|
15
|
+
everywhere else: a policy-level `record_ip_address` still read as though it
|
|
16
|
+
wanted a sentence, `record_ip_geolocation` refused to do anything without a
|
|
17
|
+
field list, and the encryption escape hatch still demanded a reason be
|
|
18
|
+
phrased twice. This finishes the job the owner asked for.
|
|
19
|
+
|
|
20
|
+
The principle, in their words: **the host application and its privacy policy
|
|
21
|
+
own *why* data is collected; this gem records *what* was collected, honestly,
|
|
22
|
+
and is nobody's nanny.** Its job is evidence mechanics, not gatekeeping
|
|
23
|
+
collection. Everything below now works exactly as written:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
Clickwrap.configure do |config|
|
|
27
|
+
config.record_request_evidence_by_default = true
|
|
28
|
+
config.keep_recorded_ip_addresses_indefinitely!
|
|
29
|
+
config.deliberately_store_request_evidence_unencrypted!
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Clickwrap.policy :anything do
|
|
33
|
+
agree_to :terms
|
|
34
|
+
record_ip_address
|
|
35
|
+
record_browser_user_agent
|
|
36
|
+
record_ip_geolocation
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- **All three `record_` verbs take zero keyword arguments.** `because:`,
|
|
41
|
+
`legal_basis_reference:`, `data_protection_impact_assessment_reference:`,
|
|
42
|
+
`delete_after:`, `retain_until:`, and `encrypted:` are optional in fact and
|
|
43
|
+
not merely in the signature — nothing downstream refuses their absence.
|
|
44
|
+
(`legal_basis_reference` and the DPIA reference never were required anywhere
|
|
45
|
+
in the gem; a test now pins that they never become so.)
|
|
46
|
+
- **`record_ip_geolocation` with no field named records the coarse trio** —
|
|
47
|
+
country, region, city — the same set `record_request_evidence_by_default`
|
|
48
|
+
turns on, and nothing finer. The field keywords moved from `false` to `nil`
|
|
49
|
+
defaults so "did not mention this field" is distinguishable from "named it
|
|
50
|
+
and turned it off". Naming even one field means you are choosing the set
|
|
51
|
+
yourself, and the set is exactly what you named. Naming every field `false`
|
|
52
|
+
is still refused, because calling the verb and disabling everything cannot
|
|
53
|
+
mean anything; `do_not_record_ip_geolocation` is how to say that.
|
|
54
|
+
- **`keep_recorded_{ip_addresses,browser_user_agents,ip_geolocation}_indefinitely!`
|
|
55
|
+
take no arguments at all.**
|
|
56
|
+
- **`deliberately_store_request_evidence_unencrypted!` no longer needs a
|
|
57
|
+
`because:`.** The method NAME is the ceremony: `encrypt_recorded_* = false`
|
|
58
|
+
still cannot be reached without writing that line, and a reviewer still
|
|
59
|
+
finds it in the diff. When the host writes no reason the gem records
|
|
60
|
+
`Vocabulary::DEFAULT_REASON_FOR_STORING_REQUEST_EVIDENCE_UNENCRYPTED`.
|
|
61
|
+
Encryption itself is untouched — on by default for all three categories,
|
|
62
|
+
with a test pinning that the one switch does not weaken it.
|
|
63
|
+
- **The install generator stops refusing an incomplete category.**
|
|
64
|
+
`--record-ip-addresses-by-default` with no reason and no period writes the
|
|
65
|
+
file and simply omits those two lines, so the gem's own defaults apply.
|
|
66
|
+
- The `ReviewedText` placeholder check now only ever applies to text a host
|
|
67
|
+
actually supplied. Absence is never scaffolding.
|
|
68
|
+
|
|
69
|
+
### Unchanged, deliberately
|
|
70
|
+
|
|
71
|
+
- Reasons that are not about *collection* keep their required `because:`:
|
|
72
|
+
`delete_recorded_ip_address!` and its siblings, `dispose_core_event!`,
|
|
73
|
+
`place_on_legal_hold!` / `release_legal_hold!`, `plan_disposition_for`,
|
|
74
|
+
unredacted receipt export, and the lifecycle verbs. Those record a
|
|
75
|
+
destructive act, an access, or a state change — there the audit trail *is*
|
|
76
|
+
the reason, and there is no honest default for "why did somebody delete
|
|
77
|
+
this".
|
|
78
|
+
- Still refused, because each is the host contradicting themselves rather than
|
|
79
|
+
leaving a blank: scaffolding text the host actually wrote standing in for a
|
|
80
|
+
purpose (including in the installer, where a `TODO` in a shipped initializer
|
|
81
|
+
is worse than no line and the gem would reject it at boot anyway), a
|
|
82
|
+
negative deletion period passed to the installer, a deletion clock declared
|
|
83
|
+
alongside `keep_recorded_..._indefinitely!` for the same category, and
|
|
84
|
+
`record_ip_geolocation` with every field explicitly off.
|
|
85
|
+
- The gem's code default is still record-nothing. Claim boundaries, receipt
|
|
86
|
+
state labeling (`not_configured` / `unavailable` / `recorded` /
|
|
87
|
+
`deleted_after_retention`), and every released receipt format are untouched.
|
|
88
|
+
|
|
89
|
+
### Documentation
|
|
90
|
+
|
|
91
|
+
- README, `guides/request-evidence.md`, and `guides/naming.md` show the
|
|
92
|
+
zero-keyword forms as the ordinary way to write a policy, with purposes,
|
|
93
|
+
legal bases, and clocks as the upgrade path.
|
|
94
|
+
- `CLAUDE.md` / `AGENTS.md` rule 6 is rewritten around the principle above and
|
|
95
|
+
records the owner directive and its date. What it keeps as non-negotiable:
|
|
96
|
+
never a switch whose NAME hides what it collects, never an overclaim, never
|
|
97
|
+
a blurred receipt state, encryption on by default behind its named call,
|
|
98
|
+
host-supplied scaffolding rejected, and contradictions refused.
|
|
99
|
+
|
|
100
|
+
## [0.3.0] - 2026-08-20
|
|
101
|
+
|
|
102
|
+
### Changed — recording request evidence is one switch, not a checklist
|
|
103
|
+
|
|
104
|
+
Owner directive, and a deliberate reversal of part of this gem's own
|
|
105
|
+
philosophy. The reasoning, stated plainly so a future reader can weigh it:
|
|
106
|
+
the ceremony around *enabling* request evidence — a written purpose, a
|
|
107
|
+
disposal answer, and a reviewed trusted-proxy digest, each of them a boot
|
|
108
|
+
refusal — was not producing better-reviewed collection. It was producing
|
|
109
|
+
**no collection at all**. Integrators hit three refusals in a row on the way
|
|
110
|
+
to their first capture and turned the fields off, and an agreement with no
|
|
111
|
+
corroboration is worse evidence than one corroborated under a purpose the gem
|
|
112
|
+
stated on their behalf. So the friction is gone from turning collection *on*.
|
|
113
|
+
None of it is gone from describing collection honestly.
|
|
114
|
+
|
|
115
|
+
- **`config.record_request_evidence_by_default = true`.** One line records, on
|
|
116
|
+
every policy, the IP address the request arrived from, the browser user
|
|
117
|
+
agent it sent, and a coarse country / region / city estimate for that
|
|
118
|
+
address. Nothing finer: a postal code, coordinates, a timezone, a continent,
|
|
119
|
+
a metro code, and an accuracy radius each remain their own separately named
|
|
120
|
+
setting. It is a fan-out setter over the existing `record_*_by_default`
|
|
121
|
+
flags, so it composes with them in reading order, and any policy still
|
|
122
|
+
overrides it with `record_ip_address(...)` or `do_not_record_ip_address`.
|
|
123
|
+
The reader reports what is actually on rather than a remembered assignment.
|
|
124
|
+
- **A purpose is no longer the price of admission.** A category enabled
|
|
125
|
+
without `because:` / `reason_for_recording_*_by_default` records
|
|
126
|
+
`Vocabulary::DEFAULT_REQUEST_EVIDENCE_PURPOSE` — "Corroborate who performed
|
|
127
|
+
each recorded act, from where, on what client — to defend the recorded
|
|
128
|
+
agreement itself." Every compiled policy revision therefore still carries a
|
|
129
|
+
purpose into every receipt, and `Clickwrap::Privacy.inventory` marks each one
|
|
130
|
+
`"purpose_source": "gem_default"` or `"host"` so the gem's sentence can never
|
|
131
|
+
be mistaken for a decision somebody reviewed.
|
|
132
|
+
- **No disposal answer means indefinite.** The four gatekeepers that refused a
|
|
133
|
+
recorded field with no clock — `Configuration#validate_request_evidence_defaults!`,
|
|
134
|
+
the policy-level check in `RequestEvidencePolicy`, the reference validator,
|
|
135
|
+
and the capture-time extractor — now treat absence as "keeps pace with the
|
|
136
|
+
evidence it corroborates", which is what core evidence has done since 0.2.0.
|
|
137
|
+
The annex is stamped with no schedule and the retention planner never lists
|
|
138
|
+
it. `keep_recorded_*_indefinitely!` still works, still records its reason,
|
|
139
|
+
and now accepts no `because:` at all (defaulting to "Corroboration lives as
|
|
140
|
+
long as the evidence it corroborates").
|
|
141
|
+
- **`trusted_proxy_configuration_digest` is no longer required to record an IP
|
|
142
|
+
address.** When it is absent the annex stores `nil`, and that nil is honest
|
|
143
|
+
provenance: no reviewed proxy configuration was recorded when this address
|
|
144
|
+
was observed. Hosts who set one still get the stronger record, the setter
|
|
145
|
+
still refuses anything that is not a complete prefixed SHA-2 digest, and
|
|
146
|
+
`clickwrap:doctor` still warns while it is unset.
|
|
147
|
+
- **Bundled `trackdown` is used without a wiring line.** A policy that records
|
|
148
|
+
IP geolocation and names no resolver now gets
|
|
149
|
+
`Clickwrap::IpGeolocation::TrackdownResolver` automatically when the host's
|
|
150
|
+
bundle carries trackdown 0.4 or newer — lazily, considered once, and only at
|
|
151
|
+
the moment something actually needs an address resolved. The privacy
|
|
152
|
+
inventory reports such a resolver with `"source": "gem_default"` and doctor
|
|
153
|
+
names it. An installed release older than 0.4 gets the adapter's own
|
|
154
|
+
upgrade sentence rather than a misleading "trackdown is not installed", and
|
|
155
|
+
a host with no trackdown gets the boot sentence, now naming `bundle add
|
|
156
|
+
trackdown` as the first option.
|
|
157
|
+
|
|
158
|
+
### Unchanged, deliberately
|
|
159
|
+
|
|
160
|
+
- The gem's code default is still record-nothing. The switch is opt-in.
|
|
161
|
+
- Claim boundaries are untouched: nothing says compliant, enforceable, proves
|
|
162
|
+
identity, or physical location, and IP geolocation remains network context —
|
|
163
|
+
not identity, not GPS.
|
|
164
|
+
- Encryption stays on by default, and turning it off keeps its
|
|
165
|
+
`deliberately_store_request_evidence_unencrypted!(because:)` ceremony. That
|
|
166
|
+
one is a genuine hazard with a named escape hatch; the directive was about
|
|
167
|
+
the friction of enabling collection, not the friction of weakening it.
|
|
168
|
+
- Scaffolding text is still refused wherever the host actually wrote it
|
|
169
|
+
(`"TODO: ask legal"` is not a purpose), and a deletion clock declared
|
|
170
|
+
alongside `keep_recorded_..._indefinitely!` for the same category is still
|
|
171
|
+
refused as opposite decisions.
|
|
172
|
+
- Receipts still distinguish `not_configured` / `unavailable` / `recorded` /
|
|
173
|
+
`deleted_after_retention`, and no released receipt format changed. The
|
|
174
|
+
purpose provenance is deliberately kept off `to_snapshot`, so policy
|
|
175
|
+
revision digests and every golden fixture verify exactly as before.
|
|
176
|
+
|
|
177
|
+
### Documentation — the request-evidence story is rewritten around the switch
|
|
178
|
+
|
|
179
|
+
- README, `guides/request-evidence.md`, `guides/integrating.md`, and
|
|
180
|
+
`guides/naming.md` lead with the one switch and present purposes, legal
|
|
181
|
+
bases, clocks, and proxy digests as the upgrade path for teams who want
|
|
182
|
+
reviewed records — not as the entry fee. The now-false "boot refuses without
|
|
183
|
+
a reason or a period" claims are gone.
|
|
184
|
+
- `CLAUDE.md` / `AGENTS.md` rule 6 is rewritten to the new truth and records
|
|
185
|
+
this directive and its date, so a future agent does not restore the
|
|
186
|
+
refusals as a "fix". The prohibition it keeps is the one that was always the
|
|
187
|
+
point: never add a switch whose *name* hides what it collects.
|
|
188
|
+
|
|
189
|
+
### Documentation — the docs stop contradicting the shipped gem
|
|
190
|
+
|
|
191
|
+
- **The README and `guides/integrating.md` teach `gem "clickwrap"`.** Both
|
|
192
|
+
still told readers that `bundle add clickwrap` would fetch the empty 0.0.0
|
|
193
|
+
name placeholder and that they should install from GitHub instead — four
|
|
194
|
+
published releases after that stopped being true, and directly under a
|
|
195
|
+
correct Gemfile snippet in the README's own quick start.
|
|
196
|
+
- **The status banner says where the gem actually stands.** It claimed the gem
|
|
197
|
+
was "not yet proven in production" and had not been through its planned
|
|
198
|
+
production integrations; it now records that the gem runs in production in
|
|
199
|
+
more than one application — behind real signups, and under a
|
|
200
|
+
payout-authorization flow — and ships enabled by default in RailsFast, while
|
|
201
|
+
naming the two gates that genuinely remain open — the unfamiliar-developer setup
|
|
202
|
+
exercise, and the legal/privacy review of the default wording. The old
|
|
203
|
+
"don't put it under a payout flow just yet" line is replaced by the
|
|
204
|
+
discipline such a flow deserves: keep your existing verifier authoritative
|
|
205
|
+
until you have shown parity with it, bind each one-time authorization to its
|
|
206
|
+
subject, and test the double-submit race.
|
|
207
|
+
- **One `## [0.1.0]` heading in this file, not two.** The section dated
|
|
208
|
+
2026-08-15 was drafted as release notes on the day the engine landed, but
|
|
209
|
+
0.1.0 was not published until 2026-08-19. It now reads as the
|
|
210
|
+
first-implementation subsection of the release it was always part of, with
|
|
211
|
+
its text intact.
|
|
212
|
+
|
|
9
213
|
## [0.2.1] - 2026-08-19
|
|
10
214
|
|
|
11
215
|
### Added — request evidence can keep pace with the evidence it corroborates
|
|
@@ -584,7 +788,13 @@ Changes driven by the first production host application:
|
|
|
584
788
|
tests read the signed presentation token and its controls back off the
|
|
585
789
|
rendered page, the way a browser does.
|
|
586
790
|
|
|
587
|
-
|
|
791
|
+
### The first implementation, drafted 2026-08-15
|
|
792
|
+
|
|
793
|
+
> These notes were written on 2026-08-15, the day the engine first landed, and
|
|
794
|
+
> were drafted then as the release notes for 0.1.0. The release itself did not
|
|
795
|
+
> go out until 2026-08-19; the sections above are the four days in between.
|
|
796
|
+
> Everything below shipped as part of that same 0.1.0 and was never released
|
|
797
|
+
> on its own.
|
|
588
798
|
|
|
589
799
|
First implemented release. `clickwrap` turns terms acceptance, privacy notice
|
|
590
800
|
acknowledgment, consent, factual declarations, operator attestations, and
|
|
@@ -599,7 +809,7 @@ its purpose, and its retention. The gem provides evidence mechanics only: your
|
|
|
599
809
|
application and its counsel still own the legal text, lawful basis, substantive
|
|
600
810
|
validity, capacity, authority, and retention periods.
|
|
601
811
|
|
|
602
|
-
|
|
812
|
+
#### Added
|
|
603
813
|
|
|
604
814
|
- **Immutable versioned documents.** `Clickwrap.document :terms, version:, from:`
|
|
605
815
|
points at the files your application already owns; `bin/rails clickwrap:publish`
|
data/README.md
CHANGED
|
@@ -35,7 +35,7 @@ If the evidence can't be recorded, the action doesn't happen. If the action fail
|
|
|
35
35
|
No JavaScript package. No Redis. No background jobs. No external accounts or per-event API calls. No legal-document vendor. Just Rails, your database, and a DSL that reads like plain English.
|
|
36
36
|
|
|
37
37
|
> [!IMPORTANT]
|
|
38
|
-
> **Status:
|
|
38
|
+
> **Status: in production, with two gates still open.** Everything in this README is implemented and covered by the test suite; the gem runs in production in more than one application today, behind real signups and under a payout-authorization flow; and it ships enabled by default — request-evidence annex included — in the [RailsFast](https://railsfast.com/?ref=clickwrap) template. Two gates remain open: no unfamiliar developer has installed it unassisted, and no focused legal or privacy review has passed over the default wording, the receipt claim boundaries, or the request-evidence posture — so read those defaults with your own counsel rather than adopting them on trust. High-assurance flows — payouts, one-time authorizations — still deserve the full discipline rather than a leap of faith: keep your existing verifier authoritative until you have demonstrated parity with it, bind every authorization to its subject and consume it exactly once, and test the double-submit race in your own application. The [stability promise](#stability-and-upgrade-promise) applies from 0.1.0 onward.
|
|
39
39
|
|
|
40
40
|
## 👨💻 Example
|
|
41
41
|
|
|
@@ -110,10 +110,11 @@ Clickwrap.policy :signup do
|
|
|
110
110
|
end
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
(
|
|
114
|
-
|
|
115
|
-
a period for you. A minimal,
|
|
116
|
-
|
|
113
|
+
(A retention class that names no rule keeps the evidence, which is the same
|
|
114
|
+
posture request evidence takes: keeping is reversible, deleting is the explicit
|
|
115
|
+
act. `clickwrap` will not pick a *deletion* period for you. A minimal,
|
|
116
|
+
digest-linked disposition tombstone remains after a reviewed core deletion so
|
|
117
|
+
the deletion itself does not become an unexplained hole.)
|
|
117
118
|
|
|
118
119
|
Add one macro to your model:
|
|
119
120
|
|
|
@@ -165,7 +166,7 @@ bin/rails generate clickwrap:install
|
|
|
165
166
|
bin/rails db:migrate
|
|
166
167
|
```
|
|
167
168
|
|
|
168
|
-
`bundle add clickwrap`
|
|
169
|
+
`bundle add clickwrap` writes that Gemfile line and installs the gem in one command, if you would rather not edit the file yourself.
|
|
169
170
|
|
|
170
171
|
The installer detects Rails authentication vs. Devise, integer vs. UUID primary keys, and your database adapter, then generates adaptive migrations, one annotated initializer, and a conventional signup policy. It emits only the tables your installation can actually write to; the capabilities that are off by default bring their own migration when you want them:
|
|
171
172
|
|
|
@@ -722,39 +723,72 @@ With the engine mounted, users can view and download their own receipts, and ope
|
|
|
722
723
|
|
|
723
724
|
`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
|
|
|
725
|
-
|
|
726
|
+
So turn it on. One line, and you are done:
|
|
727
|
+
|
|
728
|
+
```ruby
|
|
729
|
+
Clickwrap.configure do |config|
|
|
730
|
+
config.record_request_evidence_by_default = true
|
|
731
|
+
end
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
That records, on every policy: the IP address the request arrived from, the browser user agent it sent, and a coarse country / region / city estimate for that address. Add [`trackdown`](https://github.com/rameerez/trackdown) 0.4+ to your Gemfile and the geolocation half resolves itself — Clickwrap picks up the official adapter with no wiring line, so trackdown plus Cloudflare genuinely is "bundle it and flip the switch".
|
|
735
|
+
|
|
736
|
+
Nothing else is required — anywhere, at either level. **Your privacy policy owns the why; the gem records the what, honestly, and is nobody's nanny.** Clickwrap supplies its own defaults for every part you did not write:
|
|
737
|
+
|
|
738
|
+
- **Purpose.** Every recorded field carries one into the receipt. Yours if you wrote one, otherwise Clickwrap's: *"Corroborate who performed each recorded act, from where, on what client — to defend the recorded agreement itself."* The [privacy inventory](#operations) marks which of the two it is reading back (`"purpose_source": "gem_default"` vs `"host"`), so a gem sentence never passes for a decision your team reviewed.
|
|
739
|
+
- **How long.** No clock means it keeps pace with the evidence it corroborates — the same posture core evidence has had since 0.2.0. A corroboration scheduled to expire before the agreement it corroborates is a scheduled weakening of the record.
|
|
740
|
+
- **Proxy provenance.** `trusted_proxy_configuration_digest` is recorded when you set one and left `nil` when you have not, and the `nil` is itself the honest answer: no reviewed proxy configuration was in force when this address was observed. `bin/rails clickwrap:doctor` says so out loud.
|
|
741
|
+
|
|
742
|
+
What the gem still will not do is hide what it collects behind a name. The switch is called `record_request_evidence_by_default` because that is what it does; there is deliberately no `gdpr_compliant_mode`, `maximum_evidence`, or `legal_proof`, and no flag here makes any claim about the law. It also stops exactly at the coarse trio: a postal code, coordinates, a timezone, a metro code, and an accuracy radius each stay their own separately named line, because a switch that reads "record request evidence" should not hand you coordinates you never asked for.
|
|
743
|
+
|
|
744
|
+
### The upgrade path: reviewed records
|
|
745
|
+
|
|
746
|
+
The one-liner is the entry point, not the ceiling. Teams who want the stronger record — words their counsel signed off on, a legal basis reference, a named clock, a reviewed proxy topology — write them, and Clickwrap keeps every one of them as the host's own:
|
|
726
747
|
|
|
727
748
|
```ruby
|
|
728
749
|
# clickwrap-doc-test: syntax-only — the resolver needs trackdown installed
|
|
729
750
|
Clickwrap.configure do |config|
|
|
730
|
-
config.
|
|
751
|
+
config.record_request_evidence_by_default = true
|
|
752
|
+
|
|
731
753
|
config.reason_for_recording_ip_addresses_by_default =
|
|
732
754
|
"Corroborate who performed each recorded act, to defend the agreement itself"
|
|
755
|
+
config.legal_basis_reference_for_recording_ip_addresses_by_default = "LIA-SECURITY-2026-01"
|
|
733
756
|
config.keep_recorded_ip_addresses_indefinitely!(
|
|
734
757
|
because: "Corroboration must live exactly as long as the evidence it corroborates")
|
|
735
758
|
|
|
736
|
-
config.record_browser_user_agent_by_default = true
|
|
737
759
|
config.reason_for_recording_browser_user_agents_by_default =
|
|
738
760
|
"Corroborate the client context of each recorded act"
|
|
739
|
-
config.
|
|
740
|
-
because: "Corroboration must live exactly as long as the evidence it corroborates")
|
|
761
|
+
config.delete_recorded_browser_user_agents_after = 2.years
|
|
741
762
|
|
|
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
763
|
config.reason_for_recording_ip_geolocation_by_default =
|
|
746
764
|
"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
765
|
config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
|
|
750
766
|
|
|
767
|
+
config.trusted_proxy_configuration_digest =
|
|
768
|
+
Clickwrap.trusted_proxy_configuration_digest_for_rails_application
|
|
751
769
|
config.review_default_request_evidence_configuration_on = Date.new(2027, 8, 1)
|
|
752
770
|
end
|
|
753
771
|
```
|
|
754
772
|
|
|
755
|
-
|
|
773
|
+
Two things are still refused, and both are you contradicting yourself rather than leaving a blank: scaffolding text (`"TODO: ask legal"`) standing in for a purpose, and a deletion clock set alongside `keep_recorded_..._indefinitely!` for the same category.
|
|
774
|
+
|
|
775
|
+
### Per policy, with as much or as little as you want
|
|
776
|
+
|
|
777
|
+
A single surface can name the fields itself instead of inheriting the default. The frictionless form takes no arguments at all:
|
|
778
|
+
|
|
779
|
+
```ruby
|
|
780
|
+
Clickwrap.policy :withdrawal_authorization do
|
|
781
|
+
authorize :regulated_action, one_time: true, valid_for: 10.minutes
|
|
782
|
+
|
|
783
|
+
record_ip_address
|
|
784
|
+
record_browser_user_agent
|
|
785
|
+
record_ip_geolocation
|
|
786
|
+
end
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
`record_ip_geolocation` with no field named records the same coarse trio as the switch — country, region, city. Name even one field and you are choosing the set yourself, and the set is exactly what you named.
|
|
756
790
|
|
|
757
|
-
|
|
791
|
+
The same declarations with the full record a reviewed team would want:
|
|
758
792
|
|
|
759
793
|
```ruby
|
|
760
794
|
Clickwrap.policy :regulated_authorization do
|
|
@@ -773,10 +807,13 @@ end
|
|
|
773
807
|
Recorded values live in a separately encrypted annex with their own retention, so
|
|
774
808
|
they can be deleted later without rewriting the core event payload. Core payloads
|
|
775
809
|
have their own reviewed disposition path and leave a digest-linked tombstone.
|
|
776
|
-
|
|
777
|
-
|
|
810
|
+
Encryption is on by default for all three categories. Turning it off keeps its
|
|
811
|
+
own ceremony — you cannot reach `encrypt_recorded_ip_addresses = false` without
|
|
812
|
+
first writing `config.deliberately_store_request_evidence_unencrypted!` — but the
|
|
813
|
+
ceremony is the method name a reviewer finds in the diff, not a sentence the gem
|
|
814
|
+
makes you type. `because:` there is optional too.
|
|
778
815
|
|
|
779
|
-
For IP geolocation, [`trackdown`](https://github.com/rameerez/trackdown) 0.4 or newer is the
|
|
816
|
+
For IP geolocation, [`trackdown`](https://github.com/rameerez/trackdown) 0.4 or newer is the official resolver, and Clickwrap uses it automatically when your bundle has it and you named no resolver of your own. Set it explicitly when you want a different provider per policy, or when you are wiring Trackdown's per-request CDN trust:
|
|
780
817
|
|
|
781
818
|
```ruby
|
|
782
819
|
Trackdown.configure do |trackdown|
|
|
@@ -1237,8 +1274,11 @@ Clickwrap.configure do |config|
|
|
|
1237
1274
|
controller.current_user == receipt.actor
|
|
1238
1275
|
end
|
|
1239
1276
|
|
|
1240
|
-
#
|
|
1241
|
-
#
|
|
1277
|
+
# Nothing request-derived is stored until you say so. This one line says so:
|
|
1278
|
+
# IP address, browser user agent, and a coarse country/region/city estimate,
|
|
1279
|
+
# on every policy. Per-field flags and per-policy declarations still override
|
|
1280
|
+
# it in either direction.
|
|
1281
|
+
config.record_request_evidence_by_default = true
|
|
1242
1282
|
|
|
1243
1283
|
# Optional hooks run only after evidence and domain state have committed:
|
|
1244
1284
|
config.after_event_is_committed = ->(event) { }
|
|
@@ -1340,7 +1380,7 @@ No — and `clickwrap` never equates scrolling with reading. It makes documents
|
|
|
1340
1380
|
|
|
1341
1381
|
### Should I record IP addresses?
|
|
1342
1382
|
|
|
1343
|
-
|
|
1383
|
+
We think yes, for assent policies — they are what turns "the row says yes" into "this sentence was accepted from this address, on this client, at this second". They corroborate request context; they do not prove identity or physical location. The gem's own default is still off, and turning them on is `config.record_request_evidence_by_default = true`.
|
|
1344
1384
|
|
|
1345
1385
|
### Can I keep my domain models?
|
|
1346
1386
|
|
data/guides/integrating.md
CHANGED
|
@@ -23,10 +23,8 @@ survived contact with production code.
|
|
|
23
23
|
## 1. Install, in this exact order
|
|
24
24
|
|
|
25
25
|
```ruby
|
|
26
|
-
# Gemfile
|
|
27
|
-
|
|
28
|
-
# version is published there.
|
|
29
|
-
gem "clickwrap", github: "rameerez/clickwrap"
|
|
26
|
+
# Gemfile
|
|
27
|
+
gem "clickwrap"
|
|
30
28
|
```
|
|
31
29
|
|
|
32
30
|
```bash
|
|
@@ -562,8 +560,11 @@ history, not just post-migration. The shape that worked:
|
|
|
562
560
|
|
|
563
561
|
## 9. Request evidence, when a surface earns it
|
|
564
562
|
|
|
565
|
-
|
|
566
|
-
|
|
563
|
+
The gem collects nothing until you say otherwise, and since 0.3.0 saying
|
|
564
|
+
otherwise for every policy at once is one line —
|
|
565
|
+
`config.record_request_evidence_by_default = true`. We took the narrower road
|
|
566
|
+
and enabled per policy, per field, where the surface earned it (our money path
|
|
567
|
+
did; our signup did not):
|
|
567
568
|
|
|
568
569
|
```ruby
|
|
569
570
|
policy.record_ip_address(
|
|
@@ -574,16 +575,19 @@ policy.record_ip_address(
|
|
|
574
575
|
)
|
|
575
576
|
```
|
|
576
577
|
|
|
577
|
-
|
|
578
|
-
a digest of the effective proxy rules, not a
|
|
579
|
-
records which configuration was in force. Generate
|
|
580
|
-
rules (or Rails' actual defaults when none were
|
|
578
|
+
`config.trusted_proxy_configuration_digest` is worth setting and is no longer
|
|
579
|
+
required to record an address — a digest of the effective proxy rules, not a
|
|
580
|
+
prose label, so old evidence records which configuration was in force. Generate
|
|
581
|
+
it from Rails' configured rules (or Rails' actual defaults when none were
|
|
582
|
+
overridden):
|
|
581
583
|
|
|
582
584
|
```ruby
|
|
583
585
|
config.trusted_proxy_configuration_digest =
|
|
584
586
|
Clickwrap.trusted_proxy_configuration_digest_for_rails_application
|
|
585
587
|
```
|
|
586
588
|
|
|
589
|
+
Leave it unset and the annex records a `nil` digest, which honestly says nobody
|
|
590
|
+
reviewed a proxy topology; `clickwrap:doctor` warns while that is the case.
|
|
587
591
|
This records configuration provenance; it does not prove the rules were
|
|
588
592
|
correctly deployed or reviewed. Sharing the same fields across several policies?
|
|
589
593
|
A plain Ruby lambda in `config/clickwrap.rb` calling
|
data/guides/naming.md
CHANGED
|
@@ -106,11 +106,19 @@ The same distinction runs through the six request-evidence states. `not_configur
|
|
|
106
106
|
configuration fact; `unavailable` is an observation; `deleted_after_retention` is a third thing
|
|
107
107
|
entirely. None of them is blank.
|
|
108
108
|
|
|
109
|
-
### 6. One option never secretly enables another category of data
|
|
109
|
+
### 6. One option never *secretly* enables another category of data
|
|
110
110
|
|
|
111
|
-
**Why:** this is the failure mode the gem exists to prevent. An option
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
**Why:** this is the failure mode the gem exists to prevent. An option whose name does not say
|
|
112
|
+
what it collects makes the diff unreviewable and the upgrade dangerous — a later release can
|
|
113
|
+
widen what the profile covers, and nobody reading the initializer would know.
|
|
114
|
+
|
|
115
|
+
The test is the name, not the count. `config.record_request_evidence_by_default = true` does
|
|
116
|
+
enable three categories at once, and it is fine, because the line tells a reviewer what those
|
|
117
|
+
three are and the gem may never widen them: the switch is defined as the IP address, the
|
|
118
|
+
browser user agent, and a coarse country/region/city estimate, and a future release that
|
|
119
|
+
wanted to add coordinates would have to change its name. A `precision: :full` or a
|
|
120
|
+
`level: :enhanced` fails the test for the opposite reason — nobody reading it can say what
|
|
121
|
+
came back.
|
|
114
122
|
|
|
115
123
|
```ruby
|
|
116
124
|
# Before
|
|
@@ -249,7 +257,7 @@ banned for a specific reason, not for taste.
|
|
|
249
257
|
|---|---|
|
|
250
258
|
| `:network`, `:full`, `:enhanced`, `:forensic`, `:maximum` | They hide what will be collected behind a word that sounds like a quality level |
|
|
251
259
|
| `record_location` | A developer could reasonably read it as GPS or physical location. It is neither |
|
|
252
|
-
| `request_evidence: :network`, `track_everything`, `record_everything` |
|
|
260
|
+
| `request_evidence: :network`, `track_everything`, `record_everything` | Their names do not name their contents. See rule 6, and contrast `record_request_evidence_by_default`, which does |
|
|
253
261
|
| `maximum_evidence`, `full_evidence`, `legal_proof: true` | They imply a verdict the gem cannot reach, and they enable data as a side effect |
|
|
254
262
|
| An opaque privacy-profile switch keyed to a regulation | No runtime flag can make a legal determination on anyone's behalf, and the name would be the least accurate string in the codebase |
|
|
255
263
|
| `include_sensitive_context: true` | One flag turning on three categories of personal data makes an operator's intent unreviewable. Use `include_ip_address:`, `include_browser_user_agent:`, `include_ip_geolocation:` |
|
|
@@ -283,7 +291,7 @@ object.
|
|
|
283
291
|
| `chain_event_history_with`, `anchor_event_history_with`, `timestamp_receipts_with` | one `integrity_level` setting | Three different mechanisms making three different claims. One setting would let a reader infer the strongest from the presence of the weakest |
|
|
284
292
|
| `after_event_is_committed` | `after_commit` | Says which commit, and reads as a sentence |
|
|
285
293
|
| `authorize_unredacted_request_evidence_access_with` | `access_control` | Long, and correct. It names exactly which access it authorizes |
|
|
286
|
-
| `deliberately_store_request_evidence_unencrypted
|
|
294
|
+
| `deliberately_store_request_evidence_unencrypted!` | `encryption: false` | Turning encryption off should be a sentence a reviewer can find in a diff — not a `false`. The method NAME is the ceremony; its `because:` is optional |
|
|
287
295
|
|
|
288
296
|
The last row is the pattern worth copying. When an option has a consequence somebody should
|
|
289
297
|
have to think about, make the name carry the thinking.
|