clickwrap 0.0.0 → 0.1.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.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +90 -0
  3. data/CHANGELOG.md +612 -0
  4. data/README.md +830 -1204
  5. data/SECURITY.md +33 -0
  6. data/app/assets/stylesheets/clickwrap.css +241 -0
  7. data/app/controllers/clickwrap/application_controller.rb +79 -0
  8. data/app/controllers/clickwrap/captures_controller.rb +145 -0
  9. data/app/controllers/clickwrap/document_versions_controller.rb +71 -0
  10. data/app/controllers/clickwrap/receipts_controller.rb +115 -0
  11. data/app/controllers/clickwrap/withdrawals_controller.rb +60 -0
  12. data/app/helpers/clickwrap/engine_helper.rb +97 -0
  13. data/app/views/clickwrap/captures/show.html.erb +34 -0
  14. data/app/views/clickwrap/receipts/index.html.erb +38 -0
  15. data/app/views/clickwrap/receipts/show.html.erb +91 -0
  16. data/app/views/clickwrap/shared/_error_summary.html.erb +39 -0
  17. data/app/views/clickwrap/shared/_fields.html.erb +100 -0
  18. data/app/views/clickwrap/shared/_statement.html.erb +105 -0
  19. data/app/views/clickwrap/withdrawals/new.html.erb +30 -0
  20. data/config/locales/en.yml +160 -0
  21. data/config/locales/es.yml +138 -0
  22. data/config/routes.rb +41 -0
  23. data/exe/clickwrap +374 -0
  24. data/guides/README.md +30 -0
  25. data/guides/accessibility.md +249 -0
  26. data/guides/consent-and-lifecycle.md +299 -0
  27. data/guides/integrating.md +610 -0
  28. data/guides/integrity.md +212 -0
  29. data/guides/migrating.md +335 -0
  30. data/guides/naming.md +320 -0
  31. data/guides/organizations.md +320 -0
  32. data/guides/receipts-and-verification.md +415 -0
  33. data/guides/request-evidence.md +512 -0
  34. data/guides/retention-and-legal-holds.md +438 -0
  35. data/lib/clickwrap/actor_proxy.rb +147 -0
  36. data/lib/clickwrap/anonymous_actor.rb +47 -0
  37. data/lib/clickwrap/authority.rb +174 -0
  38. data/lib/clickwrap/canonical_json.rb +216 -0
  39. data/lib/clickwrap/capture/event_builder.rb +220 -0
  40. data/lib/clickwrap/capture/presentation_verifier.rb +521 -0
  41. data/lib/clickwrap/capture.rb +650 -0
  42. data/lib/clickwrap/configuration.rb +1129 -0
  43. data/lib/clickwrap/controller_helpers.rb +758 -0
  44. data/lib/clickwrap/current_state.rb +282 -0
  45. data/lib/clickwrap/digest.rb +125 -0
  46. data/lib/clickwrap/doctor.rb +418 -0
  47. data/lib/clickwrap/document_definition.rb +255 -0
  48. data/lib/clickwrap/document_renderer.rb +83 -0
  49. data/lib/clickwrap/document_renderers/markdown.rb +175 -0
  50. data/lib/clickwrap/document_renderers/markdown_rails.rb +126 -0
  51. data/lib/clickwrap/dsl/policy_builder.rb +462 -0
  52. data/lib/clickwrap/dsl/retention_builder.rb +89 -0
  53. data/lib/clickwrap/durable_commit_callback.rb +37 -0
  54. data/lib/clickwrap/engine.rb +184 -0
  55. data/lib/clickwrap/errors.rb +181 -0
  56. data/lib/clickwrap/form_builder_extensions.rb +341 -0
  57. data/lib/clickwrap/front_matter.rb +67 -0
  58. data/lib/clickwrap/identifier.rb +112 -0
  59. data/lib/clickwrap/import/external_receipt.rb +241 -0
  60. data/lib/clickwrap/import/fine_print.rb +290 -0
  61. data/lib/clickwrap/import/legacy.rb +450 -0
  62. data/lib/clickwrap/integrations/organizations_authority.rb +81 -0
  63. data/lib/clickwrap/integrity/anchor.rb +130 -0
  64. data/lib/clickwrap/integrity/attestation_reconciler.rb +114 -0
  65. data/lib/clickwrap/integrity/attestor.rb +221 -0
  66. data/lib/clickwrap/integrity/chain.rb +313 -0
  67. data/lib/clickwrap/integrity/timestamp.rb +143 -0
  68. data/lib/clickwrap/ip_geolocation/location.rb +112 -0
  69. data/lib/clickwrap/ip_geolocation/null_resolver.rb +35 -0
  70. data/lib/clickwrap/ip_geolocation/resolver.rb +97 -0
  71. data/lib/clickwrap/ip_geolocation/static_resolver.rb +107 -0
  72. data/lib/clickwrap/ip_geolocation/trackdown_resolver.rb +330 -0
  73. data/lib/clickwrap/ip_geolocation.rb +16 -0
  74. data/lib/clickwrap/lifecycle.rb +534 -0
  75. data/lib/clickwrap/linter.rb +382 -0
  76. data/lib/clickwrap/localized_text.rb +101 -0
  77. data/lib/clickwrap/macros.rb +203 -0
  78. data/lib/clickwrap/models/application_record.rb +20 -0
  79. data/lib/clickwrap/models/chain_head.rb +79 -0
  80. data/lib/clickwrap/models/concerns/has_clickwraps.rb +55 -0
  81. data/lib/clickwrap/models/disposition_plan.rb +208 -0
  82. data/lib/clickwrap/models/document.rb +46 -0
  83. data/lib/clickwrap/models/document_version.rb +163 -0
  84. data/lib/clickwrap/models/event.rb +743 -0
  85. data/lib/clickwrap/models/event_document.rb +79 -0
  86. data/lib/clickwrap/models/event_statement.rb +92 -0
  87. data/lib/clickwrap/models/external_action.rb +150 -0
  88. data/lib/clickwrap/models/integrity_attestation.rb +90 -0
  89. data/lib/clickwrap/models/legal_hold.rb +81 -0
  90. data/lib/clickwrap/models/policy_revision.rb +115 -0
  91. data/lib/clickwrap/models/presentation.rb +59 -0
  92. data/lib/clickwrap/models/receipt_access.rb +53 -0
  93. data/lib/clickwrap/models/recording_sequence.rb +21 -0
  94. data/lib/clickwrap/models/request_evidence.rb +378 -0
  95. data/lib/clickwrap/models/statement_identity_lock.rb +38 -0
  96. data/lib/clickwrap/models/statement_state.rb +130 -0
  97. data/lib/clickwrap/pending_receipt.rb +177 -0
  98. data/lib/clickwrap/policy.rb +283 -0
  99. data/lib/clickwrap/presentation_manifest.rb +210 -0
  100. data/lib/clickwrap/presenter.rb +716 -0
  101. data/lib/clickwrap/privacy.rb +419 -0
  102. data/lib/clickwrap/protected_outcome.rb +120 -0
  103. data/lib/clickwrap/receipt.rb +606 -0
  104. data/lib/clickwrap/receipt_html.rb +235 -0
  105. data/lib/clickwrap/receipt_verifier.rb +978 -0
  106. data/lib/clickwrap/reference.rb +44 -0
  107. data/lib/clickwrap/registration.rb +236 -0
  108. data/lib/clickwrap/registry.rb +54 -0
  109. data/lib/clickwrap/remediation_token.rb +155 -0
  110. data/lib/clickwrap/request_evidence_extractor.rb +590 -0
  111. data/lib/clickwrap/request_evidence_policy.rb +261 -0
  112. data/lib/clickwrap/retention/applier.rb +231 -0
  113. data/lib/clickwrap/retention/disposition.rb +221 -0
  114. data/lib/clickwrap/retention/planner.rb +502 -0
  115. data/lib/clickwrap/retention_class.rb +97 -0
  116. data/lib/clickwrap/reviewed_text.rb +28 -0
  117. data/lib/clickwrap/schema_requirements.rb +196 -0
  118. data/lib/clickwrap/services/authorize_external_action.rb +149 -0
  119. data/lib/clickwrap/services/load_policies.rb +69 -0
  120. data/lib/clickwrap/services/publish_documents.rb +251 -0
  121. data/lib/clickwrap/services/validate_policy_references.rb +166 -0
  122. data/lib/clickwrap/statement.rb +248 -0
  123. data/lib/clickwrap/subject_fingerprint.rb +28 -0
  124. data/lib/clickwrap/submission.rb +169 -0
  125. data/lib/clickwrap/system_actor.rb +31 -0
  126. data/lib/clickwrap/test_helpers.rb +676 -0
  127. data/lib/clickwrap/testing.rb +211 -0
  128. data/lib/clickwrap/trusted_proxy_configuration.rb +92 -0
  129. data/lib/clickwrap/verification.rb +504 -0
  130. data/lib/clickwrap/version.rb +12 -1
  131. data/lib/clickwrap/view_helpers.rb +190 -0
  132. data/lib/clickwrap/vocabulary.rb +294 -0
  133. data/lib/clickwrap.rb +497 -7
  134. data/lib/generators/clickwrap/document_generator.rb +164 -0
  135. data/lib/generators/clickwrap/hardening_generator.rb +177 -0
  136. data/lib/generators/clickwrap/install_generator.rb +1287 -0
  137. data/lib/generators/clickwrap/link_generator.rb +56 -0
  138. data/lib/generators/clickwrap/policy_generator.rb +118 -0
  139. data/lib/generators/clickwrap/templates/clickwrap_hardening.rb.erb +256 -0
  140. data/lib/generators/clickwrap/templates/clickwrap_policies.rb.erb +192 -0
  141. data/lib/generators/clickwrap/templates/create_clickwrap_external_action_tables.rb.erb +128 -0
  142. data/lib/generators/clickwrap/templates/create_clickwrap_integrity_tables.rb.erb +157 -0
  143. data/lib/generators/clickwrap/templates/create_clickwrap_presentation_tables.rb.erb +160 -0
  144. data/lib/generators/clickwrap/templates/create_clickwrap_request_evidence_tables.rb.erb +180 -0
  145. data/lib/generators/clickwrap/templates/create_clickwrap_retention_tables.rb.erb +174 -0
  146. data/lib/generators/clickwrap/templates/create_clickwrap_tables.rb.erb +568 -0
  147. data/lib/generators/clickwrap/templates/initializer.rb.erb +439 -0
  148. data/lib/generators/clickwrap/templates/link_clickwrap_event_migration.rb.erb +12 -0
  149. data/lib/generators/clickwrap/templates/policy.rb.erb +31 -0
  150. data/lib/generators/clickwrap/templates/policy_test.rb.erb +56 -0
  151. data/lib/generators/clickwrap/templates/privacy.md.erb +58 -0
  152. data/lib/generators/clickwrap/templates/terms.md.erb +49 -0
  153. data/lib/generators/clickwrap/upgrade_generator.rb +50 -0
  154. data/lib/generators/clickwrap/views_generator.rb +101 -0
  155. data/lib/tasks/clickwrap.rake +569 -0
  156. metadata +311 -16
data/README.md CHANGED
@@ -1,571 +1,553 @@
1
- # ☑️ `clickwrap` trustworthy agreements, consent, declarations, and authorizations for Rails
1
+ # ☑️ `clickwrap` - Make your Rails users accept your Terms and legal documents
2
2
 
3
- > [!IMPORTANT]
4
- > **README-first product contract.** `clickwrap` is not implemented or published yet. This README deliberately describes the finished gem we intend to build so we can work backward from the ideal developer experience. Every public promise below is an acceptance criterion, not a claim about code that exists today. Remove this notice only after the implementation and proof integrations satisfy it.
3
+ [![Gem Version](https://badge.fury.io/rb/clickwrap.svg)](https://badge.fury.io/rb/clickwrap) [![Build Status](https://github.com/rameerez/clickwrap/workflows/Tests/badge.svg)](https://github.com/rameerez/clickwrap/actions)
4
+
5
+ > [!TIP]
6
+ > **🚀 Ship your next Rails app 10x faster!** I've built **[RailsFast](https://railsfast.com/?ref=clickwrap)**, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks — including versioned Terms and Privacy Notice acceptance powered by this gem. Go [check it out](https://railsfast.com/?ref=clickwrap)!
5
7
 
6
- `clickwrap` is the missing evidence-and-assent layer for Rails.
8
+ `clickwrap` makes your Rails users accept your Terms of Service, acknowledge your Privacy Notice, give (and withdraw) consent, make declarations, and authorize one-time actions and keeps evidence of all of it that you can still reproduce and verify years later.
7
9
 
8
- It makes ordinary Terms acceptance and its action one beautiful form-builder call:
10
+ Perfect for SaaS signups, marketplaces, fintech payouts, health apps, and any Rails app where "the user agreed to this" needs to be provable long after the fact.
11
+
12
+ Ordinary Terms acceptance is one line in your signup form:
9
13
 
10
14
  ```erb
11
15
  <%= form.clickwrap :signup, submit: "Create account" %>
12
16
  ```
13
17
 
14
- And it grows with you all the way to expiring declarations, withdrawable consent, one-time authorizations, exact historical receipts, transaction-bound evidence, retention, legal holds, and independently verifiable exports—without making the simple path feel complicated.
18
+ …and it renders one line on the page. One checkbox, one sentence, your legal pages linked inside it:
19
+
20
+ > ☐ I agree to the [Terms of Service](#) and I acknowledge the [Privacy Policy](#).
21
+
22
+ No "Required" flag, no "(opens in a new tab)" printed beside every link, no version label sitting under a checkbox. Behind that single control, the receipt still records two separate acts — an *agreement* to the Terms and an *acknowledgment* of the Privacy Notice — with their own versions, digests, and lifecycles.
23
+
24
+ And when an action is consequential enough that it must never happen without its evidence (a payout, a data handoff, a contract), the evidence and the action commit in the same database transaction:
15
25
 
16
26
  ```ruby
17
- receipt = Clickwrap.capture_and!(
18
- :withdrawal_authorization,
19
- actor: current_user,
20
- subject: withdrawal,
21
- http_request: request,
22
- submission: clickwrap_submission
23
- ) do |pending_receipt|
27
+ Clickwrap.capture_and!(:withdrawal_authorization, actor: current_user, subject: withdrawal,
28
+ http_request: request, submission: clickwrap_submission) do |pending_receipt|
24
29
  withdrawal.submit!(authorized_by_clickwrap_event: pending_receipt.event_id)
25
30
  end
26
31
  ```
27
32
 
28
- If evidence cannot be recorded, the protected database action does not happen. If the action fails, the evidence does not pretend it succeeded.
29
-
30
- No JavaScript package. No Redis. No external account. No legal-document vendor. No required per-event API call. No required background job. Just Rails, your database, and an API that reads like plain English.
33
+ If the evidence can't be recorded, the action doesn't happen. If the action fails, the evidence doesn't pretend it succeeded.
31
34
 
32
- > [!TIP]
33
- > **Building a new Rails product?** [RailsFast](https://railsfast.com/?ref=clickwrap) ships the conventional signup integration, so new applications start with versioned Terms, a distinct Privacy Notice acknowledgment, atomic evidence, and receipts instead of inventing an `accepted_terms_at` column.
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.
34
36
 
35
- ## The five-minute version
37
+ > [!IMPORTANT]
38
+ > **Status: built and tested, not yet proven in production.** Everything in this README is implemented and covered by the test suite, but the gem hasn't been through its planned production integrations, an unfamiliar-developer usability test, or legal review of its default wording yet. Treat it as a release candidate for evaluation — don't put it under a payout flow just yet. The [stability promise](#stability-and-upgrade-promise) applies from 0.1.0 onward.
36
39
 
37
- Install it:
40
+ ## 👨‍💻 Example
38
41
 
39
- ```bash
40
- bundle add clickwrap
41
- bin/rails generate clickwrap:install
42
- bin/rails db:migrate
43
- ```
42
+ Define your documents and a policy in plain Ruby. Each document points at the file that *is* your legal text — and that file names its own version, in the front matter it probably already has. The top of `app/content/legal/terms.md`:
44
43
 
45
- The installer detects Rails authentication versus Devise, integer versus UUID primary keys, and the database adapter. It generates adaptive migrations, one annotated initializer, a conventional signup policy, and the correct explicit authentication integration. It never invents legal text or silently guesses an ambiguous actor model.
44
+ ```markdown
45
+ ---
46
+ title: Terms of Service
47
+ last_updated: 2026-08-15
48
+ ---
46
49
 
47
- Point the generated policy at the exact documents your application already owns:
50
+ # Terms of Service
51
+ ```
48
52
 
49
53
  ```ruby
54
+ # clickwrap-doc-test: syntax-only — terms.md and privacy.md are files in your app
50
55
  # config/clickwrap.rb
51
56
  Clickwrap.document :terms,
52
- version: "2026-08-15",
53
- from: Rails.root.join("app/content/legal/terms.md")
57
+ from: Rails.root.join("app/content/legal/terms.md"),
58
+ link: "/legal/terms"
54
59
 
55
60
  Clickwrap.document :privacy_notice,
56
- version: "2026-08-15",
57
- from: Rails.root.join("app/content/legal/privacy.md")
58
-
59
- Clickwrap.policy :signup do
60
- agree_to :terms
61
- acknowledge :privacy_notice
62
- end
63
- ```
64
-
65
- Tell Clickwrap which records can act:
66
-
67
- ```ruby
68
- # app/models/user.rb
69
- class User < ApplicationRecord
70
- has_clickwraps
71
- end
61
+ from: Rails.root.join("app/content/legal/privacy.md"),
62
+ link: "/legal/privacy"
72
63
  ```
73
64
 
74
- Render the policy and its bound submit action:
75
-
76
- ```erb
77
- <%= form_with model: resource do |form| %>
78
- <%# email, password, etc. %>
79
-
80
- <%= form.clickwrap :signup, submit: "Create account" %>
81
- <% end %>
82
- ```
65
+ `from:` is the bytes Clickwrap freezes, digests, and keeps as evidence. `link:` is where a *person* reads them — your own formatted page, with your typography and your navigation — and it is the path Clickwrap both renders and signs, so the receipt never cites a different target from the link somebody pressed. Leave `link:` off and the sentence links to the engine's rendering of the exact published version instead. (Your page shows whatever is current; that trade is yours to make, and it is written down in the declaration where a reviewer will see it.)
83
66
 
84
- Publish immutable snapshots and boot the app:
67
+ Changing your Terms is then one edit in one file: new words, new `last_updated:`, publish. There is no second copy of the version label anywhere to drift — and a file carrying neither `clickwrap_version:` nor `last_updated:` fails the boot with a sentence instead of getting a label Clickwrap invented. Sources that can't carry front matter still name their label the explicit way:
85
68
 
86
- ```bash
87
- bin/rails clickwrap:publish
69
+ ```ruby
70
+ Clickwrap.document :handbook,
71
+ version: "2026-08-15",
72
+ from: Rails.root.join("app/content/legal/handbook.pdf")
88
73
  ```
89
74
 
90
- That is the whole conventional integration. The helper renders the initially unselected controls and the submit button as one presentation, so the exact call to action in the signed manifest is the one the user can press. The generated Rails-authentication or Devise adapter saves the account and required evidence in one database transaction.
91
-
92
- At first render there is no persisted user yet. Clickwrap does not pretend otherwise: it binds the presentation to a short-lived prospective-actor registration flow, then the authentication adapter binds the resulting account to that presentation inside the same transaction. The receipt identifies the attribution method as account registration, not an authenticated session.
75
+ #### Reading that front matter yourself: `Clickwrap::FrontMatter`
93
76
 
94
- From that moment on:
77
+ Your own pages usually need the same two answers, and it is the same block, so use the same reader rather than writing a third one:
95
78
 
96
79
  ```ruby
97
- user.clickwraps.agreed_to?(:terms) # => true
98
- user.clickwraps.acknowledged?(:privacy_notice) # => true
99
- user.clickwraps.current_for?(:signup) # => true
100
-
101
- receipt = user.clickwraps.receipts.last
102
- receipt.event_id # => "01K2..."
103
- receipt.verify.success? # => true
104
- receipt.to_canonical_json
105
- receipt.to_html
80
+ Clickwrap::FrontMatter.version_label_in(File.read(path)) # => "2026-08-15", or nil
81
+ Clickwrap::FrontMatter.strip(File.read(path)) # the body, without the block
106
82
  ```
107
83
 
108
- Clickwrap preserves the exact document bytes and digests, policy revision, assertion and link text, choices, submit-button text, locale, presentation manifest, actor, authentication context, server time, lifecycle, and resulting protected action. Optional request evidence stays off until you explicitly ask for it.
109
-
110
- Everything below is depth, not setup tax.
84
+ It reads a leading `---` block closed by `---` or `...`, takes simple top-level `key: value` lines only, and answers with `clickwrap_version:` when present, `last_updated:` otherwise — a same-day correction that still changes bytes needs a fresh label while the date readers see stays put. Two details are exactly where hand-rolled readers diverge, so they are worth naming: a quoted value has its quotes removed, and an unquoted trailing YAML comment is not part of the value, so `last_updated: 2026-11-01 # was 2026-08-15` is the label `2026-11-01`, precisely as YAML reads it.
111
85
 
112
- If you came for one particular job:
86
+ `strip` removes the block from the *rendered* representation only. The source digest still covers the exact file bytes, front matter included, because that is what the file was.
113
87
 
114
- - start with [the form helper](#the-form-helper) for ordinary Rails forms;
115
- - use [`capture_and!`](#capture-evidence-and-the-protected-action-together) for consequential same-database actions;
116
- - read [consent](#consent-that-can-actually-be-withdrawn), [declarations](#expiring-and-corrected-declarations), or [one-time authorization](#narrow-one-time-authorizations) for richer lifecycles;
117
- - configure [optional request evidence](#optional-request-evidence-private-by-default) only after reading its privacy boundaries;
118
- - use [receipts](#receipts-answer-show-me-exactly-what-happened), [retention](#retention-deletion-and-legal-holds-are-first-class), and [integrity tiers](#progressive-honest-integrity) when the audit trail matters; or
119
- - jump to [the complete initializer](#the-generated-initializer-explains-itself) to see every default together.
120
-
121
- ---
88
+ Then say how long the evidence lives and what the server offers:
122
89
 
123
- ## Why this gem exists
124
-
125
- A checkbox is easy. Answering these questions three years later is not:
126
-
127
- - Which exact version did this person agree to?
128
- - What did the page actually say beside the control and submit button?
129
- - Was the checkbox initially empty and required on the server?
130
- - Did the account, payout, declaration, or provider handoff succeed without its evidence?
131
- - Was this consent later withdrawn?
132
- - Had this declaration expired?
133
- - Did this authorization cover this exact transaction, or was it replayed for another one?
134
- - Can an auditor reproduce the document without checking out historical application code?
135
- - Can optional personal request evidence be deleted without rewriting the historical event?
136
- - Can the exported receipt still be verified after several gem and Rails upgrades?
90
+ ```ruby
91
+ Clickwrap.retention :ordinary_agreement_evidence do
92
+ retain_core_event_for 6.years
93
+ end
137
94
 
138
- Most applications eventually accumulate some combination of:
95
+ Clickwrap.policy :signup do
96
+ agree_to :terms
97
+ acknowledge :privacy_notice
139
98
 
140
- ```text
141
- accepted_terms_at
142
- terms_version
143
- an audit log
144
- a few hidden form fields
145
- an after_create callback
146
- some IP-address columns
147
- several domain-specific "confirmed_at" timestamps
99
+ retain_with :ordinary_agreement_evidence
100
+ end
148
101
  ```
149
102
 
150
- Each part looks reasonable alone. Together they produce partial writes, client-owned policy decisions, mutable history, confused consent semantics, and evidence that only the original engineer can explain.
103
+ (Yes, the payload-retention decision is mandatory `clickwrap` will not silently
104
+ default captured evidence or request evidence to "keep forever" and will not pick
105
+ a period for you. A minimal, digest-linked disposition tombstone remains after a
106
+ reviewed core deletion so the deletion itself does not become an unexplained hole.)
151
107
 
152
- `clickwrap` turns that recurring plumbing into one coherent Rails primitive:
108
+ Add one macro to your model:
153
109
 
154
- ```text
155
- immutable document
156
- +
157
- server-owned policy
158
- +
159
- exact presentation
160
- +
161
- explicit actor action
162
- +
163
- atomic protected outcome
164
- +
165
- append-only lifecycle
166
- =
167
- reproducible receipt
110
+ ```ruby
111
+ class User < ApplicationRecord
112
+ has_clickwraps
113
+ end
168
114
  ```
169
115
 
170
- It is intentionally not a “one checkbox makes anything legal” gem. It provides excellent evidence mechanics. Your application and counsel still own the words, lawful basis, fairness, capacity, authority, jurisdiction, formalities, and retention decisions.
171
-
172
- ## Six verbs, six honest meanings
116
+ Render the line and the submit button as one bound presentation:
173
117
 
174
- Not every checkbox is “consent,” and not every timestamp is a “signature.” Clickwrap gives each act the lifecycle it actually needs:
118
+ ```erb
119
+ <%= form.clickwrap :signup, submit: "Create account" %>
120
+ ```
175
121
 
176
- | Policy verb | Evidence kind | Meaning | Typical lifecycle |
177
- |---|---|---|---|
178
- | `agree_to` | `agreement` | Assent to contractual terms | agreed → superseded/new version |
179
- | `acknowledge` | `acknowledgment` | Affirmative receipt or awareness of a notice/risk | acknowledged → superseded/expired |
180
- | `consent_to` | `consent` | Purpose-specific permission where consent is the host’s chosen basis | granted → withdrawn/renewed/scope changed |
181
- | `declare` | `declaration` | A factual statement made by the actor | declared → corrected/superseded/expired |
182
- | `attest` | `attestation` | An operational fact affirmed by an authorized actor | attested → corrected/superseded |
183
- | `authorize` | `authorization` | Narrow permission bound to a protected action | authorized → consumed/revoked/expired |
122
+ > I agree to the [Terms of Service](#) and I acknowledge the [Privacy Policy](#).
184
123
 
185
- The DSL is intentionally verbal:
124
+ From that moment on, you can ask readable questions everywhere:
186
125
 
187
126
  ```ruby
188
- Clickwrap.policy :example do
189
- agree_to :terms
190
- acknowledge :privacy_notice
191
- consent_to :product_updates, optional: true
192
- declare :information_is_accurate
193
- attest :bank_transfer_was_accepted
194
- authorize :withdrawal, one_time: true, valid_for: 10.minutes
195
- end
127
+ user.clickwraps.agreed_to?(:terms) # => true
128
+ user.clickwraps.acknowledged?(:privacy_notice) # => true
129
+ user.clickwraps.current_for?(:signup) # => true
196
130
  ```
197
131
 
198
- The policy compiler rejects incoherent combinations at boot. A one-time authorization cannot be indefinite. Consent needs a withdrawal path. A declaration can expire without pretending the original statement was false. Withdrawing future consent never rewrites a historical agreement.
132
+ And every acceptance produces a receipt you can export and verify even outside your app, without your app's source code:
199
133
 
200
- This taxonomy is product design, not statutory vocabulary. The host chooses the correct kind with appropriate legal/product review.
134
+ ```ruby
135
+ receipt = user.clickwraps.receipts.last
136
+ receipt.verify.success? # => true
137
+ receipt.to_canonical_json # canonical JSON for the standalone verifier
138
+ receipt.to_html # human-readable version of the same evidence
139
+ ```
201
140
 
202
- One submitted policy produces one root evidence event and one receipt, even when the policy contains several acts. Each act keeps its own kind, statement, documents, answer, and lifecycle under that root event. That gives the protected domain action one stable `event_id` to reference without flattening “agreed to Terms” and “acknowledged the Privacy Notice” into the same meaning.
141
+ Sounds good? Let's get started!
203
142
 
204
- ## Documents are immutable, reproducible records
143
+ ## Quick start
205
144
 
206
- Define a logical document once and publish as many immutable versions and locales as needed:
145
+ Add the gem and run the installer:
207
146
 
208
147
  ```ruby
209
- Clickwrap.document :terms,
210
- version: "2026-08-15",
211
- locale: :en,
212
- effective_at: Time.utc(2026, 8, 15),
213
- from: Rails.root.join("app/content/legal/terms.en.md")
214
-
215
- Clickwrap.document :terms,
216
- version: "2026-08-15",
217
- locale: :es,
218
- effective_at: Time.utc(2026, 8, 15),
219
- from: Rails.root.join("app/content/legal/terms.es.md")
148
+ # Gemfile
149
+ gem "clickwrap", github: "rameerez/clickwrap"
220
150
  ```
221
151
 
222
- Publish them during development or deployment:
223
-
224
152
  ```bash
225
- bin/rails clickwrap:publish
153
+ bundle install
154
+ bin/rails generate clickwrap:install
155
+ bin/rails db:migrate
226
156
  ```
227
157
 
228
- Publishing:
229
-
230
- - reads the exact bytes;
231
- - records media type and locale;
232
- - calculates a versioned digest;
233
- - snapshots the exact rendered representation when a source format is transformed for display;
234
- - records the renderer and sanitizer identity/version used for that representation;
235
- - freezes a database snapshot;
236
- - compiles and freezes every policy revision that references it; and
237
- - refuses to reuse a version label for different bytes.
158
+ `bundle add clickwrap` would install version 0.0.0, a deliberately empty name placeholder on RubyGems — install from GitHub until the first real version is published there.
238
159
 
239
- The task is idempotent. A changed document requires a new version. Export never fetches a mutable live URL and calls it historical evidence.
240
-
241
- Preview the plan without writing:
160
+ 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:
242
161
 
243
162
  ```bash
244
- bin/rails clickwrap:publish:plan
245
- ```
246
-
247
- The default database store is deliberately boring and complete. Larger applications can switch document bodies to content-addressed Active Storage or object-lock storage while keeping the same digest and receipt contract:
248
-
249
- ```ruby
250
- config.store_document_contents_in = :active_storage
163
+ bin/rails generate clickwrap:install --with-request-evidence # the IP / user-agent / geolocation annex
164
+ --with-integrity # event chaining, anchoring, timestamps
165
+ --with-retention-ops # legal holds and disposition plans
166
+ --with-external-actions # the outbox for external handoffs
167
+ --with-persisted-presentations
251
168
  ```
252
169
 
253
- Every storage adapter must return immutable bytes plus a verifiable digest. A URL alone is never a document version.
170
+ Add any of them later by re-running the generator with the flag. Turning a capability on without its migration is caught at boot, by `bin/rails clickwrap:doctor`, and at the call itself — always with the exact command that fixes it. Enabling a request-evidence field brings the annex table automatically, because an installation that records IP addresses into a table it never created is not a schema choice. If your legal pages already live in the app, it points `from:` at those exact files and writes no `version:` line — the pages name their own versions. It never invents legal text and never silently guesses your actor model.
254
171
 
255
- Markdown, HTML, plain text, and attached files are evidence inputs, not trusted markup by accident. The reference renderer sanitizes display HTML. A custom renderer must return the exact rendered bytes it offered, and Clickwrap stores their digest alongside the original-source digest. That preserves the distinction between “this Markdown file existed” and “this rendered representation was offered.”
256
-
257
- ## Policies are server-owned offers
258
-
259
- A policy declares what the server will present and accept. The browser may answer; it may never choose the policy, document version, validity, subject, retention, or request-evidence fields.
260
-
261
- ```ruby
262
- Clickwrap.policy :driver_declaration do
263
- declare :non_professional_driver,
264
- document: :driver_declaration,
265
- statement: "I declare that I drive privately and not as a professional driver.",
266
- valid_for: 1.year,
267
- subject_fingerprint_with: ->(scheme) { scheme.evidence_fingerprint }
172
+ Point the generated policy at the documents your app already owns (see the example above), add `has_clickwraps` to your user model, and drop `form.clickwrap` into your signup form:
268
173
 
269
- retain_with :regulated_evidence
270
- end
174
+ ```erb
175
+ <%= form.clickwrap :signup, submit: "Create account" %>
271
176
  ```
272
177
 
273
- Policies compile at boot. Clickwrap fails loudly for:
274
-
275
- - missing documents or locales;
276
- - duplicate statement keys;
277
- - invalid lifecycle options;
278
- - a consent policy without a configured withdrawal path;
279
- - a one-time authorization without expiry/consumption behavior;
280
- - request evidence without a named present purpose and retention decision;
281
- - a subject-bound policy without a subject fingerprint; or
282
- - a changed compiled policy reusing the same revision.
283
-
284
- Policy revisions are defined pleasantly in Ruby and persisted as frozen canonical snapshots. Historical receipts do not need current source code to explain what revision meant.
285
-
286
- Every human-facing value can be a literal, an I18n key, or a locale map. Clickwrap resolves it before presentation, fails closed when a required translation is missing, and stores the resolved text and locale—not merely an I18n key whose meaning may change later.
287
-
288
- ### Reacceptance is explicit
289
-
290
- New document bytes do not silently reinterpret old evidence:
178
+ Then wire the door that creates the account, because the form is only half the circuit — some line has to write the account and its evidence in the same transaction:
291
179
 
292
180
  ```ruby
293
- Clickwrap.policy :current_terms do
294
- agree_to :terms, require_current_version: true
181
+ # Devise — app/controllers/users/registrations_controller.rb
182
+ class Users::RegistrationsController < Devise::RegistrationsController
183
+ clickwraps_registration_with :signup
295
184
  end
296
185
  ```
297
186
 
298
187
  ```ruby
299
- Clickwrap.required?(:current_terms, actor: user) # => true after a new version publishes
300
- user.clickwraps.current_for?(:current_terms) # => false
188
+ # Rails authentication, an OAuth finish screen, a service object — any door
189
+ # that builds the record itself.
190
+ unless register_with_clickwrap(:signup, user: @user) { @user.save! }
191
+ return render :new, status: :unprocessable_entity
192
+ end
301
193
  ```
302
194
 
303
- The application decides which change is material. Clickwrap enforces the rule it is given; it does not decide legal materiality.
195
+ Do not skip that step. Leave it out and everything still *looks* right — the checkbox renders, the person ticks it, the account is created and there is no evidence at all. It is the one omission this gem cannot warn you about at runtime, because an app with no door simply never calls it.
304
196
 
305
- Before activating a new required version, operators can preview its effect:
197
+ Finally, publish immutable snapshots of your documents:
306
198
 
307
199
  ```bash
308
- bin/rails clickwrap:reacceptance:plan POLICY=current_terms
200
+ bin/rails clickwrap:publish
309
201
  ```
310
202
 
311
- The plan reports affected actor counts and configured remediation routes without emailing anyone, changing current state, or calling the change “material.” Scheduled versions become presentable only at their explicit `effective_at`; correcting a published mistake means publishing a new version or stopping future presentation with an append-only operator reason, never replacing historical bytes.
203
+ That's the only time you run that by hand: publishing rides `db:prepare`, so a deploy that runs
204
+ it also freezes the snapshots for whatever you declared, before the server takes traffic
205
+ (`config.publish_documents_after_database_preparation = false` if you'd rather own the step).
312
206
 
313
- ## Presentation manifests stop render-to-submit substitution
207
+ That's it! Your app now records which exact document versions the server offered, which explicit
208
+ answers it accepted, the bound presentation wording, and when—atomically with account creation.
209
+ Let's see how it works.
314
210
 
315
- `form.clickwrap` does more than render controls. It creates a short-lived presentation manifest bound to an actor or prospective-actor flow, subject, and tenant containing:
211
+ ### What that one line renders
316
212
 
317
- - policy key and frozen revision;
318
- - document versions, locales, and digests;
319
- - exact statements, labels, link labels/targets, choices, required state, and CTA text;
320
- - actor, tenant, and subject bindings;
321
- - subject fingerprint;
322
- - template, application, and gem versions;
323
- - capture channel;
324
- - issue time, expiry, and one-use nonce; and
325
- - a canonical manifest digest.
213
+ One line:
326
214
 
327
- The browser receives a signed presentation token. On submit, Clickwrap verifies it against current server policy and rejects stale, swapped, expired, cross-account, cross-tenant, or cross-subject tokens.
215
+ > I agree to the [Terms of Service](#) and I acknowledge the [Privacy Policy](#).
328
216
 
329
- A deploy between GET and POST never causes the server to record a version the actor was not offered. The policy either honors that still-valid presentation or asks the user to review the new one.
217
+ One checkbox, one label, one sentence, with the documents linked *inside* it. The label **is** the
218
+ line, so pressing the words toggles the control and a screen reader announces the sentence and the
219
+ box together. There is no "Required" flag, no "(opens in a new tab)" printed beside every link, and
220
+ no version label under the checkbox. (The `required` attribute is still there as progressive
221
+ enhancement — **the server decides** — the "opens in a new tab" truth is still announced to screen
222
+ readers when the link really does open one, and versions still appear on receipts, where somebody
223
+ is actually reading the record.)
330
224
 
331
- The default signed-manifest path performs no database write on GET. A high-assurance flow can explicitly retain pre-submit presentation attempts:
225
+ Behind that single control the evidence is unchanged: two statements, two kinds, two document
226
+ versions, two lifecycles. Ticking the box records an *agreement* to the Terms and an
227
+ *acknowledgment* of the Privacy Notice; leaving it empty refuses both. The manifest signs the exact
228
+ composed sentence and which statements the one control answered, so the substitution defense holds
229
+ over the wording a person actually read.
230
+
231
+ Clickwrap composes that line only when every statement in the policy is an ordinary, required,
232
+ default-worded `agree_to` or `acknowledge`. Anything else keeps a control of its own, **below** the
233
+ line:
332
234
 
333
235
  ```ruby
334
- Clickwrap.policy :regulated_authorization do
335
- persist_presentations_before_submission_for 30.days,
336
- because: "Investigate disputes about this regulated authorization"
337
- authorize :regulated_action, one_time: true, valid_for: 10.minutes
338
- end
339
- ```
236
+ Clickwrap.policy :signup do
237
+ # These two compose into the line.
238
+ agree_to :terms, link_label: "Terms of Service"
239
+ acknowledge :privacy_notice, link_label: "Privacy Policy"
340
240
 
341
- Persisted presentations carry their own purpose, access, abuse controls, and retention; an abandoned GET is labeled `presented_by_server`, never `accepted` or `seen_by_human`.
241
+ # This one gets its own box, below the line, with its withdrawal route.
242
+ consent_to :product_updates,
243
+ document: :marketing_notice,
244
+ optional: true,
245
+ withdrawal_path: "/settings/privacy"
342
246
 
343
- The receipt says exactly what this proves: the server generated and accepted a particular presentation manifest. It does not claim the person read the document, understood it, saw particular pixels, or received a legally sufficient interface in every jurisdiction.
247
+ retain_with :ordinary_agreement_evidence
248
+ end
249
+ ```
344
250
 
345
- ### The form helper
251
+ An optional consent is never folded in — bundling it would silently make it required, and unbundled
252
+ consent is the whole point of the `consent_to` verb. Neither is a recorded yes/no, a statement with
253
+ a withdrawal route, or copy your application wrote itself. And a policy with nothing composable —
254
+ the operator attestation rails, the payout authorization — renders exactly as it always has, one
255
+ control per act.
346
256
 
347
- The strongest happy path is one line because the component owns both the controls and the action whose wording it records:
257
+ Want the itemized shape anyway? One boolean, and it reaches the presenter, so the manifest signs
258
+ the shape that was actually offered:
348
259
 
349
260
  ```erb
350
- <%= form.clickwrap :signup, submit: "Create account" %>
261
+ <%= form.clickwrap :signup, submit: "Create account", combined: false %>
351
262
  ```
352
263
 
353
- Submit options remain ordinary Rails:
264
+ The words are yours. `clickwrap.sentence.agreement` and `clickwrap.sentence.acknowledgment` are
265
+ ordinary translations with `%{documents}` marking where the links go, and each document's link text
266
+ comes from `link_label:` on the statement — which is how "Privacy Notice" becomes "Privacy Policy"
267
+ without touching what the statement asserts.
354
268
 
355
- ```erb
356
- <%= form.clickwrap :signup,
357
- actor: current_user,
358
- subject: @organization,
359
- locale: I18n.locale,
360
- submit: {
361
- text: "Create organization",
362
- class: "button button--primary",
363
- data: { turbo_submits_with: "Creating…" }
364
- } %>
365
- ```
269
+ Legal pages in Markdown? `config.document_renderer = :markdown` renders through whichever
270
+ Markdown library you already bundle, and `:markdown_rails` renders through your application's
271
+ *own* registered markdown-rails renderer — the exact pipeline your public `/legal` pages go
272
+ through, so the snapshot people accept comes out byte-for-byte identical to the rendered text
273
+ those pages serve, by construction rather than by careful copying.
366
274
 
367
- The helper renders:
275
+ Wiring the gem into an existing production app — or handing the job to an AI agent? The
276
+ [integrating guide](guides/integrating.md) is the step-by-step playbook from a full
277
+ production migration, in the exact order that avoids every mistake we made.
368
278
 
369
- - real, initially unselected controls;
370
- - kind-appropriate first-person language;
371
- - obvious document links before the submit action;
372
- - stable label/control/error associations;
373
- - server errors and accessible error summaries;
374
- - the signed presentation token; and
375
- - no hidden IP address, browser user-agent, policy version, validity date, or other client-owned security decision.
279
+ Hotwire Native? One setting answers both halves of the native question — the href
280
+ *and* the link attributes:
376
281
 
377
- HTML `required` is progressive enhancement. Server validation is always authoritative.
282
+ ```ruby
283
+ Clickwrap.configure do |config|
284
+ config.hotwire_native_document_links = {
285
+ open_in: :external_browser,
286
+ canonical_host: "https://www.example.com"
287
+ }
288
+ end
289
+ ```
378
290
 
379
- If your design system needs to render the action separately, use the deliberately explicit split API:
291
+ Here's why that matters: on a native authentication sheet, a same-host document
292
+ link is routed by the app itself, which pops the sheet and takes the half-filled
293
+ signup form with it. `:external_browser` absolutizes the signed document path
294
+ against your canonical host and opens it outside the WebView, so the form is
295
+ still there when the person comes back. `:same_screen` keeps a plain same-host
296
+ link for your own native path configuration to route (a document sheet inside a
297
+ signed-in funnel, say).
380
298
 
381
- ```erb
382
- <%= form.clickwrap_fields :signup,
383
- submit_button_text: "Create account" %>
299
+ One app often needs both — the auth sheet must escape, the signed-in funnel
300
+ routes its own sheet — so `open_in:` also takes a callable:
384
301
 
385
- <%= form.submit "Create account" %>
302
+ ```ruby
303
+ config.hotwire_native_document_links = {
304
+ open_in: ->(controller) { controller.signing_up? ? :external_browser : :same_screen },
305
+ canonical_host: "https://www.example.com"
306
+ }
386
307
  ```
387
308
 
388
- The repeated text is intentional: it makes the evidence contract visible in code. Development and system-test assertions compare the declared text with the rendered submit control and reject a mismatch. The one-call API is preferred because it makes that class of drift impossible.
309
+ It is asked once when the href is signed and once when the link is rendered,
310
+ with the same controller both times, so the two halves of a link cannot
311
+ disagree.
389
312
 
390
- ### Use the ready-made standalone remediation screen
313
+ Another client needs different attributes, or different ones per screen? Keep the
314
+ gem's canonical partial and set `config.document_link_html_options_with`. It can
315
+ add `data: { turbo: false }`, `target`, or `rel`; it cannot replace the immutable
316
+ `href` that Clickwrap signs into the presentation. When the native setting above
317
+ is set it answers native renders entirely, and this hook goes on answering every
318
+ other render.
391
319
 
392
- Any policy can be completed outside its original flow:
320
+ ## How it works
393
321
 
394
- ```ruby
395
- # config/routes.rb
396
- mount Clickwrap::Engine => "/agreements"
397
- ```
322
+ Most apps eventually accumulate an `accepted_terms_at` column, a `terms_version` string, a few hidden form fields, an `after_create` callback, and some IP columns. Each part looks reasonable alone. Together they produce partial writes, client-owned policy decisions, mutable history, and evidence only the original engineer can explain.
398
323
 
399
- ```ruby
400
- clickwrap_capture_path(:driver_declaration)
401
- ```
324
+ `clickwrap` replaces that plumbing with one coherent primitive:
402
325
 
403
- The engine provides actor-owned capture, receipt, consent-withdrawal, and document-history surfaces using your parent controller, layout, locale, and authorization callbacks. This makes a required agreement or declaration resolvable in place instead of becoming a dead end.
326
+ 1. **Documents are immutable.** Publishing reads the exact bytes, digests them, and freezes a snapshot. A changed document requires a new version the task refuses to reuse a version label for different bytes.
327
+ 2. **Policies are server-owned.** The browser may answer; it may never choose the policy, document version, validity, subject, or what gets recorded. Policies compile at boot and fail loudly when misconfigured.
328
+ 3. **Presentations are signed.** `form.clickwrap` creates a short-lived signed manifest of what the server generated for the form: documents, digests, statements, choices, and the submit button text. Stale, swapped, expired, or cross-account tokens are rejected at submit. A deploy between render and submit cannot record a version that was not bound to the accepted submission. This does not prove human perception or comprehension.
329
+ 4. **Capture is atomic.** Evidence and the protected database action commit together or not at all. Replays of the same submission return the original result instead of running twice.
330
+ 5. **Lifecycle history appends.** Through Clickwrap's public/model APIs, withdrawal, expiry, correction, and supersession append new events instead of rewriting the earlier event. Optional PostgreSQL hardening rejects additional direct database mutation paths; the integrity verifier detects covered changes rather than pretending a fully privileged database actor is impossible.
331
+ 6. **Receipts have a standalone verifier.** Canonical JSON ([RFC 8785](https://www.rfc-editor.org/rfc/rfc8785)) with versioned schemas and SHA-256 digests can be checked by the bundled `clickwrap` CLI without booting Rails. The result distinguishes fully verified, failed, and incomplete checks; document-byte checks need the exported artifacts, and reviewed disposition is reported as disposition rather than ordinary verification.
404
332
 
405
- ### Eject or fully own the UI
333
+ ## Six verbs, six honest meanings
406
334
 
407
- Copy the tested reference views:
335
+ Not every checkbox is "consent," and not every timestamp is a "signature." Each verb gets the lifecycle it actually needs:
408
336
 
409
- ```bash
410
- bin/rails generate clickwrap:views
411
- ```
412
-
413
- Your copies shadow the gem’s views. Tailwind, Bootstrap, ViewComponent, Phlex, custom design systems, and plain ERB are all welcome.
337
+ | Policy verb | Meaning | Typical lifecycle |
338
+ |---|---|---|
339
+ | `agree_to` | Assent to contractual terms | agreed → superseded by new version |
340
+ | `acknowledge` | Affirmative receipt of a notice or risk | acknowledged → superseded / expired |
341
+ | `consent_to` | Purpose-specific permission | granted withdrawn / renewed |
342
+ | `declare` | A factual statement made by the actor | declared → corrected / expired |
343
+ | `attest` | An operational fact affirmed by an operator | attested → corrected / superseded |
344
+ | `authorize` | Narrow permission bound to one protected action | authorized → consumed / expired |
414
345
 
415
- For a completely custom surface, ask the presenter for primitives rather than recreating hidden inputs:
346
+ The DSL is intentionally verbal:
416
347
 
417
348
  ```ruby
418
- presentation = Clickwrap.present(
419
- :signup,
420
- actor: current_user,
421
- subject: nil,
422
- locale: I18n.locale,
423
- submit_button_text: "Create account"
424
- )
349
+ Clickwrap.policy :example do
350
+ agree_to :terms
351
+ acknowledge :privacy_notice
352
+ consent_to :product_updates, optional: true, withdrawal_path: "/settings/privacy"
353
+ declare :information_is_accurate
354
+ attest :bank_transfer_was_accepted
355
+ authorize :withdrawal, one_time: true, valid_for: 10.minutes
356
+
357
+ retain_with :ordinary_agreement_evidence
358
+ end
425
359
  ```
426
360
 
427
- ```erb
428
- <%= hidden_field_tag "clickwrap_submission[presentation_token]", presentation.token %>
361
+ The policy compiler rejects incoherent combinations at boot, in full sentences that tell you what's wrong and what to do about it: a one-time authorization can't be indefinite, consent needs a withdrawal path, and withdrawing future consent never rewrites a historical agreement.
429
362
 
430
- <% presentation.statements.each do |statement| %>
431
- <%# Render statement.control_name, label, document links, choices and errors. %>
432
- <% end %>
433
- ```
363
+ ## Protect an action with its evidence
434
364
 
435
- The development linter compares the submitted manifest with the policy/presenter contract and warns about missing statements, preselected consent, absent links, controls placed after the CTA, or unregistered custom copy. It reports objective problems; it never prints “legally compliant.”
365
+ `capture_and!` is the gem's signature move. In one supported database transaction it verifies the presentation, appends the evidence event, yields to your domain action, records the outcome, and commits both together:
436
366
 
437
- ## Capture evidence and the protected action together
367
+ Declare the exact post-action snapshot once. The callback receives the value
368
+ returned by the protected-action block—not the pre-action subject—and
369
+ `Clickwrap.protected_outcome` owns the stable reference and canonical
370
+ fingerprint:
438
371
 
439
- For an existing actor in a normal Rails controller:
372
+ ```ruby
373
+ Clickwrap.policy :withdrawal_authorization do
374
+ authorize :withdrawal,
375
+ one_time: true,
376
+ valid_for: 10.minutes,
377
+ protected_outcome_version: "submitted-withdrawal-v1",
378
+ record_protected_outcome_with: lambda { |withdrawal|
379
+ Clickwrap.protected_outcome(
380
+ action: :submitted,
381
+ record: withdrawal,
382
+ state: withdrawal.status,
383
+ facts: {
384
+ amount_in_cents: withdrawal.amount_cents,
385
+ currency: withdrawal.currency,
386
+ destination_reference: withdrawal.destination_reference
387
+ }
388
+ )
389
+ }
390
+
391
+ retain_with :regulated_evidence
392
+ end
393
+ ```
440
394
 
441
395
  ```ruby
442
396
  def create
443
397
  withdrawal = current_user.withdrawals.build(withdrawal_params)
444
398
 
445
- receipt = capture_clickwrap_and!(
446
- :withdrawal_authorization,
447
- actor: current_user,
448
- subject: withdrawal
449
- ) do |pending_receipt|
399
+ capture_clickwrap_and!(:withdrawal_authorization, actor: current_user, subject: withdrawal) do |pending_receipt|
450
400
  withdrawal.submit!(authorized_by_clickwrap_event: pending_receipt.event_id)
401
+ withdrawal # the exact completed result given to the outcome recorder
451
402
  end
452
403
 
453
404
  redirect_to withdrawal
454
405
  end
455
406
  ```
456
407
 
457
- The controller helper reads only the generated `clickwrap_submission` envelope and the current `http_request`. It delegates to the same public service API:
458
-
459
- ```ruby
460
- receipt = Clickwrap.capture_and!(
461
- :withdrawal_authorization,
462
- actor: current_user,
463
- subject: withdrawal,
464
- http_request: request,
465
- submission: clickwrap_submission
466
- ) do |pending_receipt|
467
- withdrawal.submit!(authorized_by_clickwrap_event: pending_receipt.event_id)
468
- end
469
- ```
408
+ If the event write fails, the action rolls back. If your block raises, the event rolls back. Repeating an identical submission returns the original result without running the block twice; a conflicting replay fails with a stable `Clickwrap::ReplayRejected`. That remains true when the successful action itself changes the fingerprinted subject: once the signed nonce committed, replay verifies the frozen event context and exact answers instead of requiring the old pre-action state to still exist.
470
409
 
471
- Within one supported database transaction, Clickwrap:
410
+ Link the row to the evidence that authorized it, so the connection survives years and engineers:
472
411
 
473
- 1. verifies actor, tenant, subject, presentation, policy, document digests, answers, expiry, and nonce;
474
- 2. acquires the required idempotency/subject locks;
475
- 3. appends the pending evidence event;
476
- 4. yields its receipt to the protected domain action;
477
- 5. records the resulting outcome and consumes one-time authorization where applicable;
478
- 6. commits both together; and
479
- 7. invokes optional notifications/analytics only after commit.
480
-
481
- If the event write fails, the protected action rolls back. If the block raises, the event rolls back. Repeating an identical idempotency key returns the original result without running the block twice. A conflicting replay fails with a stable `Clickwrap::ReplayRejected` result.
412
+ ```bash
413
+ bin/rails generate clickwrap:link withdrawals && bin/rails db:migrate
414
+ ```
482
415
 
483
- The block receives a read-only `Clickwrap::PendingReceipt`. Its stable `event_id` can be stored by the domain row, but export/verification methods are unavailable until commit. `capture_and!` returns the finalized `Clickwrap::Receipt`; if the transaction rolls back, the pending object becomes invalid instead of masquerading as committed evidence.
416
+ ```ruby
417
+ class Withdrawal < ApplicationRecord
418
+ has_clickwrap_evidence policy: :withdrawal_authorization,
419
+ statement: :withdrawal,
420
+ actor: :user,
421
+ subject: :self
422
+ end
484
423
 
485
- Atomic commit does not give Clickwrap permission to guess what a host method meant. Without a configured outcome snapshot, the receipt says only that the named policy, bound subject, evidence event, and block committed together. `record_protected_outcome_with` can add an exact post-action reference/state/fingerprint; it runs and validates inside the transaction, and a failure rolls the whole operation back.
424
+ capture_clickwrap_and!(:withdrawal_authorization, actor: current_user, subject: withdrawal) do |pending_receipt|
425
+ withdrawal.clickwrap_event_id = pending_receipt.event_id
426
+ withdrawal.save!
427
+ withdrawal
428
+ end
486
429
 
487
- The transaction contract is documented precisely for ownership, nested transactions, savepoints, deadlock/serialization retries, idempotency, callbacks, and after-commit behavior. Automatic retries occur only when Clickwrap can prove the block is safe to retry; otherwise a stable retryable error returns control to the host. Clickwrap never promises atomicity across two independent systems.
430
+ withdrawal.clickwrap_receipt.verify.success? # one line, years later
431
+ ```
488
432
 
489
- ### Capture without a protected action
433
+ When the protected domain row needs the person's submitted choice, read it
434
+ from the pending receipt rather than parsing controller params a second time:
490
435
 
491
436
  ```ruby
492
- receipt = Clickwrap.capture!(
493
- :current_terms,
494
- actor: current_user,
495
- http_request: request,
496
- submission: clickwrap_submission
497
- )
437
+ capture_clickwrap_and!(:privacy_preferences, subject: membership) do |pending_receipt|
438
+ membership.show_on_public_profile =
439
+ pending_receipt.granted?(:public_profile_visibility)
440
+ membership.save!
441
+ end
498
442
  ```
499
443
 
500
- ### Devise and Rails authentication
444
+ `answer_for`, `answered?`, `granted?`, and `declined?` read the validated,
445
+ server-bound event being committed. An optional control left unselected returns
446
+ `nil`/`false`; a statement name the policy never declared raises. Silence can
447
+ therefore never become permission, while a typo cannot silently disable a
448
+ feature. This keeps the browser's raw params out of protected domain logic.
501
449
 
502
- The installer detects the authentication stack and generates an explicit adapter—not a hidden `after_create` callback.
450
+ This model-first deployment order is safe. Before the generated column exists,
451
+ `has_clickwrap_evidence` stays inert and `clickwrap_receipt` returns `nil`; as
452
+ soon as the migration adds `clickwrap_event_id`, every new row is fail-closed
453
+ by default. That also lets historical data migrations replay schemas from
454
+ before Clickwrap without loading a model method for a column that did not yet
455
+ exist. It does not weaken current rows: after the column exists, missing,
456
+ mismatched, or replaced links fail validation.
503
457
 
504
- For Devise, the generated controller reads:
458
+ And when a *person* causes the refusal a stale token, a required box left unticked — every such case is one exception family carrying a sentence you can actually show them:
505
459
 
506
460
  ```ruby
507
- class Users::RegistrationsController < Devise::RegistrationsController
508
- clickwraps_registration_with :signup
461
+ def create
462
+ # ... capture_clickwrap_and! as above ...
463
+ rescue Clickwrap::CaptureRefused => refusal
464
+ redirect_to new_withdrawal_path, alert: refusal.user_facing_message, status: :see_other
509
465
  end
510
466
  ```
511
467
 
512
- For Rails’ authentication generator, the generated registration command uses:
468
+ Or drop the bang and let it read like `save`. `capture_clickwrap_and` and
469
+ `capture_clickwrap` absorb exactly that family, return `false`, put the
470
+ per-statement message beside the control it belongs to, and leave the whole
471
+ refusal on `clickwrap_refusal`:
513
472
 
514
473
  ```ruby
515
- register_with_clickwrap :signup, user: @user do
516
- @user.save!
474
+ def create
475
+ receipt = capture_clickwrap_and(:withdrawal_authorization, subject: withdrawal) do |pending_receipt|
476
+ withdrawal.submit!(authorized_by_clickwrap_event: pending_receipt.event_id)
477
+ end
478
+
479
+ unless receipt
480
+ flash.now[:alert] = clickwrap_refusal.user_facing_message
481
+ return render :new, status: :unprocessable_entity
482
+ end
483
+
484
+ redirect_to withdrawal
517
485
  end
518
486
  ```
519
487
 
520
- Both integrations ensure account activation and required evidence commit together. Emails, sign-in, redirects, and after-commit side effects occur only after the transaction has succeeded. A failed evidence write never leaves a normal public account silently active.
488
+ Nothing else is absorbed, by either form. Infrastructure failures stay outside that family and stay loud: an evidence write that fails refuses the protected action instead of being swallowed. So do lifecycle conflicts a conflicting replay (`Clickwrap::ReplayRejected`) or an already-consumed one-time authorization (`Clickwrap::OneTimeAuthorizationConflict`) still raises, because "this was already done" needs a domain answer that no generic rescue can supply honestly.
521
489
 
522
- Signup is modeled honestly as a prospective-actor flow:
490
+ That atomicity has one exact boundary: Clickwrap's event and the protected domain
491
+ write must use the same database connection. If a host model uses another Rails
492
+ database/connection, its transaction cannot commit atomically with Clickwrap's
493
+ tables. Put Clickwrap on the same connection for database-local work; use an
494
+ explicit outbox/reconciliation design for another database or service.
523
495
 
524
- 1. the GET creates a short-lived, signed registration-flow identifier;
525
- 2. the presentation token binds to that flow, the form object type, and any host-selected tenant—not to a fictional persisted or authenticated user;
526
- 3. the adapter validates the submitted presentation before account activation;
527
- 4. one transaction persists the account, binds its stable actor reference to the evidence, and commits both; and
528
- 5. the receipt records `account_registration` attribution and the actual pre-registration authentication state.
496
+ For capture without a protected action, use `Clickwrap.capture!`. For external providers (Stripe, identity services) that can't share your database transaction, use `Clickwrap.authorize_external_action!` — a pending authorization plus idempotent outbox, so a provider timeout never becomes a fictional success or a double debit.
529
497
 
530
- Email addresses, passwords, and raw signup fields are not copied into the token. A token from another browser flow, tenant, form object, or already-created account is rejected. Applications that own a custom registration service use the same primitive directly:
498
+ Controllers get the ambient actor, tenant, request, authentication context, and
499
+ submitted presentation automatically:
531
500
 
532
501
  ```ruby
533
- receipt = Clickwrap.register!(
534
- :signup,
535
- prospective_actor: @user,
536
- http_request: request,
537
- submission: clickwrap_submission
538
- ) do
539
- @user.save!
540
- end
502
+ authorization = authorize_clickwrap_external_action!(
503
+ :identity_provider_handoff,
504
+ subject: verification,
505
+ provider_name: "identity_provider"
506
+ )
541
507
  ```
542
508
 
543
- `register!` returns the same receipt type as `capture_and!`; the authentication adapters are thin conveniences over it.
544
-
545
- ### External providers use an outbox, not pretend-ACID
546
-
547
- Stripe, identity services, timestamp providers, and remote signatures cannot share your database transaction. Use a pending authorization and idempotent outbox:
509
+ If a migration requires a legacy/domain projection to commit with the event and
510
+ pending outbox row, use the deliberately named local-transaction callback (or
511
+ the equivalent block form):
548
512
 
549
513
  ```ruby
550
- authorization = Clickwrap.authorize_external_action!(
514
+ authorization = authorize_clickwrap_external_action!(
551
515
  :identity_provider_handoff,
552
- actor: current_user,
553
516
  subject: verification,
554
- http_request: request,
555
- submission: clickwrap_submission
556
- )
557
-
558
- ProviderHandoffJob.perform_later(
559
- authorization_id: authorization.id,
560
- idempotency_key: authorization.idempotency_key
517
+ provider_name: "identity_provider",
518
+ after_pending_action_is_saved_inside_transaction: lambda do |pending_action:, pending_receipt:|
519
+ LegacyAuditLog.create!(
520
+ event_id: pending_receipt.event_id,
521
+ external_action_id: pending_action.id
522
+ )
523
+ end
561
524
  )
562
525
  ```
563
526
 
527
+ It runs once, only on initial capture, inside that local database transaction;
528
+ if it raises, all three local writes roll back. Never call the provider or do
529
+ network work there. The provider call starts only after the helper returns.
530
+
531
+ ### One-time, subject-bound authorizations
532
+
564
533
  ```ruby
565
- authorization.record_provider_success_and_consume!(provider_receipt)
534
+ Clickwrap.policy :withdrawal_authorization do
535
+ acknowledge :withdrawal_requirements
536
+
537
+ declare :coverage_exclusivity,
538
+ subject_fingerprint_version: "covered-orders-v1",
539
+ subject_fingerprint_with: ->(withdrawal) { withdrawal.covered_orders_fingerprint }
540
+
541
+ authorize :withdrawal,
542
+ one_time: true,
543
+ valid_for: 10.minutes,
544
+ requires: %i[withdrawal_requirements coverage_exclusivity]
545
+
546
+ retain_with :regulated_evidence
547
+ end
566
548
  ```
567
549
 
568
- That final method is one idempotent local transaction. Failures and ambiguous timeouts use `record_provider_failure!` and `record_provider_outcome_unknown!`; the reconciliation task can safely resolve them later. A provider timeout never becomes a fictional success or a second debit.
550
+ The authorization is locked and consumed in the same transaction as the withdrawal. Another withdrawal, a changed subject, a stale declaration, or a concurrent replay cannot reuse it. This is the difference between "the user once accepted something" and "this exact evidence authorized this exact operation."
569
551
 
570
552
  ## Ask readable questions everywhere
571
553
 
@@ -573,407 +555,202 @@ The actor proxy is the everyday API:
573
555
 
574
556
  ```ruby
575
557
  user.clickwraps.current_for?(:signup)
576
- user.clickwraps.required_for?(:current_terms)
577
558
  user.clickwraps.agreed_to?(:terms)
578
- user.clickwraps.acknowledged?(:privacy_notice)
579
559
  user.clickwraps.consented_to?(:product_updates)
580
- user.clickwraps.declared?(:non_professional_driver, subject: scheme)
560
+ user.clickwraps.declared?(:independent_contractor, subject: scheme)
581
561
  user.clickwraps.authorized?(:withdrawal, subject: withdrawal)
582
562
  ```
583
563
 
584
- Every predicate has a structured form when “no” needs an explanation:
564
+ When "no" needs an explanation, `verify` returns a structured result with a stable error symbol (`:declaration_expired`, `:consent_withdrawn`, `:wrong_subject`, …), a matching predicate, and a localized message — you never parse English to make an authorization decision. `Clickwrap.require!` raises a typed error carrying the same result. Service boundaries read aloud:
585
565
 
586
566
  ```ruby
587
- result = Clickwrap.verify(
588
- :withdrawal_authorization,
589
- actor: user,
590
- subject: withdrawal
591
- )
567
+ preparation = Clickwrap.verify(:withdrawal_preparation, actor: user,
568
+ require_current_revision: true)
569
+ declaration = Clickwrap.verify(:coverage_exclusivity, actor: user, subject: user,
570
+ require_current_revision: true)
592
571
 
593
- result.success? # => false
594
- result.error # => :declaration_expired
595
- result.message # localized human explanation
596
- result.event_id
597
- result.details # stable machine-readable facts, no surprise PII
572
+ declaration.stale_policy_revision? # legal reworded it → re-ask
573
+ declaration.subject_fingerprint_mismatch? # what it covers changed since capture
574
+ declaration.recorded_after?(preparation) # ordering enforced, not assumed
598
575
  ```
599
576
 
600
- Stable errors cover wrong actor/tenant/subject, stale policy, unseen document version, missing answer, expiry, withdrawal, predecessor/order, fingerprint mismatch, consumption, replay, and integrity failure.
577
+ `recorded_after?` answers from a database-assigned recording sequence, so it stays true across actors, application processes, and same-microsecond writes — ULID lexical order is deliberately not used as chronology. Read its `false` carefully: it means "not after", **or** that one of the two has no sequence at all, which is the case for evidence recorded before the ordering migration and for a missing event. An upgrade cannot invent honest order for rows written before it, so `false` is the answer it gives rather than a guess. Branch on it as a guard (`return unless declaration.recorded_after?(preparation)`), never as proof of the opposite.
601
578
 
602
- The convention is consistent: predicates answer booleans, `verify` returns a result, and bang methods raise a typed error carrying that same result. Applications never need to parse an English error message to make an authorization decision.
579
+ `require_current_revision: true` fails evidence recorded under a superseded policy revision, so "we changed the wording, everyone re-accepts" is one keyword instead of a hand-rolled revision comparison.
603
580
 
604
- ### Controller gates that always have remediation
581
+ The same call takes an event id, which is how you re-ask about one specific recorded act years later:
605
582
 
606
583
  ```ruby
607
- class BillingController < ApplicationController
608
- requires_clickwrap :current_terms, only: :show
609
- end
584
+ Clickwrap.verify(event_id, subject: order_batch, require_current_revision: true)
610
585
  ```
611
586
 
612
- The gate redirects HTML/Hotwire users to the mounted policy capture screen and returns them to the original safe destination after completion. API clients receive a structured `clickwrap_required` response with a presentation endpoint.
587
+ Both keywords mean exactly what they mean above: `subject:` re-derives the fingerprint from the record as it is *now*, and `require_current_revision:` compares the act's recorded revision against the wording compiled today. That is the complete "is this old evidence still good?" question, so nothing needs to reach into `Clickwrap::PolicyRevision` or `Clickwrap::SubjectFingerprint` to ask it. If the policy is no longer declared at all, the result says `:unknown_policy` — "we can no longer check this" never gets spelled the same way as "this is fine".
613
588
 
614
- A required gate must have a remediation route or an explicit host support fallback. Clickwrap refuses to compile a dead-end gate.
615
-
616
- Security-sensitive services should still verify at the domain boundary:
589
+ Controller gates redirect users to a ready-made remediation screen and bring them back when they're done:
617
590
 
618
591
  ```ruby
619
- Clickwrap.require!(
620
- :withdrawal_authorization,
621
- actor: user,
622
- subject: withdrawal
623
- )
592
+ class BillingController < ApplicationController
593
+ requires_clickwrap :current_terms, only: :show
594
+ end
624
595
  ```
625
596
 
626
- Controller gates improve flow; service verification protects the action.
627
-
628
- ## Consent that can actually be withdrawn
629
-
630
- Consent is purpose-specific, initially unselected, and separate from Terms or a Privacy Notice acknowledgment:
597
+ ### Reacceptance when documents change
631
598
 
632
599
  ```ruby
633
- Clickwrap.document :marketing_notice,
634
- version: "2026-08-15",
635
- from: Rails.root.join("app/content/legal/marketing.md")
636
-
637
- Clickwrap.policy :marketing_preferences do
638
- consent_to :product_updates,
639
- document: :marketing_notice,
640
- optional: true,
641
- withdrawal_path: "/settings/privacy"
642
-
643
- consent_to :partner_offers,
644
- document: :marketing_notice,
645
- optional: true,
646
- withdrawal_path: "/settings/privacy"
600
+ Clickwrap.policy :current_terms do
601
+ agree_to :terms, require_current_version: true
647
602
 
648
- retain_with :marketing_consent_evidence
603
+ retain_with :ordinary_agreement_evidence
649
604
  end
650
605
  ```
651
606
 
652
- Leaving an optional checkbox unselected creates no consent grant. The capture receipt can show that the option was offered and not granted, but it does not call silence an affirmative refusal. A policy that truly needs a recorded yes/no choice uses explicit unselected controls:
607
+ Publish a new version and `current_for?` flips to `false` for everyone who accepted the old one. Preview the blast radius before you activate it:
653
608
 
654
- ```ruby
655
- consent_to :research_contact,
656
- choices: { yes: :grant, no: :decline },
657
- require_an_explicit_choice: true,
658
- withdrawal_path: "/settings/privacy"
609
+ ```bash
610
+ bin/rails clickwrap:reacceptance:plan POLICY=current_terms
659
611
  ```
660
612
 
661
- ```ruby
662
- Clickwrap.withdraw!(
663
- :product_updates,
664
- actor: current_user,
665
- http_request: request,
666
- because: "The user withdrew this purpose in privacy settings"
667
- )
668
- ```
613
+ ## Consent that can actually be withdrawn
669
614
 
670
- Withdrawal appends an event; it never deletes or mutates the historical grant. The policy’s post-commit hook can stop future processing or enqueue host-owned deletion work without making the original transaction depend on an analytics/job backend.
615
+ Consent is purpose-specific, initially unselected, and separate from Terms:
671
616
 
672
617
  ```ruby
673
- config.after_event_is_committed = lambda do |event|
674
- Marketing::StopProcessingJob.perform_later(event.actor_id) if event.consent_was_withdrawn?
675
- end
676
- ```
677
-
678
- Clickwrap structurally requires an accessible withdrawal path. It does not decide whether consent is the correct lawful basis.
679
-
680
- ## Expiring and corrected declarations
618
+ Clickwrap.policy :marketing_preferences do
619
+ consent_to :product_updates, optional: true, withdrawal_path: "/settings/privacy"
620
+ consent_to :partner_offers, optional: true, withdrawal_path: "/settings/privacy"
681
621
 
682
- ```ruby
683
- Clickwrap.policy :driver_declaration do
684
- declare :non_professional_driver,
685
- document: :driver_declaration,
686
- valid_for: 1.year,
687
- subject_fingerprint_with: ->(scheme) { scheme.evidence_fingerprint }
622
+ retain_with :marketing_consent_evidence
688
623
  end
689
624
  ```
690
625
 
691
626
  ```ruby
692
- user.clickwraps.declared?(:non_professional_driver, subject: scheme)
693
- user.clickwraps.declaration(:non_professional_driver, subject: scheme).expires_at
627
+ Clickwrap.withdraw!(:product_updates, actor: current_user, http_request: request,
628
+ because: "The user withdrew this purpose in privacy settings")
694
629
  ```
695
630
 
696
- Renewal always starts a new validity period. Correction, supersession, and expiry append linked lifecycle events:
631
+ Withdrawal appends an event it never deletes or mutates the historical grant. Declarations work the same way: they expire, get corrected, or get superseded through linked lifecycle events, without pretending the original statement never happened.
697
632
 
698
- ```ruby
699
- Clickwrap.correct_declaration!(
700
- :non_professional_driver,
701
- actor: user,
702
- subject: scheme,
703
- replaces: old_receipt,
704
- http_request: request,
705
- submission: clickwrap_submission
706
- )
707
- ```
708
-
709
- The host retains domain-specific eligibility and declaration models. Clickwrap owns presentation, evidence, lifecycle, receipts, and verification—not your business rules.
710
-
711
- ## Narrow, one-time authorizations
633
+ Three of those transitions are new statements by the same person rather than administrative flags, so each one is captured through a real presentation and submission, exactly like the first statement was:
712
634
 
713
635
  ```ruby
714
- Clickwrap.policy :withdrawal_authorization do
715
- acknowledge :withdrawal_requirements
716
-
717
- declare :ride_exclusivity,
718
- subject_fingerprint_with: ->(withdrawal) { withdrawal.covered_rides_fingerprint }
719
-
720
- authorize :withdrawal,
721
- one_time: true,
722
- valid_for: 10.minutes,
723
- requires: %i[withdrawal_requirements ride_exclusivity],
724
- record_protected_outcome_with: lambda { |withdrawal|
725
- {
726
- action: :submitted,
727
- reference: withdrawal.to_gid.to_s,
728
- fingerprint: withdrawal.evidence_fingerprint
729
- }
730
- }
731
- end
732
- ```
733
-
734
- `capture_and!` locks and consumes the authorization in the same transaction as the withdrawal. Another withdrawal, changed ride set, stale declaration, wrong ordering, or concurrent replay cannot reuse it.
735
-
736
- This is the core difference between “the user once accepted something” and “this exact evidence authorized this exact operation.”
737
-
738
- ## Operator attestations
739
-
740
- ```ruby
741
- Clickwrap.policy :manual_bank_transfer do
742
- attest :beneficiary_matches_verified_identity
743
- attest :bank_accepted_transfer
744
- authorize :record_transfer_as_sent, one_time: true
745
- end
746
- ```
747
-
748
- Attestations preserve which authorized operator asserted which operational fact, under which role and authentication context, while the host owns permissions and domain state.
749
-
750
- ## External agreements and imported receipts
636
+ # The facts someone declared changed. A correction never implies the original
637
+ # was false when it was made.
638
+ Clickwrap.correct_declaration!(:contractor_status, actor: current_user, subject: engagement,
639
+ submission: clickwrap_submission,
640
+ because: "The person told us their circumstances changed")
751
641
 
752
- When Stripe, DocuSign, Ironclad, or another provider owns the presentation, do not pretend your application captured the click:
642
+ # A new validity period, starting now never the old expiry pushed along, so a
643
+ # stale expiry cannot quietly survive a renewal.
644
+ Clickwrap.renew!(:contractor_status, actor: current_user, subject: engagement,
645
+ submission: clickwrap_submission,
646
+ because: "The person renewed their declaration before it lapsed")
753
647
 
754
- ```ruby
755
- Clickwrap.import_external_receipt!(
756
- :connected_account_service_agreement,
757
- actor: user,
758
- provider_name: "stripe",
759
- provider_event_id: account.id,
760
- provider_receipt: account.service_agreement,
761
- verified_with: :stripe_api,
762
- verified_at: Time.current
763
- )
648
+ # Consent that now covers something narrower or wider. Rescoping is not
649
+ # withdrawal: the permission stays active, under new terms.
650
+ Clickwrap.change_consent_scope!(:product_updates, actor: current_user,
651
+ submission: clickwrap_submission,
652
+ because: "The person narrowed this permission in privacy settings")
764
653
  ```
765
654
 
766
- The event is labeled `external_receipt`, preserves provider provenance and validation status, and can participate in host verification without becoming a fictional local presentation.
655
+ Every one of them appends a linked event, leaves the earlier event exactly as it was recorded, and produces a receipt that verifies on its own.
767
656
 
768
- ## Receipts answer “show me exactly what happened”
657
+ Seeds, imports, and admin-created accounts never fake a human click either — `Clickwrap.exempt!` records an explicit exemption with who created it and why, and exemptions never satisfy `agreed_to?`.
658
+
659
+ ## Receipts show exactly what the application recorded
769
660
 
770
661
  Every event has one canonical JSON receipt and one human-readable HTML projection:
771
662
 
772
663
  ```ruby
773
664
  receipt = Clickwrap.receipt(event_id)
774
-
775
665
  receipt.to_canonical_json
776
666
  receipt.to_html
777
- receipt.to_pdf # optional renderer; never the source of truth
778
667
  receipt.verify
779
668
  ```
780
669
 
781
- An abbreviated receipt looks like:
670
+ An abbreviated receipt:
782
671
 
783
672
  ```json
784
673
  {
785
674
  "schema": "clickwrap.receipt.v1",
786
675
  "event_id": "01K2Y8T5QY0N4V6N1H4G4CQY8J",
787
676
  "policy": { "key": "signup", "revision": "sha256:..." },
788
- "actor": {
789
- "type": "User",
790
- "reference": "usr_...",
791
- "attribution": { "method": "account_registration", "authenticated": false }
792
- },
793
677
  "acts": [
794
678
  { "statement": "terms", "kind": "agreement", "action": "agreed" },
795
- {
796
- "statement": "privacy_notice",
797
- "kind": "acknowledgment",
798
- "action": "acknowledged"
799
- }
679
+ { "statement": "privacy_notice", "kind": "acknowledgment", "action": "acknowledged" }
800
680
  ],
801
681
  "documents": [
802
- { "key": "terms", "version": "2026-08-15", "locale": "en", "sha256": "..." },
803
682
  {
804
- "key": "privacy_notice",
683
+ "key": "terms",
805
684
  "version": "2026-08-15",
806
685
  "locale": "en",
807
- "sha256": "..."
686
+ "source_digest": "sha256:...",
687
+ "rendered_digest": "sha256:..."
808
688
  }
809
689
  ],
810
690
  "presentation": {
811
- "manifest_sha256": "...",
691
+ "manifest_digest": "sha256:...",
812
692
  "submit_button_text": "Create account",
813
693
  "offered_at": "2026-08-15T12:34:56.123456Z"
814
694
  },
815
- "outcome": { "type": "User", "reference": "usr_...", "status": "created" },
816
- "request_evidence": {
817
- "ip_address": { "state": "not_configured" },
818
- "browser_user_agent": { "state": "not_configured" },
819
- "ip_geolocation": { "state": "not_configured" }
820
- },
821
- "integrity": { "digest_algorithm": "sha256", "verified": true }
695
+ "integrity": { "digest_algorithm": "sha256", "receipt_digest": "sha256:..." }
822
696
  }
823
697
  ```
824
698
 
825
- The bundle can include exact document files, manifest, per-act lifecycle/predecessor graph, protected outcome, optional provider receipts, integrity/checkpoint verification, system explanation, and verifier version.
826
-
827
- `to_canonical_json` returns the verifiable core receipt and omits raw sensitive request evidence by default. Raw IP address, browser user-agent, and IP-geolocation values live in a separately encrypted evidence annex with its own digest, authorization, retention, hold, and disposition state. That boundary lets the core event remain immutable when a permitted retention process later removes the annex.
828
-
829
- Canonical receipts use versioned schemas and the [JSON Canonicalization Scheme (RFC 8785)](https://www.rfc-editor.org/rfc/rfc8785), plus a published Clickwrap profile for UTC timestamps, decimals, identifiers, binary digests, absent values, and extension names. They never depend on Ruby object serialization, YAML, database column order, or the current policy source. Unknown schema versions fail honestly instead of being “best effort” reinterpreted.
830
-
831
- ### View and download
832
-
833
- With the engine mounted:
834
-
835
- ```ruby
836
- clickwrap_receipt_path(receipt)
837
- ```
838
-
839
- Actors can view their own receipts. Operator access is always host-authorized:
840
-
841
- ```ruby
842
- config.authorize_receipt_access_with = lambda do |controller, receipt|
843
- controller.current_user == receipt.actor || controller.current_user.admin?
844
- end
845
- ```
846
-
847
- Foreign IDs return not found; existence is not leaked.
848
-
849
- ### Export only the sensitive fields you intend
850
-
851
- ```ruby
852
- Clickwrap.export_receipt(
853
- receipt,
854
- requested_by: current_operator,
855
- because: "Investigate dispute 2026-184",
856
- include_ip_address: false,
857
- include_browser_user_agent: false,
858
- include_ip_geolocation: false
859
- )
860
- ```
861
-
862
- There is intentionally no vague `include_sensitive_context: true` switch. Unredacted operator access and export require host authorization plus a human-readable reason and append an access event. Actor self-service follows the host’s configured disclosure policy without revealing internal fraud/security fields by accident.
863
-
864
- ### Verify inside or outside the application
865
-
866
- ```ruby
867
- Clickwrap::Receipt.verify(canonical_json, documents: document_files)
868
- ```
699
+ Verify it inside the app, or completely outside it with the bundled CLI:
869
700
 
870
701
  ```bash
871
702
  clickwrap verify receipt.json --documents ./receipt-documents
872
703
  ```
873
704
 
874
- The standalone verifier does not need the host application’s source code. At the baseline tier it verifies schema, canonical bytes, digests, links, and bundled content consistency; it does not claim that a self-contained file could not have been fabricated by someone controlling every source. Independent anchors/provider signatures add the stronger origin/time evidence they actually supply. Golden fixtures ensure new releases continue verifying every historical receipt format.
875
-
876
- ## Optional request evidence, private by default
705
+ Golden fixtures make a verifier regression for any released receipt schema fail the test suite.
877
706
 
878
- Clickwrap always records its event ID, server time, capture channel, policy/application version, configured actor/authentication source, and HTTP request ID when available.
707
+ 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.
879
708
 
880
- It records none of these personal/request-derived fields unless the initializer or policy names them:
709
+ ## Request evidence is off by default
881
710
 
882
- - raw IP address;
883
- - raw browser User-Agent;
884
- - IP-geolocation country, region, city, postal code, coordinates, timezone, continent, metro code, or accuracy radius;
885
- - browser/device fingerprints; or
886
- - actual GPS/device location.
887
-
888
- Browser fingerprinting and GPS are never collected by the base gem. IP geolocation is provider-estimated network context—not identity, GPS, a street address, or proof that the person was physically there.
889
-
890
- Those defaults are evidence design, not fear of useful data. IP addresses and linked online identifiers can be personal data ([Breyer, C-582/14](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A62014CJ0582)); keeping them on first-party infrastructure does not remove purpose, lawful-basis, transparency, minimization, protection-by-default, security, retention, or high-risk-assessment duties ([GDPR Articles 5](https://eur-lex.europa.eu/eli/reg/2016/679/art_5/oj/eng), [6](https://eur-lex.europa.eu/eli/reg/2016/679/art_6/oj/eng), [13](https://eur-lex.europa.eu/eli/reg/2016/679/art_13/oj/eng), [25](https://eur-lex.europa.eu/eli/reg/2016/679/art_25/oj/eng), [32](https://eur-lex.europa.eu/eli/reg/2016/679/art_32/oj/eng), and [35](https://eur-lex.europa.eu/eli/reg/2016/679/art_35/oj/eng)). Clickwrap therefore supports rich capture while requiring a present, named posture.
891
-
892
- MaxMind expressly describes GeoIP as approximate and not capable of identifying a household, individual, or street address; Cloudflare describes its fields as location information for an IP address ([MaxMind accuracy guidance](https://support.maxmind.com/knowledge-base/articles/maxmind-geolocation-accuracy); [Cloudflare IP geolocation](https://developers.cloudflare.com/network/ip-geolocation/)). Clickwrap preserves that uncertainty instead of polishing an estimate into a stronger claim.
893
-
894
- ### Enable exactly what one policy needs
711
+ `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:
895
712
 
896
713
  ```ruby
897
714
  Clickwrap.policy :regulated_authorization do
898
715
  authorize :regulated_action, one_time: true, valid_for: 10.minutes
899
716
 
900
- review_request_evidence_configuration_on Date.new(2027, 8, 15)
901
-
902
717
  record_ip_address(
903
718
  encrypted: true,
904
719
  retain_until: :regulated_evidence_retention_ends,
905
- because: "Investigate account compromise and disputes about this action",
906
- legal_basis_reference: "LIA-SECURITY-2026-01"
907
- )
908
-
909
- record_browser_user_agent(
910
- encrypted: true,
911
- retain_until: :regulated_evidence_retention_ends,
912
- because: "Corroborate the client context used for this action",
913
- legal_basis_reference: "LIA-SECURITY-2026-01"
720
+ because: "Investigate account compromise and disputes about this action"
914
721
  )
915
722
 
916
- record_ip_geolocation(
917
- country: true,
918
- region: true,
919
- city: true,
920
- postal_code: false,
921
- latitude_and_longitude: true,
922
- timezone: true,
923
- continent: false,
924
- metro_code: false,
925
- accuracy_radius_in_kilometers: true,
926
- using: :trackdown,
927
- retain_until: :regulated_evidence_retention_ends,
928
- because: "Corroborate anomalous access and investigate action disputes",
929
- legal_basis_reference: "LIA-SECURITY-2026-01",
930
- data_protection_impact_assessment_reference: "DPIA-2026-04"
931
- )
723
+ retain_with :regulated_evidence
932
724
  end
933
725
  ```
934
726
 
935
- Every enabled IP-geolocation result carries provider name/source, estimated state, resolution time, unavailable reason, and any database/accuracy provenance the resolver supplies. A policy cannot keep provider-derived coordinates while stripping the uncertainty needed to interpret them.
936
-
937
- Receipts distinguish `not_configured`, `unavailable`, `recorded`, `redacted_for_this_viewer`, `deleted_after_retention`, and `held`. “Blank” is never allowed to blur “we chose not to collect it” into “collection failed.”
938
-
939
- The browser cannot submit or replace server-observed values. Clickwrap conventionally reads `request.remote_ip`, and the host must configure/test trusted proxies correctly; Rails documents the forwarding, trusted-proxy, and spoof-check assumptions in [`ActionDispatch::RemoteIp`](https://api.rubyonrails.org/classes/ActionDispatch/RemoteIp.html).
940
-
941
- Required request enrichment resolves before the evidence/domain transaction begins and is carried into it as verified input; it is never filled in later by analytics. A policy chooses explicitly whether an unavailable resolver blocks capture or produces an `unavailable` state. Network resolvers are supported, but local databases or already-verified edge metadata avoid holding a domain transaction open around a remote call.
942
-
943
- ### Trackdown is the optional official resolver
727
+ Recorded values live in a separately encrypted annex with their own retention, so
728
+ they can be deleted later without rewriting the core event payload. Core payloads
729
+ have their own reviewed disposition path and leave a digest-linked tombstone.
730
+ There is deliberately no `gdpr_compliant_mode` or `maximum_evidence` switch —
731
+ every field is named individually, in plain English.
944
732
 
945
- ```ruby
946
- bundle add trackdown
947
- ```
733
+ For IP geolocation, [`trackdown`](https://github.com/rameerez/trackdown) 0.4 or newer is the optional official resolver:
948
734
 
949
735
  ```ruby
950
- config.ip_geolocation_resolver =
951
- Clickwrap::IpGeolocation::TrackdownResolver.new
952
- ```
953
-
954
- `trackdown` remains optional. Clickwrap stores only the fields authorized by the active server policy, never the entire result object. Provider presence is not source trust: Cloudflare-derived fields are marked host-verified only when the application explicitly verifies that requests came through its trusted Cloudflare path.
955
-
956
- `footprinted` remains analytics, not authoritative evidence. A sanitized event ID/policy/kind may be emitted to analytics after commit; analytics failure can never undo or substitute for the Clickwrap event.
957
-
958
- ### Easy installer recipes without a fake compliance switch
959
-
960
- The installer can scaffold either starting point:
961
-
962
- ```bash
963
- bin/rails generate clickwrap:install \
964
- --request-evidence-recipe=privacy-minimized
965
- ```
736
+ Trackdown.configure do |trackdown|
737
+ trackdown.verify_request_came_through_trusted_cloudflare_path_with do |request|
738
+ request.env["my_app.cloudflare_origin_was_verified"] == true
739
+ end
740
+ end
966
741
 
967
- ```bash
968
- bin/rails generate clickwrap:install \
969
- --request-evidence-recipe=evidence-rich
742
+ config.ip_geolocation_resolver = Clickwrap::IpGeolocation::TrackdownResolver.new
970
743
  ```
971
744
 
972
- The second recipe asks about every field, purpose, encryption choice, access/export policy, trusted-source posture, and retention rule. It then writes every individual setting into the initializer and disappears. There is no runtime `gdpr_compliant_mode`, `maximum_evidence`, `track_everything`, or `legal_proof` option.
745
+ Clickwrap passes the exact Rack request to Trackdown and records the provider that actually
746
+ answered, its source and database provenance, and Trackdown's per-request trust result. It
747
+ never treats CDN header presence as proof of a trusted path. The host must derive the Rack
748
+ flag above from its real origin protection; Trackdown documents the supported patterns in
749
+ [“Did the request really come through your CDN?”](https://github.com/rameerez/trackdown/blob/v0.4.0/README.md#did-the-request-really-come-through-your-cdn).
973
750
 
974
- Recipes are scaffolding, never compliance verdicts.
751
+ The [request evidence guide](guides/request-evidence.md) covers every field, the provenance model, and the privacy boundaries.
975
752
 
976
- ## Retention, deletion, and legal holds are first-class
753
+ ## Retention, deletion, and legal holds
977
754
 
978
755
  Every policy chooses an application-defined retention class:
979
756
 
@@ -982,727 +759,576 @@ Clickwrap.retention :ordinary_agreement_evidence do
982
759
  retain_core_event_for 6.years
983
760
  delete_recorded_ip_address_after 90.days
984
761
  delete_recorded_browser_user_agent_after 90.days
985
- delete_recorded_ip_geolocation_after 90.days
986
762
  end
987
763
  ```
988
764
 
989
- Event-based and “later of” rules are supported for regulated records:
765
+ Disposition is previewed, planned, and applied explicitly and rechecked at apply time, so a newly placed legal hold or changed policy stops a stale plan:
990
766
 
991
- ```ruby
992
- Clickwrap.retention :regulated_evidence do
993
- retain_core_event_until :regulated_evidence_retention_ends
994
- retain_recorded_ip_address_until :security_evidence_retention_ends
995
- retain_recorded_browser_user_agent_until :security_evidence_retention_ends
996
- retain_recorded_ip_geolocation_until :security_evidence_retention_ends
997
- end
767
+ ```bash
768
+ bin/rails clickwrap:retention:plan
769
+ bin/rails clickwrap:retention:apply PLAN=01K2Y8T5QY0N4V6N1H4G4CQY8J
998
770
  ```
999
771
 
1000
- ```ruby
1001
- config.calculate_retention_time_for :regulated_evidence_retention_ends do |event|
1002
- [
1003
- event.recorded_at_by_server + 5.years,
1004
- event.subject_liquidated_at&.+(3.years)
1005
- ].compact.max
1006
- end
1007
- ```
772
+ Destructive methods say exactly what they delete (`Clickwrap.delete_recorded_ip_address!`), deletions append a disposition event, and deleting a user account never silently cascades evidence away. Each event keeps the schedule recorded when that event was created; linked lifecycle events do not inherit their root's elapsed time or get deleted merely because the root became due. Legal holds pause disposition and are recorded through named append/release transitions. Details in the [retention and legal holds guide](guides/retention-and-legal-holds.md).
773
+
774
+ ## Progressive integrity, honestly labeled
1008
775
 
1009
- Clickwrap does not decide those periods. It makes reviewed policies executable and auditable.
776
+ Start useful with an ordinary Rails database; add assurance without changing the capture API:
1010
777
 
1011
- Preview every disposition before applying it:
778
+ | Tier | What it adds |
779
+ |---|---|
780
+ | Baseline | Canonical receipts, immutable snapshots, SHA-256 digests, standalone verifier |
781
+ | Database hardening | Adapter-specific update/delete protections |
782
+ | Chained history | Per-tenant event chains and checkpoints |
783
+ | Independent anchoring | A verified publication of an exact event-chain snapshot outside the primary database |
784
+ | Third-party timestamps | A provider token over an exact event digest, with the adapter's verification result |
785
+
786
+ Each tier states exactly what threat it addresses. A local hash is never called tamper-proof, server time is never called trusted time, and an IP address is never called identity. The [integrity guide](guides/integrity.md) has the threat model.
1012
787
 
1013
788
  ```bash
1014
- bin/rails clickwrap:retention:plan
1015
- bin/rails clickwrap:retention:apply PLAN=01K2Y8T5QY0N4V6N1H4G4CQY8J
789
+ bin/rails clickwrap:verify # verify continuously in production
1016
790
  ```
1017
791
 
1018
- The plan is immutable, scoped, expiring, and rechecked at apply time. A newly placed hold, changed policy, changed eligibility, or stale plan stops disposition instead of deleting a broader set than the operator reviewed.
792
+ ## Works with Devise, Rails authentication, Hotwire, and APIs
1019
793
 
1020
- Destructive public methods name exactly what they remove:
794
+ The installer detects your authentication stack and prints the exact door line to add, with your own file path and class name filled in. You add it yourself: this is an explicit adapter you can read in your own controller, not a hidden `after_create` callback the gem installs behind your back.
1021
795
 
1022
796
  ```ruby
1023
- Clickwrap.delete_recorded_ip_address!(receipt, because: "Retention period ended")
1024
- Clickwrap.delete_recorded_browser_user_agent!(receipt, because: "Retention period ended")
1025
- Clickwrap.delete_recorded_ip_geolocation!(receipt, because: "Retention period ended")
797
+ # Devise
798
+ class Users::RegistrationsController < Devise::RegistrationsController
799
+ clickwraps_registration_with :signup
800
+ end
1026
801
  ```
1027
802
 
1028
- Deletion removes the selected encrypted annex value, appends a disposition event, and changes the current receipt projection to `deleted`; it does not rewrite the historical agreement/declaration/authorization. Verification thereafter proves the immutable core event and its disposition history while reporting that the raw annex value is no longer available. A retained digest is described as a retained linkable digest, never automatically called anonymous.
1029
-
1030
- ### Legal holds
1031
-
1032
803
  ```ruby
1033
- receipt.place_on_legal_hold!(
1034
- because: "Pending dispute 2026-184",
1035
- placed_by: current_operator,
1036
- review_on: 6.months.from_now
1037
- )
804
+ # Rails authentication generator, or any hand-rolled signup door
805
+ def create
806
+ @user = User.new(user_params)
1038
807
 
1039
- receipt.release_legal_hold!(
1040
- because: "Dispute resolved",
1041
- released_by: current_operator
1042
- )
808
+ unless register_with_clickwrap(:signup, user: @user) { @user.save! }
809
+ return render :new, status: :unprocessable_entity
810
+ end
811
+
812
+ start_new_session_for @user
813
+ redirect_to after_authentication_url
814
+ end
1043
815
  ```
1044
816
 
1045
- A hold pauses scheduled disposition, requires a reason/owner/review date, and is itself append-only evidence.
817
+ Both make account activation and its evidence commit together, with a prospective-actor flow that's honest about the fact that no authenticated user exists yet at render time.
1046
818
 
1047
- Deleting an actor account never silently cascades evidence. The installer uses restrictive/nullifying relationships plus a stable configured pseudonymous actor reference. Host retention policy decides what remains.
819
+ The door helpers come in a pair, exactly like `save` and `save!`. The non-bang form absorbs a *refused* signup — a stale presentation, an unticked box, a validation the account failed into the same human sentences the Devise adapter paints (inline beside the control, once on the record's `:base`) and returns `false`, ready for that 422 re-render. `register_with_clickwrap!` raises instead, for flows that handle the exceptions themselves. An infrastructure failure escapes *both* forms: a broken database is not a refusal to dress up as validation, so the sign-in, the welcome email, and the redirect that would normally follow simply do not happen. That's the difference between a refused signup and a live account nobody can explain.
1048
820
 
1049
- ### Privacy inventory and actor requests
821
+ During a legacy migration, keep a required dual-write inside that same
822
+ transaction without replacing Devise's controller action:
1050
823
 
1051
- Clickwrap can describe what the application configured without pretending that configuration is lawful:
824
+ ```ruby
825
+ class Users::RegistrationsController < Devise::RegistrationsController
826
+ clickwraps_registration_with :signup,
827
+ after_account_is_saved_inside_transaction: :record_legacy_acceptance!
1052
828
 
1053
- ```bash
1054
- bin/rails clickwrap:privacy:inventory
1055
- bin/rails clickwrap:privacy:export ACTOR=gid://my-app/User/123
1056
- bin/rails clickwrap:privacy:disposition:plan ACTOR=gid://my-app/User/123
829
+ private
830
+
831
+ def record_legacy_acceptance!(account:, pending_receipt:)
832
+ account.terms_acceptances.create!(
833
+ clickwrap_event_id: pending_receipt.event_id,
834
+ accepted_at: Time.current
835
+ )
836
+ end
837
+ end
1057
838
  ```
1058
839
 
1059
- The inventory lists every policy, personal/request-derived field, stated purpose, host-supplied legal-basis reference, provider/source, encryption state, access callback, retention rule, unresolved host event, and review date. The actor export uses the same authorization/redaction rules as receipts. The disposition command only creates a reviewable plan; it does not decide whether an erasure request overrides retention duties, legal claims, or a hold.
840
+ If that required legacy write fails, the account and Clickwrap evidence roll
841
+ back with it. Remove the hook after parity and cutover are proved.
1060
842
 
1061
- Programmatic equivalents return structured results for a host-owned privacy workflow:
843
+ Want the gem's controls but your own button markup? `form.clickwrap_fields` takes a block and hands you the signed presentation, so the wording is read rather than retyped:
1062
844
 
1063
- ```ruby
1064
- Clickwrap::Privacy.inventory
1065
- Clickwrap::Privacy.export_for(actor, requested_by: current_operator)
1066
- Clickwrap::Privacy.plan_disposition_for(
1067
- actor,
1068
- requested_by: current_operator,
1069
- because: "Verified erasure request DSAR-2026-41"
1070
- )
845
+ ```erb
846
+ <%= form.clickwrap_fields :signup, submit_button_text: "Create account" do |clickwrap| %>
847
+ <button type="submit" class="btn btn--primary"><%= clickwrap.submit_button_text %></button>
848
+ <% end %>
1071
849
  ```
1072
850
 
1073
- Correcting an actor’s current email/name or unlinking an account changes the host projection, not the historical snapshot. A host may append a correction/linkage event when needed; Clickwrap never silently edits what an old receipt recorded.
851
+ `submit:` and `submit_button_text:` are a deliberate pair: `form.clickwrap :signup, submit: "Create account"` binds the words *and renders the button*, while `form.clickwrap_fields :signup, submit_button_text: "Create account"` binds the words and leaves the action to you.
1074
852
 
1075
- ## Progressive, honest integrity
853
+ Everything is server-rendered HTML: full-page requests, Turbo Drive and Frames, no-JavaScript validation, and Hotwire Native all work with the same helper. JSON/API clients use `Clickwrap.present` to get the server-owned manifest and submit answers with the signed token. Views are ejectable with `bin/rails generate clickwrap:views`, or build fully custom UI on `Clickwrap.present` plus the view helpers — `clickwrap_presentation_token_field`, `clickwrap_statement_check_box`, `clickwrap_statement_radio_button`, and `clickwrap_submit_button` own the envelope name, the control names, and a call to action worded by the signed manifest itself, while you own every class and wrapper around them. The [integrating guide](guides/integrating.md#4-custom-surfaces--the-three-contracts) shows a full custom surface.
1076
854
 
1077
- Clickwrap starts useful with an ordinary Rails database and lets serious applications add assurance without changing the capture API.
1078
-
1079
- | Tier | Capability | Honest claim |
1080
- |---|---|---|
1081
- | Baseline | Canonical receipts, immutable snapshots, versioned SHA-256 digests, append-only public API, independent verifier | Detects accidental/ordinary mutation of the verified bytes |
1082
- | Database hardening | Constraints and adapter-specific update/delete protections | Rejects unsupported mutation paths within the documented database threat model |
1083
- | Chained history | Per-tenant or per-aggregate event chains/checkpoints | Makes rewriting history detectable when checkpoints remain trustworthy |
1084
- | Independent anchoring | Heads stored/published outside the primary database | Improves evidence against a privileged primary-database rewrite |
1085
- | Trusted timestamp/provider | RFC 3161 or qualified trust-service receipt adapters | Preserves exactly the assurance and validation status supplied by that provider |
855
+ ### Works with `organizations`
1086
856
 
1087
- Enable optional hardening explicitly:
857
+ A human `User` can bind an `Organizations::Organization` without collapsing
858
+ the two identities:
1088
859
 
1089
- ```bash
1090
- bin/rails generate clickwrap:hardening --database
1091
- bin/rails db:migrate
860
+ ```ruby
861
+ Clickwrap.policy :organization_terms do
862
+ agree_to :organization_terms
863
+ permit_acting_for_organization when_actor_is_at_least: :admin
864
+ retain_with :ordinary_agreement_evidence
865
+ end
1092
866
  ```
1093
867
 
1094
- ```ruby
1095
- config.digest_canonical_receipts_with = :sha256
1096
- config.chain_event_history_with = :sha256
1097
- config.anchor_event_history_with = MyIndependentAnchor.new
1098
- config.timestamp_receipts_with = MyRfc3161TimestampProvider.new
868
+ ```erb
869
+ <%= form.clickwrap :organization_terms,
870
+ acting_for: current_organization,
871
+ submit: "Accept for #{current_organization.name}" %>
1099
872
  ```
1100
873
 
1101
- A local hash is never called tamper-proof. Server-recorded time is never called trusted time. An IP address is never called identity. Provider receipts are never upgraded into guarantees the provider did not make.
874
+ Authority is checked when the form is presented and reread from the membership
875
+ inside the capture transaction. The receipt records the human actor,
876
+ represented organization, actual role at both moments, authority criterion,
877
+ source, and verification times separately. Clickwrap records the configured
878
+ application-authorization fact; it does not decide whether that role is legally
879
+ sufficient. The same integration can create a brand-new organization and its
880
+ owner membership atomically through `create_represented_party_with_clickwrap`.
881
+ See [Binding an organization through a human actor](guides/organizations.md).
1102
882
 
1103
- Run verification continuously:
883
+ ## Recipes
1104
884
 
1105
- ```bash
1106
- bin/rails clickwrap:verify
1107
- bin/rails clickwrap:verify EVENT_ID
885
+ Two situations come up in almost every real app. Here's exactly how to handle both.
886
+
887
+ ### "Accept the new Terms to continue" — wall the app until updated Terms are accepted
888
+
889
+ You know how Apple Developer releases new terms every few months and walls off the entire dashboard until you accept them? Same pattern here: legal ships a new version of your Terms, and nobody uses your app again until they've agreed to it. Accepting the new version supersedes the old one — and you keep a receipt for every version each user ever agreed to, so you always know exactly who agreed to exactly what, and when.
890
+
891
+ Bump the document version when the new text ships — in the file itself, beside
892
+ the words that changed. The top of `app/content/legal/terms.md`:
893
+
894
+ ```markdown
895
+ ---
896
+ title: Terms of Service
897
+ last_updated: 2026-11-01
898
+ ---
1108
899
  ```
1109
900
 
1110
- ## Multi-tenancy, actors, subjects, and authority
901
+ It was `2026-08-15`; new words mean a new label. (A trailing `# comment` on
902
+ that line is read as YAML reads it — not part of the label.)
1111
903
 
1112
- The conventional actor is `User`, but nothing is hard-coded:
904
+ And require the current version in the policy:
1113
905
 
1114
906
  ```ruby
1115
- Clickwrap.configure do |config|
1116
- config.actor_class_name = "Account"
1117
- config.current_actor_method_name = :current_account
907
+ # config/clickwrap.rb
908
+ Clickwrap.policy :current_terms do
909
+ agree_to :terms, require_current_version: true
1118
910
 
1119
- config.find_current_tenant_with = lambda do |controller|
1120
- controller.current_organization
1121
- end
911
+ retain_with :ordinary_agreement_evidence
1122
912
  end
1123
913
  ```
1124
914
 
1125
- Actors, subjects, and tenants are separate:
915
+ Mount the built-in acceptance screen and wall the app:
1126
916
 
1127
917
  ```ruby
1128
- Clickwrap.capture!(
1129
- :logo_rights_declaration,
1130
- actor: current_user,
1131
- subject: @organization,
1132
- tenant: current_organization,
1133
- http_request: request,
1134
- submission: clickwrap_submission
1135
- )
918
+ # config/routes.rb
919
+ mount Clickwrap::Engine => "/agreements"
1136
920
  ```
1137
921
 
1138
- Actor snapshots include only configured fields. Clickwrap never serializes a whole user or domain object into evidence.
1139
-
1140
- Authentication, actor, organization, and subject are not collapsed into one polymorphic ID. A signed-in employee acting for an organization can be represented explicitly:
1141
-
1142
922
  ```ruby
1143
- Clickwrap.capture!(
1144
- :organization_terms,
1145
- actor: current_user,
1146
- acting_for: current_organization,
1147
- subject: contract,
1148
- authentication_context: clickwrap_authentication_context,
1149
- http_request: request,
1150
- submission: clickwrap_submission
1151
- )
923
+ class ApplicationController < ActionController::Base
924
+ # Nobody gets past this until they've accepted the current Terms. Clickwrap's
925
+ # own acceptance, receipt, withdrawal, and document screens stay reachable
926
+ # automatically, so this cannot redirect-loop its remediation page.
927
+ requires_clickwrap :current_terms
928
+ end
1152
929
  ```
1153
930
 
1154
- By default, the configured actor must match the authenticated principal. Delegation, guardianship, service-account action, and impersonation are rejected unless the policy and host authority adapter explicitly permit them. When permitted, the receipt preserves the authenticated principal, asserted actor, represented party, authority source, role, and verification time as separate facts; Clickwrap does not decide whether that authority is legally sufficient.
931
+ Publish the new version and every signed-in user gets redirected to the acceptance screen on their next request and sent back to wherever they were going the moment they accept. Preview the blast radius before you activate it:
932
+
933
+ ```bash
934
+ bin/rails clickwrap:reacceptance:plan POLICY=current_terms
935
+ ```
1155
936
 
1156
- ### Anonymous actors
937
+ What you get for free: the new acceptance supersedes the old one (`agreed → superseded`) without rewriting anything, and every receipt pins the exact version, locale, and byte digest of what each user agreed to — so "which exact Terms did this person accept, and when?" stays answerable years later.
1157
938
 
1158
- Use a host-owned stable opaque identifier—not an IP address:
939
+ Want to wall off only *parts* of the app instead? Gates are per-controller and per-action, and different areas can require different policies:
1159
940
 
1160
941
  ```ruby
1161
- actor = Clickwrap.anonymous_actor("checkout_#{signed_checkout_id}")
942
+ class BillingController < ApplicationController
943
+ requires_clickwrap :current_terms
944
+ end
945
+
946
+ class Api::DashboardController < ApplicationController
947
+ requires_clickwrap :developer_terms, only: %i[show update]
948
+ end
1162
949
  ```
1163
950
 
1164
- The host owns later account linking and identity/capacity decisions.
951
+ ### "I agree" before the account even exists signup, Google sign-in
952
+
953
+ At signup, people click "I agree" before they have an account with you: there's no `current_user` to hang the acceptance on yet, and the acceptance has to survive account creation. `clickwrap` models this honestly as a *prospective-actor* flow — the acceptance binds to a short-lived signed registration flow, then the account and its acceptance evidence commit in one database transaction, and the receipt records that this was an account registration (not an authenticated session).
1165
954
 
1166
- ### System-created records and explicit exemptions
955
+ For plain email/password signup, the Devise and Rails-authentication adapters above already do all of this — `form.clickwrap :signup` in your signup form is the whole integration.
1167
956
 
1168
- Seeds, imports, administrators, invitations, and service accounts must never “accept” by omitting a browser parameter or by fabricating a human click:
957
+ For Google sign-in (OAuth, One Tap), the click happens on Google's side, so put the acceptance on a "finish creating your account" screen after the callback:
1169
958
 
1170
959
  ```ruby
1171
- Clickwrap.exempt!(
1172
- :signup,
1173
- actor: Clickwrap.system_actor("database_seed"),
1174
- subject: user,
1175
- because: "Generated demo account; no human signup occurred"
1176
- )
960
+ # The OAuth callback doesn't create the account yet — it stashes what Google
961
+ # said and sends the person to finish signing up.
962
+ def google
963
+ session[:pending_oauth] = request.env["omniauth.auth"].slice("provider", "uid", "info")
964
+ redirect_to new_finish_signup_path
965
+ end
966
+ ```
967
+
968
+ ```erb
969
+ <%# The finish screen: name and email prefilled from Google, plus your Terms. %>
970
+ <%= form_with model: @user, url: finish_signup_path do |form| %>
971
+ <%= form.clickwrap :signup, submit: "Create account" %>
972
+ <% end %>
1177
973
  ```
1178
974
 
1179
- The event is an `exemption`, not an agreement. Policies can permit or reject it explicitly. Every exemption records who/what created it and why.
975
+ ```ruby
976
+ def create
977
+ @user = User.new(user_attributes_from(session[:pending_oauth]))
1180
978
 
1181
- Exemptions never satisfy `agreed_to?`, `consented_to?`, or another human-action predicate unless a policy asks the separate `exempted_from?` question. There is no “missing checkbox means system account” inference.
979
+ # Account + acceptance commit together, or neither happens. A refused
980
+ # submission re-renders the finish screen with the reason beside the control.
981
+ unless register_with_clickwrap(:signup, user: @user) { @user.save! }
982
+ return render :new, status: :unprocessable_entity
983
+ end
1182
984
 
1183
- ## Hotwire, Hotwire Native, APIs, and no-JavaScript flows
985
+ session.delete(:pending_oauth)
986
+ sign_in @user
987
+ redirect_to root_path
988
+ end
989
+ ```
1184
990
 
1185
- The default helper is server-rendered HTML and works with:
991
+ The registration flow lives in your session and the presentation token is valid for two hours by default, so both comfortably survive the round-trip to Google and back. One thing `clickwrap` will not do, on purpose: record an agreement from the OAuth callback alone. "By continuing you agree" with no affirmative act isn't evidence of anything — a real acceptance step has to happen somewhere, and the finish screen is where it belongs.
1186
992
 
1187
- - normal full-page requests;
1188
- - Turbo Drive and Turbo Frames;
1189
- - validation re-renders with no JavaScript;
1190
- - Hotwire Native web screens;
1191
- - custom native/API presentations; and
1192
- - operator/admin surfaces.
993
+ ### One person accepts for the whole company — organization agreements
1193
994
 
1194
- No Stimulus controller is required for correctness. An optional tiny controller may improve disabled-submit affordances, but server validation and evidence capture work without it.
995
+ Your customer is a company — but companies don't click checkboxes, people do. When an admin accepts your business terms "for Acme Inc.", two facts matter and must never blur into each other: the *organization* is the party the terms are for, and a *specific human* performed the acceptance on its behalf. Years later, the question is always the same: exactly which person accepted for the company, and what authority did they have when they did?
1195
996
 
1196
- ### Hotwire Native
997
+ Declare in the policy who is allowed to accept for an organization — membership alone is deliberately not enough:
1197
998
 
1198
- Use the web component whenever possible. Legal-document links can open in the appropriate modal/sheet/external-browser context chosen by the host native shell. The same presentation token and receipt contract applies.
999
+ ```ruby
1000
+ Clickwrap.policy :organization_terms do
1001
+ agree_to :business_terms
1199
1002
 
1200
- Native path configuration remains host-owned. Mount/capture routes include both GET and form-action paths so validation stays in the intended navigation context.
1003
+ permit_acting_for_organization when_actor_is_at_least: :admin
1201
1004
 
1202
- ### JSON/API clients
1005
+ retain_with :ordinary_agreement_evidence
1006
+ end
1007
+ ```
1203
1008
 
1204
- Present a policy through the same server-owned presenter:
1009
+ Make the represented company conspicuous in the UI, and pass it as `acting_for:`:
1205
1010
 
1206
- ```ruby
1207
- presentation = Clickwrap.present(
1208
- :signup,
1209
- actor: api_actor,
1210
- locale: :es,
1211
- capture_channel: :native_api,
1212
- submit_button_text: "Crear cuenta"
1213
- )
1011
+ ```erb
1012
+ <p>You are accepting these terms for <strong><%= current_organization.name %></strong>.</p>
1214
1013
 
1215
- render json: presentation
1014
+ <%= form.clickwrap :organization_terms,
1015
+ acting_for: current_organization,
1016
+ submit: "Accept for #{current_organization.name}" %>
1216
1017
  ```
1217
1018
 
1218
- The client renders the declared statements and returns only the signed token plus answers:
1019
+ Then capture the acceptance and stamp the organization in one transaction, so the rest of your app can ask a plain domain question:
1219
1020
 
1220
1021
  ```ruby
1221
- Clickwrap.capture!(
1222
- :signup,
1223
- actor: api_actor,
1224
- capture_channel: :native_api,
1225
- submission: Clickwrap.submission_from(params),
1226
- client_reported_context: permitted_client_context
1227
- )
1022
+ def create
1023
+ organization = current_organization
1024
+
1025
+ capture_clickwrap_and!(:organization_terms, acting_for: organization) do |pending_receipt|
1026
+ organization.update!(terms_accepted_with_clickwrap_event_id: pending_receipt.event_id)
1027
+ end
1028
+
1029
+ redirect_to organization_settings_path
1030
+ end
1228
1031
  ```
1229
1032
 
1230
- `submission_from` reads only the signed presentation token and the answer keys/types declared by that manifest; unknown keys and malformed choices are rejected. Client-reported values remain explicitly labeled. They can never masquerade as server-observed IP address, server time, trusted identity, or provider-estimated IP geolocation.
1033
+ When the form is rendered, `clickwrap` verifies authority and signs that
1034
+ presentation-time source, role, criterion, and verification time into the
1035
+ manifest. At submit it requires a current membership in that exact
1036
+ organization and rereads and locks the membership role *inside* the capture
1037
+ transaction. An admin demoted between render and submit is refused; a still-
1038
+ authorized role change is recorded honestly as two different snapshots. A
1039
+ token rendered for one organization is rejected for another. The receipt keeps
1040
+ the human actor, represented organization, both authority checks, and the
1041
+ protected outcome as separate facts. An organizational acceptance never
1042
+ quietly answers a personal one, and vice versa:
1231
1043
 
1232
- ## Accessible defaults without a fake certification
1044
+ ```ruby
1045
+ user.clickwraps.current_for?(:organization_terms, acting_for: organization) # => true
1046
+ user.clickwraps.current_for?(:organization_terms) # => false
1047
+ ```
1233
1048
 
1234
- The reference helper and views ship with tested:
1049
+ That receipt is exactly what you'll be asked to produce if the agreement is ever disputed: who accepted, for which company, in what role, verified when. Whether that role was *sufficient to bind the company* is a question for your counsel when they choose the `when_actor_is_at_least:` criterion — `clickwrap` records the facts that answer it. Works out of the box with the [`organizations`](https://github.com/rameerez/organizations) gem, or with your own authority model via a registered adapter. The [organizations guide](guides/organizations.md) has the full walkthrough.
1235
1050
 
1236
- - explicit labels and programmatic names;
1237
- - initially unselected controls;
1238
- - visible keyboard focus;
1239
- - high-contrast conventional links;
1240
- - `aria-invalid` and `aria-describedby` error relationships;
1241
- - error summary and focus behavior;
1242
- - keyboard operation;
1243
- - non-color-only meaning;
1244
- - no-JavaScript validation;
1245
- - locale-aware document selection; and
1246
- - review/correction support for consequential submissions.
1051
+ If the organization does not exist until this same form creates it, opt into
1052
+ that materially different flow explicitly:
1247
1053
 
1248
- The whole host page still determines placement, clutter, contrast, action wording, accessibility, and notice quality. Clickwrap can lint known hazards; it cannot certify a host application as accessible or an agreement as enforceable.
1054
+ ```ruby
1055
+ Clickwrap.policy :organization_creation do
1056
+ declare :authority_and_content_rights,
1057
+ statement: "I am authorized to create and act for this organization and may use the content I submit.",
1058
+ document: nil,
1059
+ protected_outcome_version: "created-organization-v1",
1060
+ record_protected_outcome_with: ->(organization) {
1061
+ Clickwrap.protected_outcome(
1062
+ action: :created,
1063
+ record: organization,
1064
+ facts: { name: organization.name }
1065
+ )
1066
+ }
1249
1067
 
1250
- ## Operations you can understand at 03:00
1068
+ permit_acting_for_organization(
1069
+ when_actor_is_at_least: :owner,
1070
+ including_when_this_action_creates_the_organization: true
1071
+ )
1251
1072
 
1252
- ```bash
1253
- bin/rails clickwrap:doctor
1254
- bin/rails clickwrap:publish:plan
1255
- bin/rails clickwrap:publish
1256
- bin/rails clickwrap:reacceptance:plan POLICY=current_terms
1257
- bin/rails clickwrap:verify
1258
- bin/rails clickwrap:export EVENT_ID
1259
- bin/rails clickwrap:retention:plan
1260
- bin/rails clickwrap:retention:apply PLAN=PLAN_ID
1261
- bin/rails clickwrap:holds:review
1262
- bin/rails clickwrap:privacy:inventory
1263
- bin/rails clickwrap:reconcile_external_actions
1073
+ retain_with :ordinary_agreement_evidence
1074
+ end
1264
1075
  ```
1265
1076
 
1266
- `clickwrap:doctor` reports objective configuration and data facts:
1267
-
1268
- ```text
1269
- 6 policies compiled
1270
- all referenced documents are published and digest-verified
1271
- signup has an atomic Devise integration
1272
- ✓ every required gate has a remediation route
1273
- ✓ request-derived personal data is off by default
1274
- ! withdrawal_authorization records IP geolocation city without a review date
1275
- ! Cloudflare source trust is unverified
1276
- ✓ no overdue disposition jobs
1277
- ✓ all checked event digests verify
1077
+ ```erb
1078
+ <%= form_with model: @organization do |form| %>
1079
+ <%= form.clickwrap :organization_creation,
1080
+ acting_for: @organization,
1081
+ submit: "Create organization" %>
1082
+ <% end %>
1278
1083
  ```
1279
1084
 
1280
- It never prints “compliant,” “court-proof,” or “audit guaranteed.”
1085
+ ```ruby
1086
+ create_represented_party_with_clickwrap(
1087
+ :organization_creation,
1088
+ represented_party: @organization
1089
+ ) do |pending_receipt|
1090
+ @organization.save!
1091
+ @organization.add_member!(current_user, role: :owner)
1092
+ @organization.update!(creation_clickwrap_event_id: pending_receipt.event_id)
1093
+ @organization
1094
+ end
1095
+ ```
1281
1096
 
1282
- Metrics and notifications use stable policy/kind/outcome names without raw personal data labels. Sensitive values never appear in ordinary logs, exceptions, `inspect`, notifications, or metrics.
1097
+ The form helper creates a server-owned browser-flow binding automatically. The
1098
+ manifest says authority is `not_yet_verifiable` because the membership does not
1099
+ exist yet; after the protected block returns the persisted organization and
1100
+ creates its owner membership, the adapter verifies them and Clickwrap rebinds the final GlobalID
1101
+ before commit. If any part fails, none of the organization, membership,
1102
+ evidence, or protected outcome commits. The explicit declaration is still what
1103
+ records the human's claim of pre-existing real-world authority: an owner role
1104
+ created by the transaction proves an application fact, not the truth or legal
1105
+ sufficiency of that claim.
1283
1106
 
1284
- ## Testing is a first-class API
1107
+ ## Testing your integration
1285
1108
 
1286
- Include the helpers in Minitest:
1109
+ Documents must be published in the test database too — presentations refuse unpublished documents in tests exactly as in production:
1287
1110
 
1288
1111
  ```ruby
1112
+ # test/test_helper.rb
1289
1113
  class ActiveSupport::TestCase
1290
1114
  include Clickwrap::TestHelpers
1115
+ parallelize_setup { Clickwrap.publish! } # once per parallel worker...
1291
1116
  end
1117
+ Clickwrap.publish! # ...and once per process
1292
1118
  ```
1293
1119
 
1294
- Create real, internally consistent test evidence without knowing table details:
1295
-
1296
1120
  ```ruby
1297
- receipt = capture_clickwrap(
1298
- :signup,
1299
- actor: user,
1300
- answers: { terms: true, privacy_notice: true }
1301
- )
1121
+ receipt = submit_clickwrap(:signup, actor: user, answers: { terms: true, privacy_notice: true })
1302
1122
 
1303
1123
  assert_clickwrap_current :signup, actor: user
1304
1124
  assert_clickwrap_agreed_to :terms, actor: user
1305
- assert_clickwrap_acknowledged :privacy_notice, actor: user
1306
1125
  assert_clickwrap_receipt_verifies receipt
1307
1126
  ```
1308
1127
 
1309
- System-test helpers drive the actual UI:
1128
+ `submit_clickwrap` is the test factory: it presents the policy through the real presenter, answers it, and captures — and it *raises* when the capture is refused, because in a test a failed capture is a failed test. That is deliberately a different verb from the controller's `capture_clickwrap`, which captures a submission a person actually sent and absorbs refusals into `false`. Same word for both would mean one name with two opposite answers to "what happens when this is refused".
1129
+
1130
+ Integration tests can't fabricate a signed presentation token by hand — that's the point — so they read it off the rendered page the way a browser does:
1131
+
1132
+ ```ruby
1133
+ post user_registration_path, params: {
1134
+ user: { email: "person@example.com", password: "a-real-password" },
1135
+ **clickwrap_params_from(new_user_registration_path) # GET the page, affirm everything
1136
+ }
1137
+
1138
+ # Decline one statement instead:
1139
+ declined = clickwrap_params_from(new_user_registration_path, answers: { terms: false })
1140
+
1141
+ # Choice statements submit their real rendered values. By default the helper
1142
+ # selects the first offered radio choice; name a different choice explicitly:
1143
+ contractor = clickwrap_params_from(
1144
+ new_user_registration_path,
1145
+ answers: { employment_kind: "contractor" }
1146
+ )
1147
+ ```
1148
+
1149
+ Checkbox statements default to their affirmative value. Radio statements
1150
+ default to the first choice rendered by the application, so tests exercise a
1151
+ value the server actually offered instead of a fabricated checkbox value.
1152
+ Pass the exact choice key when the choice matters. For a conventional
1153
+ `yes`/`no` radio group, `false` selects `no`; explicit choice keys remain the
1154
+ clearest option for domain-specific choices.
1155
+
1156
+ If one page renders several independent Clickwrap forms, select the exact form;
1157
+ the helper refuses an ambiguous page instead of combining one form's token with
1158
+ another form's answers:
1310
1159
 
1311
1160
  ```ruby
1312
- complete_clickwrap :signup
1313
- click_button "Create account"
1161
+ submission = clickwrap_submission_params_from(
1162
+ response,
1163
+ form_css_selector: "form[action='/withdrawals/confirm']"
1164
+ )
1314
1165
  ```
1315
1166
 
1316
- Fault injection proves required atomicity:
1167
+ Fault injection proves the atomicity claim in your own suite:
1317
1168
 
1318
1169
  ```ruby
1319
1170
  Clickwrap::Testing.fail_next_event_write do
1320
- assert_raises(Clickwrap::EventWriteFailed) do
1321
- perform_signup
1322
- end
1171
+ assert_raises(Clickwrap::EventWriteFailed) { perform_signup }
1323
1172
  end
1324
-
1325
1173
  assert_not User.exists?(email: "person@example.com")
1326
- assert_no_clickwrap_event :signup
1327
1174
  ```
1328
1175
 
1329
- Concurrency, duplicate-submit, stale-token, actor/subject swap, disposition, legal-hold, export round-trip, and legacy-import helpers ship with the gem. No tests make real provider network calls.
1176
+ ## Configuration
1330
1177
 
1331
- ## The generated initializer explains itself
1332
-
1333
- The complete initializer is annotated in plain English. A representative configuration looks like:
1178
+ The generated initializer is fully annotated and every setting reads like a sentence. The essentials:
1334
1179
 
1335
1180
  ```ruby
1336
1181
  # config/initializers/clickwrap.rb
1337
1182
  Clickwrap.configure do |config|
1338
1183
  config.actor_class_name = "User"
1339
1184
  config.current_actor_method_name = :current_user
1340
- config.parent_controller_class_name = "ApplicationController"
1341
-
1342
- config.find_current_tenant_with = lambda do |controller|
1343
- controller.current_organization if controller.respond_to?(:current_organization)
1344
- end
1345
1185
 
1346
1186
  config.authorize_receipt_access_with = lambda do |controller, receipt|
1347
1187
  controller.current_user == receipt.actor
1348
1188
  end
1349
1189
 
1350
- config.authorize_unredacted_request_evidence_access_with =
1351
- lambda do |controller, receipt, because|
1352
- controller.current_user&.security_operator? && because.present?
1353
- end
1354
-
1355
- config.identify_actor_with = ->(actor) { actor.to_gid.to_s }
1356
- # Add only reviewed fields your receipts truly need; never serialize the model.
1357
- config.snapshot_actor_with = ->(_actor) { {} }
1358
- config.describe_authentication_with = lambda do |controller|
1359
- { method: :authenticated_session, authenticated_at: controller.session[:authenticated_at] }
1360
- end
1190
+ # Safe defaults: no IP address, browser user-agent, or IP geolocation is stored.
1191
+ # Enable fields per policy, each with a plain-English purpose and retention rule.
1361
1192
 
1362
- config.store_document_contents_in = :database
1363
- config.digest_canonical_receipts_with = :sha256
1364
- config.chain_event_history_with = nil
1365
- config.anchor_event_history_with = nil
1366
- config.timestamp_receipts_with = nil
1367
- config.application_version = -> { ENV["RELEASE_SHA"] }
1368
-
1369
- # Safe defaults: no raw network/browser/geolocation data is stored.
1370
- config.record_ip_address_by_default = false
1371
- config.record_browser_user_agent_by_default = false
1372
- config.record_ip_geolocation_country_by_default = false
1373
- config.record_ip_geolocation_region_by_default = false
1374
- config.record_ip_geolocation_city_by_default = false
1375
- config.record_ip_geolocation_postal_code_by_default = false
1376
- config.record_ip_geolocation_latitude_and_longitude_by_default = false
1377
- config.record_ip_geolocation_timezone_by_default = false
1378
- config.record_ip_geolocation_continent_by_default = false
1379
- config.record_ip_geolocation_metro_code_by_default = false
1380
- config.record_ip_geolocation_accuracy_radius_in_kilometers_by_default = false
1381
-
1382
- # If a default above becomes true, fill in the matching plain-English
1383
- # reason and a retention rule below. The policy compiler rejects an
1384
- # enabled default whose purpose or retention is blank.
1385
- config.reason_for_recording_ip_addresses_by_default = nil
1386
- config.reason_for_recording_browser_user_agents_by_default = nil
1387
- config.reason_for_recording_ip_geolocation_by_default = nil
1388
- config.legal_basis_reference_for_recording_ip_addresses_by_default = nil
1389
- config.legal_basis_reference_for_recording_browser_user_agents_by_default = nil
1390
- config.legal_basis_reference_for_recording_ip_geolocation_by_default = nil
1391
- config.review_default_request_evidence_configuration_on = nil
1392
-
1393
- config.encrypt_recorded_ip_addresses = true
1394
- config.encrypt_recorded_browser_user_agents = true
1395
- config.encrypt_recorded_ip_geolocation = true
1396
-
1397
- # Nil means every policy that enables the field must supply its own rule.
1398
- config.delete_recorded_ip_addresses_after = nil
1399
- config.delete_recorded_browser_user_agents_after = nil
1400
- config.delete_recorded_ip_geolocation_after = nil
1401
-
1402
- config.read_ip_address_from_http_request_with =
1403
- ->(http_request) { http_request.remote_ip }
1404
-
1405
- config.read_browser_user_agent_from_http_request_with =
1406
- ->(http_request) { http_request.user_agent }
1407
-
1408
- config.ip_geolocation_resolver = nil
1409
- config.fail_capture_when_ip_geolocation_is_unavailable = false
1410
-
1411
- # Runs only after required evidence and domain state have committed.
1412
- # Hook failures are reported but can never undo the committed action.
1193
+ # Optional hooks run only after evidence and domain state have committed:
1413
1194
  config.after_event_is_committed = ->(event) { }
1414
- config.report_after_commit_failure_with = ->(error, event) { Rails.error.report(error) }
1415
1195
  end
1416
1196
  ```
1417
1197
 
1418
- Every public setting validates its value and reads like a sentence. Class names are resolved lazily for Rails autoloading. Security-critical ambiguity fails at boot instead of becoming a surprising runtime default. A policy-level request-evidence declaration overrides these application defaults, so a high-risk authorization can collect more context without making ordinary signup inherit it.
1198
+ Only your decisions are live in that file. Every setting left at the gem's default appears commented with its value, under prose explaining what it does, so a reader can tell at a glance which lines somebody chose. The one deliberate exception is the request-evidence block: each `record_*_by_default` line is written even when it says `false`, because each is an answer to a question the installer asked, and "we decided not to collect this" is worth reading rather than inferring from a file that does not mention it.
1419
1199
 
1420
- ## Generators
1200
+ Class names are strings resolved lazily for autoloading, and ambiguity fails at boot instead of becoming a surprising runtime default. Optional external integrations are explicit: anchoring and timestamping are off (`nil`) until an adapter is configured; optional hook procs have working no-op defaults; and geolocation/document integrations run only when their corresponding policy or storage choice asks for them.
1421
1201
 
1422
- ```bash
1423
- bin/rails generate clickwrap:install
1424
- bin/rails generate clickwrap:policy driver_declaration
1425
- bin/rails generate clickwrap:document terms
1426
- bin/rails generate clickwrap:views
1427
- bin/rails generate clickwrap:hardening --database
1428
- bin/rails generate clickwrap:upgrade
1429
- ```
1430
-
1431
- The installer:
1432
-
1433
- - detects integer/UUID keys and supported database features;
1434
- - detects Rails authentication and Devise without making either a hard dependency;
1435
- - stops and explains itself when actor/tenant mappings are ambiguous;
1436
- - asks before wiring signup or mounting routes;
1437
- - asks separately about every request-evidence field;
1438
- - writes plain-English purposes and retention placeholders that must be reviewed;
1439
- - never overwrites host files without normal Rails generator conflict handling; and
1440
- - prints a post-install checklist for documents, semantics, privacy, retention, trusted proxies, full-page UI review, and tests.
1441
-
1442
- Upgrade generators create new migrations. Released migrations are never silently edited underneath an application.
1443
-
1444
- ## Migrate without inventing history
1445
-
1446
- ### From FinePrint
1447
-
1448
- Preview first:
1449
-
1450
- ```bash
1451
- bin/rails clickwrap:import:fine_print:plan
1452
- ```
1453
-
1454
- Then import:
1455
-
1456
- ```bash
1457
- bin/rails clickwrap:import:fine_print
1458
- ```
1202
+ ### The presentation linter
1459
1203
 
1460
- FinePrint contract versions and signatures become explicit `imported_legacy` events. Fields FinePrint did not recordpresentation manifest, IP address, CTA, protected action—remain `unknown` or `not_collected`; Clickwrap never synthesizes them.
1461
-
1462
- ### From `accepted_terms_at`
1463
-
1464
- ```ruby
1465
- Clickwrap.import_legacy!(
1466
- :terms,
1467
- actor: user,
1468
- occurred_at: user.accepted_terms_at,
1469
- known: {
1470
- document_version: user.terms_version
1471
- },
1472
- unknown: %i[
1473
- exact_document_bytes
1474
- presentation
1475
- assertion
1476
- submit_button_text
1477
- request_evidence
1478
- ],
1479
- because: "Imported from users.accepted_terms_at"
1480
- )
1481
- ```
1482
-
1483
- Imports are append-only, provenance-labeled, idempotent, dry-runnable, and report every unknown. Historical weakness remains visible instead of being laundered into modern certainty.
1484
-
1485
- ## Extension seams, not dependency soup
1486
-
1487
- The core has small adapter contracts for:
1488
-
1489
- - document storage;
1490
- - actor/tenant resolution;
1491
- - identity/authentication snapshots;
1492
- - IP geolocation;
1493
- - independent checkpoints/anchors;
1494
- - RFC 3161 or trust-service timestamps;
1495
- - external clickwrap/signature providers;
1496
- - object-lock/WORM storage;
1497
- - PDF rendering;
1498
- - authorization;
1499
- - error reporting;
1500
- - notifications; and
1501
- - post-commit analytics/auditing.
1502
-
1503
- Every optional adapter has a no-op default and explicit capability reporting. Installing Clickwrap never pulls in Redis, Sidekiq, Devise, Trackdown, Active Storage, a PDF library, a cloud SDK, or an external service unless the application chooses that integration.
1504
-
1505
- ActiveSupport notifications are available for instrumentation:
1204
+ In development and test, every render is scanned for the mistakes a form can make silently a preselected consent control, a consent sentence carrying two purposes, a document link below the submit button, a missing presentation token. Findings go to the log as warnings and never raise: a lint finding is a thing to look at, not a reason to stop a page from rendering. It is off in production, because a production request has no business scanning its own HTML on the way out.
1506
1205
 
1507
1206
  ```ruby
1508
- ActiveSupport::Notifications.subscribe("event_committed.clickwrap") do |event|
1509
- # event payload contains stable IDs and categories, not raw request evidence
1510
- end
1207
+ config.lint_presentations = false # or true to run it in another environment
1511
1208
  ```
1512
1209
 
1513
- Required writes are never delegated to notifications. Hooks are for observers, not authorization.
1514
-
1515
- ## What Clickwrap does, what your application owns, and what the receipt proves
1516
-
1517
- | Area | Clickwrap provides | Your application/counsel owns | Receipt/evidence |
1518
- |---|---|---|---|
1519
- | Documents | immutable versions, bytes/digests, locales, publication | text, translation, fairness, legal approval, materiality | exact stored version and digest |
1520
- | Presentation | tested controls/helper, manifest, token, stale/replay checks | whole-page placement/design, final CTA, accessibility review | server-generated manifest and accepted answers |
1521
- | Actor | configured reference and authentication snapshot | identity proofing, capacity, authority, guardian/organization rules | exactly which configured actor/context was recorded |
1522
- | Agreements | version/current-state mechanics | enforceability, governing law, substantive terms | agreement event and historical version |
1523
- | Privacy notice | acknowledgment mechanics | transparency content and lawful basis for processing | notice version and acknowledgment event |
1524
- | Consent | purposes, grant/withdrawal/renewal lifecycle | whether consent is the correct basis and whether it is freely given | exact grant/withdrawal history |
1525
- | Declarations | statement snapshot, expiry/correction/supersession | truth, eligibility, domain validation | what was declared, when, for which subject |
1526
- | Authorizations | scope, fingerprint, freshness, one-time consumption | domain permission and external-provider consequences | exact evidence-to-outcome binding |
1527
- | Request evidence | explicit capture, provenance, encryption/redaction/disposition | necessity, lawful basis, disclosure, trusted proxy/source, period | selected fields and honest source/state |
1528
- | Integrity | canonical digests, verification, optional chains/adapters | keys, infrastructure, access controls, backups, operational procedures | verification result and bounded assurance tier |
1529
- | Retention | executable rules, holds, dry-run disposition | legally appropriate periods and case-specific holds | retention/hold/disposition history |
1210
+ `nil` (the default) means "decide from the environment".
1530
1211
 
1531
- Clickwrap is engineering infrastructure, not legal advice or a compliance certificate.
1212
+ ## Operations
1532
1213
 
1533
- ## What Clickwrap deliberately does not become
1534
-
1535
- Clickwrap does not:
1536
-
1537
- - draft or approve your legal documents;
1538
- - choose a GDPR lawful basis or special-category condition;
1539
- - decide whether a document change is material;
1540
- - guarantee enforceability, admissibility, accessibility, or audit acceptance;
1541
- - verify identity, age, capacity, guardianship, or organizational authority;
1542
- - provide KYC, sanctions screening, fraud scoring, or biometrics;
1543
- - become a cookie CMP, tracker scanner, or script blocker;
1544
- - become DocuSign, Ironclad, a notary, a qualified trust-service provider, or a contract lifecycle platform;
1545
- - call a local hash tamper-proof;
1546
- - call an IP address identity or IP geolocation physical location;
1547
- - require forced scrolling or claim it proves reading;
1548
- - require a sprawling admin/document-authoring suite; or
1549
- - hide collection behind `compliant: true` or `maximum_evidence: true`.
1550
-
1551
- Adapters let those systems contribute provider receipts without changing what Clickwrap itself claims.
1552
-
1553
- ## FinePrint and Clickwrap solve different-sized problems
1554
-
1555
- [FinePrint](https://github.com/openstax/fine_print/blob/3b75fbcbcfb048ecd2f4ee7c4f0b9bd3d10f7603/README.md#L7-L25) is established Rails prior art for versioned contracts, signatures, gates, and views. Clickwrap should never market itself as the first Rails agreement gem.
1556
-
1557
- FinePrint’s documented core and [signature model at the audited commit](https://github.com/openstax/fine_print/blob/3b75fbcbcfb048ecd2f4ee7c4f0b9bd3d10f7603/app/models/fine_print/signature.rb#L1-L33) answer:
1558
-
1559
- ```text
1560
- Did user U sign version N of contract X?
1561
- ```
1562
-
1563
- Clickwrap is for applications that also need to answer:
1564
-
1565
- ```text
1566
- Which exact content and presentation was offered?
1567
- Which explicit statements and choices were made?
1568
- Did the required evidence and protected outcome commit together?
1569
- What subject or transaction did it cover?
1570
- Was it withdrawn, corrected, superseded, expired, or consumed?
1571
- Can the complete receipt be reproduced and verified independently?
1572
- Can optional personal request evidence be disposed of honestly?
1214
+ ```bash
1215
+ bin/rails clickwrap:doctor # objective health report, never prints "compliant"
1216
+ bin/rails clickwrap:publish # freeze document snapshots (idempotent; also rides db:prepare)
1217
+ bin/rails clickwrap:verify # verify event digests
1218
+ bin/rails clickwrap:retention:plan # preview disposition
1219
+ bin/rails clickwrap:privacy:inventory # every configured personal-data field, purpose, and rule
1220
+ bin/rails clickwrap:import:fine_print # migrate from FinePrint without inventing history
1573
1221
  ```
1574
1222
 
1575
- The goal is to be easier in the first five minutes and dramatically stronger after five years in production—not FinePrint with more columns.
1576
-
1577
- ## Legal and evidentiary posture
1578
-
1579
- Electronic form does not cure an invalid underlying transaction, missing capacity/authority, or a special formality. The US E-SIGN Act preserves electronic validity while retaining substantive requirements and exclusions ([15 U.S.C. § 7001](https://www.law.cornell.edu/uscode/text/15/7001); [15 U.S.C. § 7003](https://www.law.cornell.edu/uscode/text/15/7003)). Electronic form also does not make an unfair term fair ([Directive 93/13/EEC](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=celex%3A31993L0013)). EU eIDAS distinguishes ordinary electronic evidence from qualified electronic signatures and their specific legal effect ([Regulation (EU) No 910/2014, Article 25](https://eur-lex.europa.eu/eli/reg/2014/910/2024-05-20/eng)).
1580
-
1581
- US appellate formation decisions evaluate conspicuous notice and unambiguous assent in the context of the whole interface; no checkbox color or placement is a universal safe harbor ([Berman v. Freedom Financial Network](https://cdn.ca9.uscourts.gov/datastore/opinions/2022/04/05/20-16900.pdf); [Tejon v. Zeus Networks](https://media.ca11.uscourts.gov/opinions/pub/files/202411114.pdf); [Toth v. Everly Well](https://www.ca1.uscourts.gov/sites/ca1/files/opnfiles/23-1727P-01A.pdf)).
1582
-
1583
- GDPR consent must be demonstrable, distinguishable, and withdrawable, but consent is only one possible lawful basis. A privacy-information acknowledgment is not blanket consent ([GDPR Article 6](https://eur-lex.europa.eu/eli/reg/2016/679/art_6/oj/eng); [GDPR Article 7](https://eur-lex.europa.eu/eli/reg/2016/679/art_7/oj/eng); [AEPD FAQ 02.48](https://www.aepd.es/preguntas-frecuentes/2-tus-obligaciones-como-responsable-del-tratamiento/6-el-deber-de-informacion/FAQ-0248-sobre-si-el-usuario-tiene-que-dar-consentimiento-a-clausula-de-privacidad)). GDPR also requires purpose limitation, data minimization, storage limitation, transparency, and security; “collect everything forever” is not the evidence-maximizing default ([Article 5](https://eur-lex.europa.eu/eli/reg/2016/679/art_5/oj/eng); [Article 13](https://eur-lex.europa.eu/eli/reg/2016/679/art_13/oj/eng); [Article 32](https://eur-lex.europa.eu/eli/reg/2016/679/art_32/oj/eng)).
1584
-
1585
- These sources motivate Clickwrap’s design. They do not turn the gem into legal advice or a universal safe harbor.
1223
+ Migrating from FinePrint or a bare `accepted_terms_at` column? Clickwrap's importer appends provenance-labeled events through its supported API: fields the old system never recorded stay `unknown` instead of being laundered into modern certainty. Direct database privileges remain outside that API's boundary. See the [migration guide](guides/migrating.md).
1586
1224
 
1587
- ## Security model
1225
+ ## Will this hold up in court?
1588
1226
 
1589
- Clickwrap treats these as hostile until verified:
1227
+ Here's the honest version, in plain words, because you deserve better than marketing copy on this question.
1590
1228
 
1591
- - policy/document/version/validity values submitted by the client;
1592
- - stale or replayed presentation tokens;
1593
- - swapped actor, tenant, subject, or transaction IDs;
1594
- - forwarded IP and Cloudflare headers outside a verified proxy path;
1595
- - client timestamps and client-reported identity/location;
1596
- - duplicate/concurrent submits;
1597
- - mutable document sources;
1598
- - after-commit analytics and provider callbacks; and
1599
- - imported evidence without provider provenance.
1229
+ Electronic form alone is not a reason to deny a contract legal effect under the US E-SIGN Act
1230
+ ([15 U.S.C. § 7001](https://www.law.cornell.edu/uscode/text/15/7001)), and the EU's eIDAS
1231
+ regulation says an electronic signature may not be denied legal effect or admissibility solely
1232
+ because it is electronic or not qualified ([Regulation 910/2014, Article 25](https://eur-lex.europa.eu/eli/reg/2014/910/2024-05-20/eng)).
1233
+ That does not decide what happens around the control in a particular downstream application:
1600
1234
 
1601
- Security-sensitive values are server-owned, signed/bound, rechecked inside the transaction, and represented by stable failure results. Rails’ CSRF/session/authentication protections remain host responsibilities. Encryption keys, signing keys, and adapter credentials use Rails credentials or application-provided key providers and support rotation with versioned key identifiers.
1235
+ **Courts read your whole page, not your checkbox.** In *Berman v. Freedom Financial Network* (a 2022 Ninth Circuit decision, [opinion](https://cdn.ca9.uscourts.gov/datastore/opinions/2022/04/05/20-16900.pdf)), the terms lost: the notice was in tiny gray font, the links to the terms didn't look like links, and the button said "Continue" without mentioning them — even though an acceptance flow existed. Other federal appeals courts run the same whole-interface analysis (*[Tejon v. Zeus Networks](https://media.ca11.uscourts.gov/opinions/pub/files/202411114.pdf)*, *[Toth v. Everly Well](https://www.ca1.uscourts.gov/sites/ca1/files/opnfiles/23-1727P-01A.pdf)*). Placement, font size, contrast, clutter, the words on the button: all decided by *your* page. `clickwrap` renders one accessible, initially-unselected component and records exactly what that component said — it cannot see, or fix, the rest of your screen.
1602
1236
 
1603
- Report vulnerabilities privately according to `SECURITY.md`. Do not open a public issue containing an exploit or real evidence/PII.
1237
+ **The words in your documents matter more than the click.** In the EU, an unfair term in a consumer contract doesn't bind the consumer even when the assent flow was otherwise effective ([Directive 93/13/EEC](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=celex%3A31993L0013)). A strong record of acceptance does not change the underlying term. The gem records your words; it can't make them fair.
1604
1238
 
1605
- ## Compatibility
1606
-
1607
- The ideal supported matrix is:
1608
-
1609
- - Ruby 3.2 through current Ruby, tested explicitly;
1610
- - Rails 7.1 through current Rails 8.x;
1611
- - PostgreSQL, SQLite, and MySQL for all documented portable core behavior;
1612
- - adapter-specific hardening clearly marked and tested;
1613
- - Rails authentication and Devise, both optional integrations;
1614
- - Turbo/Hotwire and ordinary HTML;
1615
- - integer and UUID primary keys;
1616
- - multi-database applications when evidence and protected action share the documented transaction boundary; and
1617
- - API-only applications for model/service/JSON receipt APIs, with HTML engine mounting optional.
1618
-
1619
- The gem depends only on the Rails components its approved surface needs. It does not depend on the `rails` meta-gem, Redis, a job backend, a JavaScript runtime, an external provider, or a CSS framework.
1239
+ **Who acted, and in which capacity.** `clickwrap` records the actor and the authentication and
1240
+ authority facts your application supplies; it does not establish identity, capacity, or legal
1241
+ authority. For an organization, it keeps the human actor distinct from the represented party and
1242
+ records the role or permission criterion your application checked—[see the recipe](#one-person-accepts-for-the-whole-company--organization-agreements).
1620
1243
 
1621
- The actual released gemspec and CI matrix—not this wishlist—are authoritative once implementation exists.
1244
+ **Your jurisdiction and your document type.** The US E-SIGN Act expressly excludes categories
1245
+ including wills, specified family-law matters, and specified notices
1246
+ ([15 U.S.C. § 7003](https://www.law.cornell.edu/uscode/text/15/7003)). In the EU, a
1247
+ *qualified* electronic signature has the equivalent legal effect of a handwritten signature;
1248
+ Article 25 separately says other electronic signatures may not be denied legal effect or
1249
+ admissibility solely because they are electronic or not qualified
1250
+ ([eIDAS Article 25](https://eur-lex.europa.eu/eli/reg/2014/910/2024-05-20/eng)).
1251
+ This gem does not produce or claim a qualified electronic signature.
1622
1252
 
1623
- ## Stability and upgrade promise
1624
-
1625
- Clickwrap follows semantic versioning for its documented Ruby/Rails APIs, but persisted evidence gets a stricter promise:
1253
+ **And GDPR consent is its own animal.** Consent has to be demonstrable and withdrawable ([GDPR Article 7](https://eur-lex.europa.eu/eli/reg/2016/679/art_7/oj/eng)) — `clickwrap` gives you both mechanics — but merely acknowledging a privacy notice is not consent (regulator guidance from Spain's AEPD, [FAQ 02.48](https://www.aepd.es/preguntas-frecuentes/2-tus-obligaciones-como-responsable-del-tratamiento/6-el-deber-de-informacion/FAQ-0248-sobre-si-el-usuario-tiene-que-dar-consentimiento-a-clausula-de-privacidad)). That's why `acknowledge` and `consent_to` are different verbs here, with different lifecycles.
1626
1254
 
1627
- - every released receipt schema, canonicalization profile, digest field, event action, and lifecycle meaning has a permanent golden fixture;
1628
- - new gem versions continue verifying old receipts even when they stop creating that old schema;
1629
- - a format change gets a new explicit schema/version and verifier, never a silent reinterpretation;
1630
- - upgrade generators add migrations and report their exact effects; released migration files are never edited under an installed application;
1631
- - destructive or lossy data transitions require a plan, explicit operator action, and rollback/export guidance;
1632
- - deprecations name the replacement and remain executable for a documented window; and
1633
- - security fixes distinguish a vulnerable capture path from a verifier/display-only issue so operators know what historical evidence, if any, needs review.
1255
+ Notice what's left after all of that: **evidence**. Those opinions examine what interface the
1256
+ application offered and what action it recorded—not whether checkboxes are valid in the abstract.
1257
+ Which exact version of the terms did the server bind to the form? What did the presentation
1258
+ manifest say beside the control? Was the control initially unselected? Which explicit submission
1259
+ did the server accept? Was consent later withdrawn? Most apps genuinely cannot reconstruct those
1260
+ application-side facts; `clickwrap` exists so you can, with a receipt verifiable without the
1261
+ producing application's source code. It still does not prove that a person perceived or
1262
+ understood the interface.
1634
1263
 
1635
- The project publishes the CI matrix, generator diffs, benchmark script, receipt golden fixtures, threat-model changes, and upgrade notes with every release. “It still boots” is not enough for a gem whose value is long-lived evidence.
1264
+ That's also why nothing in this gem prints "legally binding" or "court-proof": those are conclusions a court reaches about *your* agreement, under *your* jurisdiction's law, looking at *your* whole page and *your* terms. The gem's job is narrower and more useful making sure that when that day comes, your lawyer is holding the receipt.
1636
1265
 
1637
- ## Performance
1266
+ ## What `clickwrap` is *not*
1638
1267
 
1639
- The ordinary capture path is one bounded database transaction with no network call. Documents and compiled policies are cached by immutable digest. Request geolocation, timestamp providers, external anchors, PDFs, and analytics are optional and never hidden in the simple path.
1268
+ This gem provides evidence mechanics excellent ones and nothing else. It does not:
1640
1269
 
1641
- There is no global event-history mutex. Sequence/chain scope is per tenant or aggregate, benchmarked under contention, and independently checkpointed where enabled. Bulk export streams records and verifies incrementally.
1270
+ - draft or approve legal documents, or decide whether a change is "material";
1271
+ - claim compliance, enforceability, admissibility, or "court-proof" anything;
1272
+ - verify identity or age, or decide whether a configured role or permission is
1273
+ legally sufficient to bind an organization (identity, KYC, and legal capacity
1274
+ belong elsewhere);
1275
+ - become DocuSign, a notary, a cookie CMP, or a contract-lifecycle platform;
1276
+ - call a local hash tamper-proof, server time trusted time, or an IP address a person;
1277
+ - hide data collection behind a `compliant: true` switch.
1642
1278
 
1643
- Performance claims are published only with reproducible benchmarks against supported databases.
1279
+ Your application and its counsel own the legal text, lawful basis, retention periods, and jurisdiction-specific requirements. `clickwrap` makes configured decisions executable and traceable in evidence—it doesn't make them for you.
1644
1280
 
1645
1281
  ## FAQ
1646
1282
 
1647
1283
  ### Is this an electronic-signature gem?
1648
1284
 
1649
- It captures electronic evidence of explicit actions and can import/provider-bind signature receipts. It does not call ordinary clickwrap a qualified electronic signature, notarization, or trusted identity proof.
1650
-
1651
- ### Does a user have to open or scroll through the document?
1652
-
1653
- Not by universal default. Clickwrap makes the document available before action and records the exact presentation. A policy can require an accurately observed open/review interaction when the host has a real requirement, but Clickwrap never equates scrolling with reading or understanding.
1285
+ It captures electronic evidence of explicit actions and can import provider signature receipts. It does not call a checkbox a qualified electronic signature.
1654
1286
 
1655
- ### Should I record IP addresses and geolocation?
1287
+ ### Does the user have to scroll through the document?
1656
1288
 
1657
- Only for policies with a present, documented purpose and reviewed access/retention posture. They can corroborate request context but do not repair weak notice or prove identity/physical location. All such fields default off.
1289
+ No and `clickwrap` never equates scrolling with reading. It makes documents available before action and records the exact presentation. A policy can require an observed open/review interaction if your app truly needs one.
1658
1290
 
1659
- ### Can I use Clickwrap without Devise?
1291
+ ### Should I record IP addresses?
1660
1292
 
1661
- Yes. Devise and Rails authentication are convenience adapters over the same public capture APIs.
1293
+ Only for policies with a real, documented purpose. They corroborate request context; they don't prove identity or location. Everything defaults off.
1662
1294
 
1663
- ### Can one policy contain several documents and statements?
1295
+ ### Can I keep my domain models?
1664
1296
 
1665
- Yes. The receipt preserves each document/version, statement, choice, and ordering independently. Agreement, acknowledgment, and optional consent controls remain semantically separate even when one page presents them together.
1297
+ Yes, and you should. `clickwrap` owns presentation, evidence, lifecycle, and receipts not your payout, eligibility, or employment rules.
1666
1298
 
1667
- ### Can I keep my domain-specific declaration or authorization model?
1668
-
1669
- Yes—and usually should. Clickwrap complements domain models; it does not replace your payout, certification, identity, employment, or eligibility rules.
1670
-
1671
- ### Can Clickwrap prove the user saw the page?
1672
-
1673
- It can prove the server generated and accepted a bound presentation manifest and record accurately observed interactions. It cannot prove human attention, comprehension, exact pixels, or legal sufficiency from a database row.
1299
+ ### Is this GDPR compliant?
1674
1300
 
1675
- ### What happens if Clickwrap is temporarily unavailable?
1301
+ No gem can answer that. `clickwrap` gives you privacy-aware mechanisms, truthful defaults, and an inventory of exactly what you configured. Lawful basis, necessity, and data-subject rights remain yours.
1676
1302
 
1677
- Required evidence fails closed: the same-database protected action rolls back. Optional after-commit hooks fail independently and are reported. Applications can define deliberate emergency/system exemptions with explicit actor and reason; there is no silent rescue-and-continue path.
1303
+ ## Compatibility
1678
1304
 
1679
- ### Can I delete evidence?
1305
+ - Ruby 3.2+, Rails 7.1 through 8.x
1306
+ - PostgreSQL, SQLite, and MySQL for all portable core behavior (hardening is adapter-specific and labeled)
1307
+ - Integer and UUID primary keys; Devise and Rails authentication both optional
1308
+ - Runtime dependencies are only the Rails components the gem actually uses (`activerecord`, `actionpack`, `actionview`, `activesupport`, `railties`) — never Redis, a job backend, a JS runtime, or an external service
1680
1309
 
1681
- Yes, according to explicit retention/disposition policy and legal holds. Optional request evidence is separately disposable. Core historical evidence is never silently deleted through an actor association, and disposition is itself recorded.
1310
+ Persisted evidence gets a stricter promise than semver: every released receipt schema has a permanent golden fixture, new versions keep verifying old receipts, and released migrations are never edited underneath your app see [Stability and upgrade promise](#stability-and-upgrade-promise).
1682
1311
 
1683
- ### Is this GDPR compliant?
1312
+ ## Stability and upgrade promise
1684
1313
 
1685
- No gem can answer that universally. Clickwrap provides privacy-aware mechanisms and truthful defaults. The host remains responsible for lawful basis, necessity, transparency, data-subject rights, security, retention, processors/transfers, DPIAs, and jurisdiction-specific requirements.
1314
+ `clickwrap` follows semantic versioning for its Ruby APIs. Evidence formats are stricter: a format change gets a new explicit schema and verifier, never a silent reinterpretation; upgrade generators add migrations and report their effects; and deprecations name their replacement and remain executable for a documented window.
1686
1315
 
1687
1316
  ## Development
1688
1317
 
1689
1318
  ```bash
1690
1319
  bin/setup
1691
- bin/test
1692
- bin/rubocop
1693
- bin/rails test
1320
+ bundle exec rake test
1321
+ bundle exec rubocop
1694
1322
  ```
1695
1323
 
1696
- The project uses Minitest, a dummy Rails application, SimpleCov, RuboCop, Appraisal matrices, SQLite/PostgreSQL/MySQL integration lanes, concurrency/fault tests, generator tests, Brakeman where relevant, and independent receipt-verifier golden fixtures.
1697
-
1698
- Every change to canonicalization, schema, receipts, migrations, cryptographic fields, or lifecycle behavior must prove backward verification against all released fixtures.
1324
+ The project uses Minitest with a dummy Rails app, SimpleCov, RuboCop, Appraisal matrices, and SQLite/PostgreSQL/MySQL CI lanes. Fault-injection, concurrency, replay, stale-token, disposition, and golden-receipt tests are load-bearing, not extras.
1699
1325
 
1700
1326
  ## Contributing
1701
1327
 
1702
- Bug reports and focused pull requests are welcome once the repository opens for implementation. Changes to public vocabulary or evidence claims require corresponding documentation, source review, migration/compatibility analysis, and proof-integration coverage.
1328
+ Bug reports and focused pull requests are welcome at https://github.com/rameerez/clickwrap. Please run `bundle exec rake test` and `bundle exec rubocop` first.
1703
1329
 
1704
- Please do not use issues to request jurisdiction-specific legal advice or ask maintainers to approve legal text.
1330
+ Two kinds of change need extra care: anything touching public vocabulary or an evidence claim (docs change alongside code, plus a note on receipts already written), and anything touching canonicalization, receipt schemas, digests, or migrations — released evidence formats are permanent. Security reports go through [`SECURITY.md`](SECURITY.md), privately.
1705
1331
 
1706
1332
  ## License
1707
1333
 
1708
- MIT.
1334
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).